From 310876cd3b367e16448e04cca050c45b77829e59 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 4 Sep 2024 15:49:37 +0100 Subject: [PATCH 001/224] 2755 Add ability to extend HostNode, NetworkNode, Service and Application outside PrimAITE. --- src/primaite/config/load.py | 14 + src/primaite/game/game.py | 40 +- src/primaite/simulator/network/container.py | 13 + .../simulator/network/hardware/base.py | 2 +- .../network/hardware/nodes/host/host_node.py | 22 +- .../hardware/nodes/network/network_node.py | 21 +- .../system/applications/application.py | 10 +- .../simulator/system/services/service.py | 22 +- tests/assets/configs/extended_config.yaml | 951 ++++++++++++++++++ ...software_installation_and_configuration.py | 2 +- .../test_software_fix_duration.py | 2 +- .../applications/extended_application.py | 220 ++++ .../extensions/nodes/giga_switch.py | 121 +++ .../extensions/nodes/super_computer.py | 43 + .../extensions/services/extended_service.py | 426 ++++++++ .../extensions/test_extendable_config.py | 32 + .../test_application_registry.py | 4 +- 17 files changed, 1926 insertions(+), 19 deletions(-) create mode 100644 tests/assets/configs/extended_config.yaml create mode 100644 tests/integration_tests/extensions/applications/extended_application.py create mode 100644 tests/integration_tests/extensions/nodes/giga_switch.py create mode 100644 tests/integration_tests/extensions/nodes/super_computer.py create mode 100644 tests/integration_tests/extensions/services/extended_service.py create mode 100644 tests/integration_tests/extensions/test_extendable_config.py diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index 144e0733..b00c26f6 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -59,3 +59,17 @@ def data_manipulation_marl_config_path() -> Path: _LOGGER.error(msg) raise FileNotFoundError(msg) return path + +def get_extended_config_path() -> Path: + """ + Get the path to an 'extended' example config that contains nodes using the extension framework + + :return: Path to the extended example config + :rtype: Path + """ + path = _EXAMPLE_CFG / "extended_config.yaml" + if not path.exists(): + msg = f"Example config does not exist: {path}. Have you run `primaite setup`?" + _LOGGER.error(msg) + raise FileNotFoundError(msg) + return path \ No newline at end of file diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 045b2467..11c968af 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -20,9 +20,10 @@ from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.airspace import AirSpaceFrequency from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.host.host_node import NIC +from primaite.simulator.network.hardware.nodes.host.host_node import NIC, HostNode from primaite.simulator.network.hardware.nodes.host.server import Printer, Server from primaite.simulator.network.hardware.nodes.network.firewall import Firewall +from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter @@ -278,8 +279,25 @@ class PrimaiteGame: for node_cfg in nodes_cfg: n_type = node_cfg["type"] + new_node = None - if n_type == "computer": + # Handle extended nodes + if n_type.lower() in HostNode._registry: + new_node = HostNode._registry[n_type]( + hostname=node_cfg["hostname"], + ip_address=node_cfg["ip_address"], + subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), + default_gateway=node_cfg.get("default_gateway"), + dns_server=node_cfg.get("dns_server", None), + operating_state=NodeOperatingState.ON + if not (p := node_cfg.get("operating_state")) + else NodeOperatingState[p.upper()]) + elif n_type in NetworkNode._registry: + new_node = NetworkNode._registry[n_type]( + **node_cfg + ) + # Default PrimAITE nodes + elif n_type == "computer": new_node = Computer( hostname=node_cfg["hostname"], ip_address=node_cfg["ip_address"], @@ -351,10 +369,18 @@ class PrimaiteGame: for service_cfg in node_cfg["services"]: new_service = None service_type = service_cfg["type"] - if service_type in SERVICE_TYPES_MAPPING: + + service_class = None + # Handle extended services + if service_type.lower() in Service._registry: + service_class = Service._registry[service_type.lower()] + elif service_type in SERVICE_TYPES_MAPPING: + service_class = SERVICE_TYPES_MAPPING[service_type] + + if service_class is not None: _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") - new_node.software_manager.install(SERVICE_TYPES_MAPPING[service_type]) - new_service = new_node.software_manager.software[service_type] + new_node.software_manager.install(service_class) + new_service = new_node.software_manager.software[service_class.__name__] # fixing duration for the service if "fix_duration" in service_cfg.get("options", {}): @@ -398,8 +424,8 @@ class PrimaiteGame: new_application = None application_type = application_cfg["type"] - if application_type in Application._application_registry: - new_node.software_manager.install(Application._application_registry[application_type]) + if application_type in Application._registry: + new_node.software_manager.install(Application._registry[application_type]) new_application = new_node.software_manager.software[application_type] # grab the instance # fixing duration for the application diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 0408acde..39fbe783 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -12,7 +12,9 @@ from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.network.airspace import AirSpace from primaite.simulator.network.hardware.base import Link, Node, WiredNetworkInterface +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.network.hardware.nodes.host.server import Printer +from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.service import Service @@ -128,6 +130,16 @@ class Network(SimComponent): def firewall_nodes(self) -> List[Node]: """The Firewalls in the Network.""" return [node for node in self.nodes.values() if node.__class__.__name__ == "Firewall"] + + @property + def extended_hostnodes(self) -> List[Node]: + """Extended nodes that inherited HostNode in the network""" + return [node for node in self.nodes.values() if node.__class__.__name__.lower() in HostNode._registry] + + @property + def extended_networknodes(self) -> List[Node]: + """Extended nodes that inherited NetworkNode in the network""" + return [node for node in self.nodes.values() if node.__class__.__name__.lower() in NetworkNode._registry] @property def printer_nodes(self) -> List[Node]: @@ -160,6 +172,7 @@ class Network(SimComponent): "Printer": self.printer_nodes, "Wireless Router": self.wireless_router_nodes, } + if nodes: table = PrettyTable(["Node", "Type", "Operating State"]) if markdown: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ef2d47c3..bf230e07 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1699,7 +1699,7 @@ class Node(SimComponent): if self.software_manager.software.get(application_name): self.sys_log.warning(f"Can't install {application_name}. It's already installed.") return RequestResponse(status="success", data={"reason": "already installed"}) - application_class = Application._application_registry[application_name] + application_class = Application._registry[application_name] self.software_manager.install(application_class) application_instance = self.software_manager.software.get(application_name) self.applications[application_instance.uuid] = application_instance diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index c197d30b..ea162e88 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -2,7 +2,7 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Optional +from typing import Any, ClassVar, Dict, Optional, Type from primaite import getLogger from primaite.simulator.network.hardware.base import ( @@ -325,10 +325,30 @@ class HostNode(Node): network_interface: Dict[int, NIC] = {} "The NICs on the node by port id." + _registry: ClassVar[Dict[str, Type["HostNode"]]] = {} + """Registry of application types. Automatically populated when subclasses are defined.""" + def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask)) + def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + """ + Register a hostnode type. + + :param identifier: Uniquely specifies an hostnode class by name. Used for finding items by config. + :type identifier: str + :raises ValueError: When attempting to register an hostnode with a name that is already allocated. + """ + if identifier == 'default': + return + # Enforce lowercase registry entries because it makes comparisons everywhere else much easier. + identifier = identifier.lower() + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Tried to define new hostnode {identifier}, but this name is already reserved.") + cls._registry[identifier] = cls + @property def nmap(self) -> Optional[NMAP]: """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 5ff791cc..6515bb02 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod -from typing import Optional +from typing import Any, ClassVar, Dict, Optional, Type from primaite.simulator.network.hardware.base import NetworkInterface, Node from primaite.simulator.network.transmission.data_link_layer import Frame @@ -16,6 +16,25 @@ class NetworkNode(Node): provide functionality for receiving and processing frames received on their network interfaces. """ + _registry: ClassVar[Dict[str, Type["NetworkNode"]]] = {} + """Registry of application types. Automatically populated when subclasses are defined.""" + + def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + """ + Register a networknode type. + + :param identifier: Uniquely specifies an networknode class by name. Used for finding items by config. + :type identifier: str + :raises ValueError: When attempting to register an networknode with a name that is already allocated. + """ + if identifier == 'default': + return + identifier = identifier.lower() + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Tried to define new networknode {identifier}, but this name is already reserved.") + cls._registry[identifier] = cls + @abstractmethod def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): """ diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 741f491d..b5284968 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -41,10 +41,10 @@ class Application(IOSoftware): install_countdown: Optional[int] = None "The countdown to the end of the installation process. None if not currently installing" - _application_registry: ClassVar[Dict[str, Type["Application"]]] = {} + _registry: ClassVar[Dict[str, Type["Application"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" - def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: """ Register an application type. @@ -52,10 +52,12 @@ class Application(IOSoftware): :type identifier: str :raises ValueError: When attempting to register an application with a name that is already allocated. """ + if identifier == 'default': + return super().__init_subclass__(**kwargs) - if identifier in cls._application_registry: + if identifier in cls._registry: raise ValueError(f"Tried to define new application {identifier}, but this name is already reserved.") - cls._application_registry[identifier] = cls + cls._registry[identifier] = cls def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 5adea6e7..74dcb506 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import abstractmethod from enum import Enum -from typing import Any, Dict, Optional +from typing import Any, ClassVar, Dict, Optional, Type from primaite import getLogger from primaite.interface.request import RequestFormat, RequestResponse @@ -46,9 +46,29 @@ class Service(IOSoftware): restart_countdown: Optional[int] = None "If currently restarting, how many timesteps remain until the restart is finished." + _registry: ClassVar[Dict[str, Type["Service"]]] = {} + """Registry of service types. Automatically populated when subclasses are defined.""" + def __init__(self, **kwargs): super().__init__(**kwargs) + def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + """ + Register a hostnode type. + + :param identifier: Uniquely specifies an hostnode class by name. Used for finding items by config. + :type identifier: str + :raises ValueError: When attempting to register an hostnode with a name that is already allocated. + """ + if identifier == 'default': + return + # Enforce lowercase registry entries because it makes comparisons everywhere else much easier. + identifier = identifier.lower() + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Tried to define new hostnode {identifier}, but this name is already reserved.") + cls._registry[identifier] = cls + def _can_perform_action(self) -> bool: """ Checks if the service can perform actions. diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml new file mode 100644 index 00000000..e1a06938 --- /dev/null +++ b/tests/assets/configs/extended_config.yaml @@ -0,0 +1,951 @@ +io_settings: + save_agent_actions: true + save_step_metadata: false + save_pcap_logs: false + save_sys_logs: false + sys_log_level: WARNING + + +game: + max_episode_length: 128 + ports: + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + thresholds: + nmne: + high: 10 + medium: 5 + low: 0 + +agents: + - ref: client_2_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: null + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_2 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 + + - ref: client_1_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: null + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_1 + + + + + + - ref: data_manipulation_attacker + team: RED + type: RedDatabaseCorruptingAgent + + observation_space: null + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: DataManipulationBot + - node_name: client_2 + applications: + - application_name: DataManipulationBot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + + - ref: defender + team: BLUE + type: ProxyAgent + + observation_space: + type: CUSTOM + options: + components: + - type: NODES + label: NODES + options: + hosts: + - hostname: domain_controller + - hostname: web_server + services: + - service_name: WebServer + - hostname: database_server + folders: + - folder_name: database + files: + - file_name: database.db + - hostname: backup_server + - hostname: security_suite + - hostname: client_1 + - hostname: client_2 + num_services: 1 + num_applications: 0 + num_folders: 1 + num_files: 1 + num_nics: 2 + include_num_access: false + include_nmne: true + monitored_traffic: + icmp: + - NONE + tcp: + - DNS + routers: + - hostname: router_1 + num_ports: 0 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + wildcard_list: + - 0.0.0.1 + port_list: + - 80 + - 5432 + protocol_list: + - ICMP + - TCP + - UDP + num_rules: 10 + + - type: LINKS + label: LINKS + options: + link_references: + - router_1:eth-1<->switch_1:eth-8 + - router_1:eth-2<->switch_2:eth-8 + - switch_1:eth-1<->domain_controller:eth-1 + - switch_1:eth-2<->web_server:eth-1 + - switch_1:eth-3<->database_server:eth-1 + - switch_1:eth-4<->backup_server:eth-1 + - switch_1:eth-7<->security_suite:eth-1 + - switch_2:eth-1<->client_1:eth-1 + - switch_2:eth-2<->client_2:eth-1 + - switch_2:eth-7<->security_suite:eth-2 + - type: "NONE" + label: ICS + options: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_SERVICE_SCAN + - type: NODE_SERVICE_STOP + - type: NODE_SERVICE_START + - type: NODE_SERVICE_PAUSE + - type: NODE_SERVICE_RESUME + - type: NODE_SERVICE_RESTART + - type: NODE_SERVICE_DISABLE + - type: NODE_SERVICE_ENABLE + - type: NODE_SERVICE_FIX + - type: NODE_FILE_SCAN + - type: NODE_FILE_CHECKHASH + - type: NODE_FILE_DELETE + - type: NODE_FILE_REPAIR + - type: NODE_FILE_RESTORE + - type: NODE_FOLDER_SCAN + - type: NODE_FOLDER_CHECKHASH + - type: NODE_FOLDER_REPAIR + - type: NODE_FOLDER_RESTORE + - type: NODE_OS_SCAN + - type: NODE_SHUTDOWN + - type: NODE_STARTUP + - type: NODE_RESET + - type: ROUTER_ACL_ADDRULE + - type: ROUTER_ACL_REMOVERULE + - type: HOST_NIC_ENABLE + - type: HOST_NIC_DISABLE + + action_map: + 0: + action: DONOTHING + options: {} + # scan webapp service + 1: + action: NODE_SERVICE_SCAN + options: + node_id: 1 + service_id: 0 + # stop webapp service + 2: + action: NODE_SERVICE_STOP + options: + node_id: 1 + service_id: 0 + # start webapp service + 3: + action: "NODE_SERVICE_START" + options: + node_id: 1 + service_id: 0 + 4: + action: "NODE_SERVICE_PAUSE" + options: + node_id: 1 + service_id: 0 + 5: + action: "NODE_SERVICE_RESUME" + options: + node_id: 1 + service_id: 0 + 6: + action: "NODE_SERVICE_RESTART" + options: + node_id: 1 + service_id: 0 + 7: + action: "NODE_SERVICE_DISABLE" + options: + node_id: 1 + service_id: 0 + 8: + action: "NODE_SERVICE_ENABLE" + options: + node_id: 1 + service_id: 0 + 9: # check database.db file + action: "NODE_FILE_SCAN" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 10: + action: "NODE_FILE_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 11: + action: "NODE_FILE_DELETE" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 12: + action: "NODE_FILE_REPAIR" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 13: + action: "NODE_SERVICE_FIX" + options: + node_id: 2 + service_id: 0 + 14: + action: "NODE_FOLDER_SCAN" + options: + node_id: 2 + folder_id: 0 + 15: + action: "NODE_FOLDER_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + options: + node_id: 2 + folder_id: 0 + 16: + action: "NODE_FOLDER_REPAIR" + options: + node_id: 2 + folder_id: 0 + 17: + action: "NODE_FOLDER_RESTORE" + options: + node_id: 2 + folder_id: 0 + 18: + action: "NODE_OS_SCAN" + options: + node_id: 0 + 19: + action: "NODE_SHUTDOWN" + options: + node_id: 0 + 20: + action: NODE_STARTUP + options: + node_id: 0 + 21: + action: NODE_RESET + options: + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 1 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 1 # ALL + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 2 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 1 # ALL + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 48: # old action num: 24 # block tcp traffic from client 1 to web app + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 3 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 3 # web server + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 49: # old action num: 25 # block tcp traffic from client 2 to web app + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 4 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 3 # web server + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 50: # old action num: 26 + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 5 + permission: 2 + source_ip_id: 7 # client 1 + dest_ip_id: 4 # database + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 51: # old action num: 27 + action: "ROUTER_ACL_ADDRULE" + options: + target_router: router_1 + position: 6 + permission: 2 + source_ip_id: 8 # client 2 + dest_ip_id: 4 # database + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + source_wildcard_id: 0 + dest_wildcard_id: 0 + 52: # old action num: 28 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 0 + 53: # old action num: 29 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 1 + 54: # old action num: 30 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 2 + 55: # old action num: 31 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 3 + 56: # old action num: 32 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 4 + 57: # old action num: 33 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 5 + 58: # old action num: 34 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 6 + 59: # old action num: 35 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 7 + 60: # old action num: 36 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 8 + 61: # old action num: 37 + action: "ROUTER_ACL_REMOVERULE" + options: + target_router: router_1 + position: 9 + 62: # old action num: 38 + action: "HOST_NIC_DISABLE" + options: + node_id: 0 + nic_id: 0 + 63: # old action num: 39 + action: "HOST_NIC_ENABLE" + options: + node_id: 0 + nic_id: 0 + 64: # old action num: 40 + action: "HOST_NIC_DISABLE" + options: + node_id: 1 + nic_id: 0 + 65: # old action num: 41 + action: "HOST_NIC_ENABLE" + options: + node_id: 1 + nic_id: 0 + 66: # old action num: 42 + action: "HOST_NIC_DISABLE" + options: + node_id: 2 + nic_id: 0 + 67: # old action num: 43 + action: "HOST_NIC_ENABLE" + options: + node_id: 2 + nic_id: 0 + 68: # old action num: 44 + action: "HOST_NIC_DISABLE" + options: + node_id: 3 + nic_id: 0 + 69: # old action num: 45 + action: "HOST_NIC_ENABLE" + options: + node_id: 3 + nic_id: 0 + 70: # old action num: 46 + action: "HOST_NIC_DISABLE" + options: + node_id: 4 + nic_id: 0 + 71: # old action num: 47 + action: "HOST_NIC_ENABLE" + options: + node_id: 4 + nic_id: 0 + 72: # old action num: 48 + action: "HOST_NIC_DISABLE" + options: + node_id: 4 + nic_id: 1 + 73: # old action num: 49 + action: "HOST_NIC_ENABLE" + options: + node_id: 4 + nic_id: 1 + 74: # old action num: 50 + action: "HOST_NIC_DISABLE" + options: + node_id: 5 + nic_id: 0 + 75: # old action num: 51 + action: "HOST_NIC_ENABLE" + options: + node_id: 5 + nic_id: 0 + 76: # old action num: 52 + action: "HOST_NIC_DISABLE" + options: + node_id: 6 + nic_id: 0 + 77: # old action num: 53 + action: "HOST_NIC_ENABLE" + options: + node_id: 6 + nic_id: 0 + + + + options: + nodes: + - node_name: domain_controller + - node_name: web_server + applications: + - application_name: DatabaseClient + services: + - service_name: WebServer + - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db + services: + - service_name: DatabaseService + - node_name: backup_server + - node_name: security_suite + - node_name: client_1 + - node_name: client_2 + + max_folders_per_node: 2 + max_files_per_folder: 2 + max_services_per_node: 2 + max_nics_per_node: 8 + max_acl_rules: 10 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + + + reward_function: + reward_components: + - type: DATABASE_FILE_INTEGRITY + weight: 0.40 + options: + node_hostname: database_server + folder_name: database + file_name: database.db + + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_1_green_user + + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_2_green_user + + agent_settings: + flatten_obs: true + action_masking: true + + + + + +simulation: + network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE + nodes: + + - hostname: router_1 + type: router + num_ports: 5 + ports: + 1: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + acl: + 18: + action: PERMIT + src_port: POSTGRES_SERVER + dst_port: POSTGRES_SERVER + 19: + action: PERMIT + src_port: DNS + dst_port: DNS + 20: + action: PERMIT + src_port: FTP + dst_port: FTP + 21: + action: PERMIT + src_port: HTTP + dst_port: HTTP + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + + - hostname: switch_1 + type: switch + num_ports: 8 + + - hostname: switch_2 + type: gigaswitch + num_ports: 8 + + - hostname: domain_controller + type: server + ip_address: 192.168.1.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + services: + - type: DNSServer + options: + domain_mapping: + arcd.com: 192.168.1.12 # web server + + - hostname: web_server + type: server + ip_address: 192.168.1.12 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - type: WebServer + applications: + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + + + - hostname: database_server + type: server + ip_address: 192.168.1.14 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - type: FTPClient + + - hostname: backup_server + type: server + ip_address: 192.168.1.16 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - type: FTPServer + + - hostname: security_suite + type: server + ip_address: 192.168.1.110 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + network_interfaces: + 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot + ip_address: 192.168.10.110 + subnet_mask: 255.255.255.0 + + - hostname: client_1 + type: supercomputer + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - type: WebBrowser + options: + target_url: http://arcd.com/users/ + - type: ExtendedApplication + options: + target_url: http://arcd.com/users/ + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + services: + - type: DNSClient + - type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - type: ExtendedService + options: + backup_server_ip: 192.168.1.16 + + - hostname: client_2 + type: computer + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - type: WebBrowser + options: + target_url: http://arcd.com/users/ + - type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + services: + - type: DNSClient + + links: + - endpoint_a_hostname: router_1 + endpoint_a_port: 1 + endpoint_b_hostname: switch_1 + endpoint_b_port: 8 + - endpoint_a_hostname: router_1 + endpoint_a_port: 2 + endpoint_b_hostname: switch_2 + endpoint_b_port: 8 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 1 + endpoint_b_hostname: domain_controller + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 2 + endpoint_b_hostname: web_server + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 3 + endpoint_b_hostname: database_server + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 4 + endpoint_b_hostname: backup_server + endpoint_b_port: 1 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 7 + endpoint_b_hostname: security_suite + endpoint_b_port: 1 + - endpoint_a_hostname: switch_2 + endpoint_a_port: 1 + endpoint_b_hostname: client_1 + endpoint_b_port: 1 + - endpoint_a_hostname: switch_2 + endpoint_a_port: 2 + endpoint_b_hostname: client_2 + endpoint_b_port: 1 + - endpoint_a_hostname: switch_2 + endpoint_a_port: 7 + endpoint_b_hostname: security_suite + endpoint_b_port: 2 diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index 3e06d371..a642564c 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -86,7 +86,7 @@ def test_node_software_install(): assert client_2.software_manager.software.get(software.__name__) is not None # check that applications have been installed on client 1 - for applications in Application._application_registry: + for applications in Application._registry: assert client_1.software_manager.software.get(applications) is not None # check that services have been installed on client 1 diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py index dd38fafd..168ebee0 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py @@ -51,7 +51,7 @@ def test_fix_duration_set_from_config(): # in config - applications take 1 timestep to fix # remove test applications from list - applications = set(Application._application_registry) - set(TestApplications) + applications = set(Application._registry) - set(TestApplications) for application in ["RansomwareScript", "WebBrowser", "DataManipulationBot", "DoSBot", "DatabaseClient"]: assert client_1.software_manager.software.get(application) is not None diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py new file mode 100644 index 00000000..c9b3006d --- /dev/null +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -0,0 +1,220 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from enum import Enum +from ipaddress import IPv4Address +from typing import Dict, List, Optional +from urllib.parse import urlparse + +from pydantic import BaseModel, ConfigDict + +from primaite import getLogger +from primaite.interface.request import RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.http import ( + HttpRequestMethod, + HttpRequestPacket, + HttpResponsePacket, + HttpStatusCode, +) +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import Application +from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.simulator.system.services.dns.dns_client import DNSClient + +_LOGGER = getLogger(__name__) + + +class ExtendedApplication(Application, identifier="ExtendedApplication"): + """ + Clone of web browser that uses the extension framework instead of being part of PrimAITE directly. + + The application requests and loads web pages using its domain name and requesting IP addresses using DNS. + """ + + target_url: Optional[str] = None + + domain_name_ip_address: Optional[IPv4Address] = None + "The IP address of the domain name for the webpage." + + latest_response: Optional[HttpResponsePacket] = None + """Keeps track of the latest HTTP response.""" + + history: List["BrowserHistoryItem"] = [] + """Keep a log of visited websites and information about the visit, such as response code.""" + + def __init__(self, **kwargs): + kwargs["name"] = "ExtendedApplication" + kwargs["protocol"] = IPProtocol.TCP + # default for web is port 80 + if kwargs.get("port") is None: + kwargs["port"] = Port.HTTP + + super().__init__(**kwargs) + self.run() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + rm.add_request( + name="execute", + request_type=RequestType( + func=lambda request, context: RequestResponse.from_bool(self.get_webpage()) + ), # noqa + ) + + return rm + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of the WebBrowser. + + :return: A dictionary capturing the current state of the WebBrowser and its child objects. + """ + state = super().describe_state() + state["history"] = [hist_item.state() for hist_item in self.history] + return state + + def get_webpage(self, url: Optional[str] = None) -> bool: + """ + Retrieve the webpage. + + This should send a request to the web server which also requests for a list of users + + :param: url: The address of the web page the browser requests + :type: url: str + """ + url = url or self.target_url + if not self._can_perform_action(): + return False + + self.num_executions += 1 # trying to connect counts as an execution + + # reset latest response + self.latest_response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND) + + try: + parsed_url = urlparse(url) + except Exception: + self.sys_log.warning(f"{url} is not a valid URL") + return False + + # get the IP address of the domain name via DNS + dns_client: DNSClient = self.software_manager.software.get("DNSClient") + domain_exists = dns_client.check_domain_exists(target_domain=parsed_url.hostname) + + # if domain does not exist, the request fails + if domain_exists: + # set current domain name IP address + self.domain_name_ip_address = dns_client.dns_cache[parsed_url.hostname] + else: + # check if url is an ip address + try: + self.domain_name_ip_address = IPv4Address(parsed_url.hostname) + except Exception: + # unable to deal with this request + self.sys_log.warning(f"{self.name}: Unable to resolve URL {url}") + return False + + # create HTTPRequest payload + payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url=url) + + # send request - As part of the self.send call, a response will be received and stored in the + # self.latest_response variable + if self.send( + payload=payload, + dest_ip_address=self.domain_name_ip_address, + dest_port=parsed_url.port if parsed_url.port else Port.HTTP, + ): + self.sys_log.info( + f"{self.name}: Received HTTP {payload.request_method.name} " + f"Response {payload.request_url} - {self.latest_response.status_code.value}" + ) + self.history.append( + WebBrowser.BrowserHistoryItem( + url=url, + status=self.BrowserHistoryItem._HistoryItemStatus.LOADED, + response_code=self.latest_response.status_code, + ) + ) + return self.latest_response.status_code is HttpStatusCode.OK + else: + self.sys_log.warning(f"{self.name}: Error sending Http Packet") + self.sys_log.debug(f"{self.name}: {payload=}") + self.history.append( + WebBrowser.BrowserHistoryItem( + url=url, status=self.BrowserHistoryItem._HistoryItemStatus.SERVER_UNREACHABLE + ) + ) + return False + + def send( + self, + payload: HttpRequestPacket, + dest_ip_address: Optional[IPv4Address] = None, + dest_port: Optional[Port] = Port.HTTP, + session_id: Optional[str] = None, + **kwargs, + ) -> bool: + """ + Sends a payload to the SessionManager. + + :param payload: The payload to be sent. + :param dest_ip_address: The ip address of the payload destination. + :param dest_port: The port of the payload destination. + :param session_id: The Session ID the payload is to originate from. Optional. + + :return: True if successful, False otherwise. + """ + self.sys_log.info(f"{self.name}: Sending HTTP {payload.request_method.name} {payload.request_url}") + + return super().send( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs + ) + + def receive(self, payload: HttpResponsePacket, session_id: Optional[str] = None, **kwargs) -> bool: + """ + Receives a payload from the SessionManager. + + :param payload: The payload to be sent. + :param session_id: The Session ID the payload is to originate from. Optional. + :return: True if successful, False otherwise. + """ + if not isinstance(payload, HttpResponsePacket): + self.sys_log.warning(f"{self.name} received a packet that is not an HttpResponsePacket") + self.sys_log.debug(f"{self.name}: {payload=}") + return False + self.sys_log.info(f"{self.name}: Received HTTP {payload.status_code.value}") + self.latest_response = payload + return True + + class BrowserHistoryItem(BaseModel): + """Simple representation of browser history, used for tracking success of web requests to calculate rewards.""" + + model_config = ConfigDict(extra="forbid") + """Error if incorrect specification.""" + + url: str + """The URL that was attempted to be fetched by the browser""" + + class _HistoryItemStatus(Enum): + NOT_SENT = "NOT_SENT" + PENDING = "PENDING" + SERVER_UNREACHABLE = "SERVER_UNREACHABLE" + LOADED = "LOADED" + + status: _HistoryItemStatus = _HistoryItemStatus.PENDING + + response_code: Optional[HttpStatusCode] = None + """HTTP response code that was received, or PENDING if a response was not yet received.""" + + def state(self) -> Dict: + """Return the contents of this dataclass as a dict for use with describe_state method.""" + if self.status == self._HistoryItemStatus.LOADED: + outcome = self.response_code.value + else: + outcome = self.status.value + return {"url": self.url, "outcome": outcome} diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py new file mode 100644 index 00000000..b86bea7d --- /dev/null +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -0,0 +1,121 @@ +from typing import Dict + +from prettytable import MARKDOWN, PrettyTable + +from primaite import _LOGGER +from primaite.exceptions import NetworkError +from primaite.simulator.network.hardware.base import Link +from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode +from primaite.simulator.network.hardware.nodes.network.switch import SwitchPort +from primaite.simulator.network.transmission.data_link_layer import Frame + + +class GigaSwitch(NetworkNode, identifier="gigaswitch"): + """ + A class representing a Layer 2 network switch. + + :ivar num_ports: The number of ports on the switch. Default is 24. + """ + + num_ports: int = 24 + "The number of ports on the switch." + network_interfaces: Dict[str, SwitchPort] = {} + "The SwitchPorts on the Switch." + network_interface: Dict[int, SwitchPort] = {} + "The SwitchPorts on the Switch by port id." + mac_address_table: Dict[str, SwitchPort] = {} + "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." + + def __init__(self, **kwargs): + print('--- Extended Component: GigaSwitch ---') + super().__init__(**kwargs) + for i in range(1, self.num_ports + 1): + self.connect_nic(SwitchPort()) + + def _install_system_software(self): + pass + + def show(self, markdown: bool = False): + """ + Prints a table of the SwitchPorts on the Switch. + + :param markdown: If True, outputs the table in markdown format. Default is False. + """ + table = PrettyTable(["Port", "MAC Address", "Speed", "Status"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.hostname} Switch Ports" + for port_num, port in self.network_interface.items(): + table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) + print(table) + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + """ + state = super().describe_state() + state["ports"] = {port_num: port.describe_state() for port_num, port in self.network_interface.items()} + state["num_ports"] = self.num_ports # redundant? + state["mac_address_table"] = {mac: port.port_num for mac, port in self.mac_address_table.items()} + return state + + def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort): + """ + Private method to add an entry to the MAC address table. + + :param mac_address: MAC address to be added. + :param switch_port: Corresponding SwitchPort object. + """ + mac_table_port = self.mac_address_table.get(mac_address) + if not mac_table_port: + self.mac_address_table[mac_address] = switch_port + self.sys_log.info(f"Added MAC table entry: Port {switch_port.port_num} -> {mac_address}") + else: + if mac_table_port != switch_port: + self.mac_address_table.pop(mac_address) + self.sys_log.info(f"Removed MAC table entry: Port {mac_table_port.port_num} -> {mac_address}") + self._add_mac_table_entry(mac_address, switch_port) + + def receive_frame(self, frame: Frame, from_network_interface: SwitchPort): + """ + Forward a frame to the appropriate port based on the destination MAC address. + + :param frame: The Frame being received. + :param from_network_interface: The SwitchPort that received the frame. + """ + src_mac = frame.ethernet.src_mac_addr + dst_mac = frame.ethernet.dst_mac_addr + self._add_mac_table_entry(src_mac, from_network_interface) + + outgoing_port = self.mac_address_table.get(dst_mac) + if outgoing_port and dst_mac.lower() != "ff:ff:ff:ff:ff:ff": + outgoing_port.send_frame(frame) + else: + # If the destination MAC is not in the table, flood to all ports except incoming + for port in self.network_interface.values(): + if port.enabled and port != from_network_interface: + port.send_frame(frame) + + def disconnect_link_from_port(self, link: Link, port_number: int): + """ + Disconnect a given link from the specified port number on the switch. + + :param link: The Link object to be disconnected. + :param port_number: The port number on the switch from where the link should be disconnected. + :raise NetworkError: When an invalid port number is provided or the link does not match the connection. + """ + port = self.network_interface.get(port_number) + if port is None: + msg = f"Invalid port number {port_number} on the switch" + _LOGGER.error(msg) + raise NetworkError(msg) + + if port._connected_link != link: + msg = f"The link does not match the connection at port number {port_number}" + _LOGGER.error(msg) + raise NetworkError(msg) + + port.disconnect_link() diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py new file mode 100644 index 00000000..8a1465e9 --- /dev/null +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -0,0 +1,43 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import ClassVar, Dict + +from primaite.simulator.network.hardware.nodes.host.host_node import NIC, HostNode +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.utils.validators import IPV4Address + + +class SuperComputer(HostNode, identifier="supercomputer"): + """ + A basic Computer class. + + Example: + >>> pc_a = Computer( + hostname="pc_a", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1" + ) + >>> pc_a.power_on() + + Instances of computer come 'pre-packaged' with the following: + + * Core Functionality: + * Packet Capture + * Sys Log + * Services: + * ARP Service + * ICMP Service + * DNS Client + * FTP Client + * NTP Client + * Applications: + * Web Browser + """ + + SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} + + def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): + print('--- Extended Component: SuperComputer ---') + super().__init__(ip_address=ip_address, subnet_mask=subnet_mask, **kwargs) + + pass diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py new file mode 100644 index 00000000..3151571b --- /dev/null +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -0,0 +1,426 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address +from typing import Any, Dict, List, Literal, Optional, Union +from uuid import uuid4 + +from primaite import getLogger +from primaite.simulator.file_system.file_system import File +from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.file_system.folder import Folder +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.service import Service, ServiceOperatingState +from primaite.simulator.system.software import SoftwareHealthState + +_LOGGER = getLogger(__name__) + + +class ExtendedService(Service, identifier='extendedservice'): + """ + A copy of DatabaseService that uses the extension framework instead of being part of PrimAITE. + + This class inherits from the `Service` class and provides methods to simulate a SQL database. + """ + + password: Optional[str] = None + """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" + + backup_server_ip: IPv4Address = None + """IP address of the backup server.""" + + latest_backup_directory: str = None + """Directory of latest backup.""" + + latest_backup_file_name: str = None + """File name of latest backup.""" + + def __init__(self, **kwargs): + kwargs["name"] = "ExtendedService" + kwargs["port"] = Port.POSTGRES_SERVER + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + self._create_db_file() + if kwargs.get('options'): + opt = kwargs["options"] + self.password = opt.get("db_password", None) + if "backup_server_ip" in opt: + self.configure_backup(backup_server=IPv4Address(opt.get("backup_server_ip"))) + + def install(self): + """ + Perform first-time setup of the ExtendedService. + + Installs an instance of FTPClient on the Node to enable database backup if it isn't installed already. + """ + super().install() + + if not self.parent.software_manager.software.get("FTPClient"): + self.parent.sys_log.info(f"{self.name}: Installing FTPClient to enable database backups") + self.parent.software_manager.install(FTPClient) + + def configure_backup(self, backup_server: IPv4Address): + """ + Set up the database backup. + + :param: backup_server_ip: The IP address of the backup server + """ + self.backup_server_ip = backup_server + + def backup_database(self) -> bool: + """Create a backup of the database to the configured backup server.""" + # check if this action can be performed + if not self._can_perform_action(): + return False + + # check if the backup server was configured + if self.backup_server_ip is None: + self.sys_log.warning(f"{self.name} - {self.sys_log.hostname}: not configured.") + return False + + software_manager: SoftwareManager = self.software_manager + ftp_client_service: FTPClient = software_manager.software.get("FTPClient") + + if not ftp_client_service: + self.sys_log.error( + f"{self.name}: Failed to perform database backup as the FTPClient software is not installed" + ) + return False + + # send backup copy of database file to FTP server + if not self.db_file: + self.sys_log.error(f"{self.name}: Attempted to backup database file but it doesn't exist.") + return False + + response = ftp_client_service.send_file( + dest_ip_address=self.backup_server_ip, + src_file_name=self.db_file.name, + src_folder_name="database", + dest_folder_name=str(self.uuid), + # Prevent's a filename clash with the real DatabaseService service implementation + dest_file_name="extended_service_database.db", + ) + + if response: + return True + + self.sys_log.error("Unable to create database backup.") + return False + + def restore_backup(self) -> bool: + """Restore a backup from backup server.""" + # check if this action can be performed + if not self._can_perform_action(): + return False + + software_manager: SoftwareManager = self.software_manager + ftp_client_service: FTPClient = software_manager.software.get("FTPClient") + + if not ftp_client_service: + self.sys_log.error( + f"{self.name}: Failed to restore database backup as the FTPClient software is not installed" + ) + return False + + # retrieve backup file from backup server + response = ftp_client_service.request_file( + src_folder_name=str(self.uuid), + src_file_name="extended_service_database.db", + dest_folder_name="downloads", + dest_file_name="extended_service_database.db", + dest_ip_address=self.backup_server_ip, + ) + + if not response: + self.sys_log.error("Unable to restore database backup.") + return False + + old_visible_state = SoftwareHealthState.GOOD + + # get db file regardless of whether or not it was deleted + db_file = self.file_system.get_file(folder_name="database", file_name="extended_service_database.db", include_deleted=True) + + if db_file is None: + self.sys_log.warning("Database file not initialised.") + return False + + # if the file was deleted, get the old visible health state + if db_file.deleted: + old_visible_state = db_file.visible_health_status + else: + old_visible_state = self.db_file.visible_health_status + self.file_system.delete_file(folder_name="database", file_name="extended_service_database.db") + + # replace db file + self.file_system.copy_file(src_folder_name="downloads", src_file_name="extended_service_database.db", dst_folder_name="database") + + if self.db_file is None: + self.sys_log.error("Copying database backup failed.") + return False + + self.db_file.visible_health_status = old_visible_state + self.set_health_state(SoftwareHealthState.GOOD) + + return True + + def _create_db_file(self): + """Creates the Simulation File and sqlite file in the file system.""" + self.file_system.create_file(folder_name="database", file_name="extended_service_database.db") + + @property + def db_file(self) -> File: + """Returns the database file.""" + return self.file_system.get_file(folder_name="database", file_name="extended_service_database.db") + + def _return_database_folder(self) -> Folder: + """Returns the database folder.""" + return self.file_system.get_folder_by_id(self.db_file.folder_id) + + def _generate_connection_id(self) -> str: + """Generate a unique connection ID.""" + return str(uuid4()) + + def _process_connect( + self, + src_ip: IPv4Address, + connection_request_id: str, + password: Optional[str] = None, + session_id: Optional[str] = None, + ) -> Dict[str, Union[int, Dict[str, bool]]]: + """Process an incoming connection request. + + :param connection_id: A unique identifier for the connection + :type connection_id: str + :param password: Supplied password. It must match self.password for connection success, defaults to None + :type password: Optional[str], optional + :return: Response to connection request containing success info. + :rtype: Dict[str, Union[int, Dict[str, bool]]] + """ + self.sys_log.info(f"{self.name}: Processing new connection request ({connection_request_id}) from {src_ip}") + status_code = 500 # Default internal server error + connection_id = None + if self.operating_state == ServiceOperatingState.RUNNING: + status_code = 503 # service unavailable + if self.health_state_actual == SoftwareHealthState.OVERWHELMED: + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) from {src_ip} declined, service is at " + f"capacity." + ) + if self.health_state_actual in [ + SoftwareHealthState.GOOD, + SoftwareHealthState.FIXING, + SoftwareHealthState.COMPROMISED, + ]: + if self.password == password: + status_code = 200 # ok + connection_id = self._generate_connection_id() + # try to create connection + if not self.add_connection(connection_id=connection_id, session_id=session_id): + status_code = 500 + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) from {src_ip} declined, " + f"returning status code 500" + ) + else: + status_code = 401 # Unauthorised + self.sys_log.info( + f"{self.name}: Connection request ({connection_request_id}) from {src_ip} unauthorised " + f"(incorrect password), returning status code 401" + ) + else: + status_code = 404 # service not found + return { + "status_code": status_code, + "type": "connect_response", + "response": status_code == 200, + "connection_id": connection_id, + "connection_request_id": connection_request_id, + } + + def _process_sql( + self, + query: Literal["SELECT", "DELETE", "INSERT", "ENCRYPT"], + query_id: str, + connection_id: Optional[str] = None, + ) -> Dict[str, Union[int, List[Any]]]: + """ + Executes the given SQL query and returns the result. + + Possible queries: + - SELECT : returns the data + - DELETE : deletes the data + - INSERT : inserts the data + - ENCRYPT : corrupts the data + + :param query: The SQL query to be executed. + :return: Dictionary containing status code and data fetched. + """ + self.sys_log.info(f"{self.name}: Running {query}") + + if not self.db_file: + self.sys_log.error(f"{self.name}: Failed to run {query} because the database file is missing.") + return {"status_code": 404, "type": "sql", "data": False} + + if self.health_state_actual is not SoftwareHealthState.GOOD: + self.sys_log.error(f"{self.name}: Failed to run {query} because the database service is unavailable.") + return {"status_code": 500, "type": "sql", "data": False} + + if query == "SELECT": + if self.db_file.health_status == FileSystemItemHealthStatus.CORRUPT: + return { + "status_code": 200, + "type": "sql", + "data": False, + "uuid": query_id, + "connection_id": connection_id, + } + elif self.db_file.health_status == FileSystemItemHealthStatus.GOOD: + return { + "status_code": 200, + "type": "sql", + "data": True, + "uuid": query_id, + "connection_id": connection_id, + } + else: + return {"status_code": 404, "type": "sql", "data": False} + elif query == "DELETE": + self.db_file.health_status = FileSystemItemHealthStatus.COMPROMISED + return { + "status_code": 200, + "type": "sql", + "data": False, + "uuid": query_id, + "connection_id": connection_id, + } + elif query == "ENCRYPT": + self.file_system.num_file_creations += 1 + self.db_file.health_status = FileSystemItemHealthStatus.CORRUPT + self.db_file.num_access += 1 + database_folder = self._return_database_folder() + database_folder.health_status = FileSystemItemHealthStatus.CORRUPT + self.file_system.num_file_deletions += 1 + return { + "status_code": 200, + "type": "sql", + "data": False, + "uuid": query_id, + "connection_id": connection_id, + } + elif query == "INSERT": + if self.health_state_actual == SoftwareHealthState.GOOD: + return { + "status_code": 200, + "type": "sql", + "data": False, + "uuid": query_id, + "connection_id": connection_id, + } + else: + return {"status_code": 404, "type": "sql", "data": False} + elif query == "SELECT * FROM pg_stat_activity": + # Check if the connection is active. + if self.health_state_actual == SoftwareHealthState.GOOD: + return { + "status_code": 200, + "type": "sql", + "data": False, + "uuid": query_id, + "connection_id": connection_id, + } + else: + return {"status_code": 401, "data": False} + else: + # Invalid query + self.sys_log.warning(f"{self.name}: Invalid {query}") + return {"status_code": 500, "data": False} + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + return super().describe_state() + + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: + """ + Processes the incoming SQL payload and sends the result back. + + :param payload: The SQL query to be executed. + :param session_id: The session identifier. + :return: True if the Status Code is 200, otherwise False. + """ + result = {"status_code": 500, "data": []} + # if server service is down, return error + if not self._can_perform_action(): + return False + + if isinstance(payload, dict) and payload.get("type"): + if payload["type"] == "connect_request": + src_ip = kwargs.get("frame").ip.src_ip_address + result = self._process_connect( + src_ip=src_ip, + password=payload.get("password"), + connection_request_id=payload.get("connection_request_id"), + session_id=session_id, + ) + elif payload["type"] == "disconnect": + if payload["connection_id"] in self.connections: + connection_id = payload["connection_id"] + connected_ip_address = self.connections[connection_id]["ip_address"] + frame = kwargs.get("frame") + if connected_ip_address == frame.ip.src_ip_address: + self.sys_log.info( + f"{self.name}: Received disconnect command for {connection_id=} from {connected_ip_address}" + ) + self.terminate_connection(connection_id=payload["connection_id"], send_disconnect=False) + else: + self.sys_log.warning( + f"{self.name}: Ignoring disconnect command for {connection_id=} as the command source " + f"({frame.ip.src_ip_address}) doesn't match the connection source ({connected_ip_address})" + ) + elif payload["type"] == "sql": + if payload.get("connection_id") in self.connections: + result = self._process_sql( + query=payload["sql"], query_id=payload["uuid"], connection_id=payload["connection_id"] + ) + else: + result = {"status_code": 401, "type": "sql"} + else: + self.sys_log.info(f"{self.name}: Ignoring payload as it is not a Database payload") + self.send(payload=result, session_id=session_id) + return True + + def send(self, payload: Any, session_id: str, **kwargs) -> bool: + """ + Send a SQL response back down to the SessionManager. + + :param payload: The SQL query results. + :param session_id: The session identifier. + :return: True if the Status Code is 200, otherwise False. + """ + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id) + + return payload["status_code"] == 200 + + def apply_timestep(self, timestep: int) -> None: + """ + Apply a single timestep of simulation dynamics to this service. + + Here at the first step, the database backup is created, in addition to normal service update logic. + """ + if timestep == 1: + self.backup_database() + return super().apply_timestep(timestep) + + def _update_fix_status(self) -> None: + """Perform a database restore when the FIXING countdown is finished.""" + super()._update_fix_status() + if self._fixing_countdown is None: + self.restore_backup() diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py new file mode 100644 index 00000000..5d8af64d --- /dev/null +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -0,0 +1,32 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.config.load import get_extended_config_path +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config +import os + +# Import the extended components so that PrimAITE registers them +from tests.integration_tests.extensions.nodes.super_computer import SuperComputer +from tests.integration_tests.extensions.nodes.giga_switch import GigaSwitch +from tests.integration_tests.extensions.services.extended_service import ExtendedService +from tests.integration_tests.extensions.applications.extended_application import ExtendedApplication + + +def test_extended_example_config(): + + """Test that the example config can be parsed properly.""" + config_path = os.path.join( "tests", "assets", "configs", "extended_config.yaml") + game = load_config(config_path) + network: Network = game.simulation.network + + assert len(network.nodes) == 10 # 10 nodes in example network + assert len(network.computer_nodes) == 1 + assert len(network.router_nodes) == 1 # 1 router in network + assert len(network.switch_nodes) == 1 # 1 switches in network + assert len(network.server_nodes) == 5 # 5 servers in network + assert len(network.extended_hostnodes) == 1 # One extended node based on HostNode + assert len(network.extended_networknodes) == 1 # One extended node based on NetworkNode + + assert 'ExtendedApplication' in network.extended_hostnodes[0].software_manager.software + assert 'ExtendedService' in network.extended_hostnodes[0].software_manager.software diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py index d8d7dfab..f97e915e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py @@ -8,7 +8,7 @@ def test_adding_to_app_registry(): class temp_application(Application, identifier="temp_app"): pass - assert Application._application_registry["temp_app"] is temp_application + assert Application._registry["temp_app"] is temp_application with pytest.raises(ValueError): @@ -19,4 +19,4 @@ def test_adding_to_app_registry(): # Because pytest doesn't reimport classes from modules, registering this temporary test application will change the # state of the Application registry for all subsequently run tests. So, we have to delete and unregister the class. del temp_application - Application._application_registry.pop("temp_app") + Application._registry.pop("temp_app") From dd931d900b560f191d2063f1414dd49c18898941 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 18 Sep 2024 16:02:25 +0100 Subject: [PATCH 002/224] port refactor - eod commit, airspace still broken --- .../simulation_components/network/network.rst | 6 +- .../network/nodes/firewall.rst | 50 ++--- .../network/nodes/wireless_router.rst | 10 +- .../network/transport_to_data_link_layer.rst | 2 +- .../system/applications/nmap.rst | 64 +++--- .../system/services/ftp_client.rst | 2 +- notebooks/test.ipynb | 157 +++++++++++++++ .../agent/observations/acl_observation.py | 4 +- .../agent/observations/host_observations.py | 18 ++ .../agent/observations/nic_observations.py | 35 +++- .../agent/observations/node_observations.py | 19 +- src/primaite/game/game.py | 39 +++- .../Command-&-Control-E2E-Demonstration.ipynb | 2 +- .../create-simulation_demo.ipynb | 2 +- .../network_simulator_demo.ipynb | 2 +- src/primaite/simulator/network/airspace.py | 82 +++----- src/primaite/simulator/network/creation.py | 4 +- .../simulator/network/hardware/base.py | 29 +-- .../hardware/nodes/network/firewall.py | 4 +- .../network/hardware/nodes/network/router.py | 97 +++++---- .../hardware/nodes/network/wireless_router.py | 8 +- src/primaite/simulator/network/networks.py | 16 +- .../simulator/network/protocols/masquerade.py | 4 +- .../network/transmission/data_link_layer.py | 8 +- .../network/transmission/network_layer.py | 43 ++-- .../network/transmission/transport_layer.py | 184 +++++++++++------- .../system/applications/database_client.py | 4 +- .../simulator/system/applications/nmap.py | 72 +++---- .../red_applications/c2/abstract_c2.py | 16 +- .../red_applications/c2/c2_beacon.py | 4 +- .../red_applications/data_manipulation_bot.py | 4 +- .../applications/red_applications/dos_bot.py | 6 +- .../red_applications/ransomware_script.py | 4 +- .../system/applications/web_browser.py | 8 +- .../simulator/system/core/session_manager.py | 62 +++--- .../simulator/system/core/software_manager.py | 22 +-- .../simulator/system/services/arp/arp.py | 4 +- .../services/database/database_service.py | 4 +- .../system/services/dns/dns_client.py | 8 +- .../system/services/dns/dns_server.py | 4 +- .../system/services/ftp/ftp_client.py | 30 +-- .../system/services/ftp/ftp_server.py | 6 +- .../system/services/ftp/ftp_service.py | 8 +- .../simulator/system/services/icmp/icmp.py | 4 +- .../system/services/icmp/router_icmp.py | 4 +- .../system/services/ntp/ntp_client.py | 6 +- .../system/services/ntp/ntp_server.py | 4 +- .../system/services/terminal/terminal.py | 4 +- .../system/services/web_server/web_server.py | 6 +- src/primaite/simulator/system/software.py | 12 +- tests/conftest.py | 28 +-- .../nodes/network/test_firewall_config.py | 32 +-- .../nodes/network/test_router_config.py | 6 +- .../applications/extended_application.py | 8 +- .../extensions/services/extended_service.py | 4 +- .../actions/test_c2_suite_actions.py | 6 +- .../actions/test_configure_actions.py | 2 +- .../actions/test_terminal_actions.py | 2 +- .../observations/test_acl_observations.py | 2 +- .../observations/test_firewall_observation.py | 6 +- .../observations/test_nic_observations.py | 2 +- .../observations/test_router_observation.py | 6 +- .../observations/test_user_observations.py | 2 +- .../game_layer/test_actions.py | 24 +-- .../game_layer/test_rewards.py | 4 +- .../network/test_airspace_config.py | 8 +- .../network/test_broadcast.py | 12 +- .../network/test_firewall.py | 32 +-- .../integration_tests/network/test_routing.py | 8 +- .../network/test_wireless_router.py | 4 +- .../test_c2_suite_integration.py | 20 +- .../test_data_manipulation_bot_and_server.py | 2 +- .../test_dos_bot_and_server.py | 6 +- .../test_ransomware_script.py | 2 +- tests/integration_tests/system/test_nmap.py | 16 +- .../system/test_service_listening_on_ports.py | 16 +- .../test_web_client_server_and_database.py | 12 +- .../test_simulation/test_request_response.py | 4 +- .../_network/_hardware/nodes/test_acl.py | 70 +++---- .../_network/_hardware/nodes/test_router.py | 4 +- .../_transmission/test_data_link_layer.py | 14 +- .../_red_applications/test_c2_suite.py | 20 +- .../test_data_manipulation_bot.py | 4 +- .../_system/_applications/test_web_browser.py | 4 +- .../_system/_services/test_dns_client.py | 4 +- .../_system/_services/test_dns_server.py | 4 +- .../_system/_services/test_ftp_client.py | 10 +- .../_system/_services/test_ftp_server.py | 4 +- .../_system/_services/test_terminal.py | 8 +- .../_system/_services/test_web_server.py | 4 +- .../_simulator/_system/test_software.py | 4 +- .../_utils/test_dict_enum_keys_conversion.py | 12 +- 92 files changed, 957 insertions(+), 682 deletions(-) create mode 100644 notebooks/test.ipynb diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index 636ffbcc..4cc121a3 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -103,13 +103,13 @@ we'll use the following Network that has a client, server, two switches, and a r router_1.acl.add_rule( action=ACLAction.PERMIT, - src_port=Port.ARP, - dst_port=Port.ARP, + src_port=Port["ARP"], + dst_port=Port["ARP"], position=22 ) router_1.acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.ICMP, + protocol=IPProtocol["ICMP"], position=23 ) diff --git a/docs/source/simulation_components/network/nodes/firewall.rst b/docs/source/simulation_components/network/nodes/firewall.rst index 149d3e67..1ef16d63 100644 --- a/docs/source/simulation_components/network/nodes/firewall.rst +++ b/docs/source/simulation_components/network/nodes/firewall.rst @@ -156,8 +156,8 @@ To prevent all external traffic from accessing the internal network, with except # Exception rule to allow HTTP traffic from external to internal network firewall.internal_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, - dst_port=Port.HTTP, + protocol=IPProtocol["TCP"], + dst_port=Port["HTTP"], dst_ip_address="192.168.1.0", dst_wildcard_mask="0.0.0.255", position=2 @@ -172,16 +172,16 @@ To enable external traffic to access specific services hosted within the DMZ: # Allow HTTP and HTTPS traffic to the DMZ firewall.dmz_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, - dst_port=Port.HTTP, + protocol=IPProtocol["TCP"], + dst_port=Port["HTTP"], dst_ip_address="172.16.0.0", dst_wildcard_mask="0.0.0.255", position=3 ) firewall.dmz_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, - dst_port=Port.HTTPS, + protocol=IPProtocol["TCP"], + dst_port=Port["HTTPS"], dst_ip_address="172.16.0.0", dst_wildcard_mask="0.0.0.255", position=4 @@ -196,9 +196,9 @@ To permit SSH access from a designated external IP to a specific server within t # Allow SSH from a specific external IP to an internal server firewall.internal_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="10.0.0.2", - dst_port=Port.SSH, + dst_port=Port["SSH"], dst_ip_address="192.168.1.10", position=5 ) @@ -212,9 +212,9 @@ To limit database server access to selected external IP addresses: # Allow PostgreSQL traffic from an authorized external IP to the internal DB server firewall.internal_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="10.0.0.3", - dst_port=Port.POSTGRES_SERVER, + dst_port=Port["POSTGRES_SERVER"], dst_ip_address="192.168.1.20", position=6 ) @@ -222,8 +222,8 @@ To limit database server access to selected external IP addresses: # Deny all other PostgreSQL traffic from external sources firewall.internal_inbound_acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol.TCP, - dst_port=Port.POSTGRES_SERVER, + protocol=IPProtocol["TCP"], + dst_port=Port["POSTGRES_SERVER"], dst_ip_address="192.168.1.0", dst_wildcard_mask="0.0.0.255", position=7 @@ -247,15 +247,15 @@ To authorize HTTP/HTTPS access to a DMZ-hosted web server, excluding known malic # Allow HTTP/HTTPS traffic to the DMZ web server firewall.dmz_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, - dst_port=Port.HTTP, + protocol=IPProtocol["TCP"], + dst_port=Port["HTTP"], dst_ip_address="172.16.0.2", position=9 ) firewall.dmz_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, - dst_port=Port.HTTPS, + protocol=IPProtocol["TCP"], + dst_port=Port["HTTPS"], dst_ip_address="172.16.0.2", position=10 ) @@ -269,9 +269,9 @@ To facilitate restricted access from the internal network to DMZ-hosted services # Permit specific internal application server HTTPS access to a DMZ-hosted API firewall.internal_outbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="192.168.1.30", # Internal application server IP - dst_port=Port.HTTPS, + dst_port=Port["HTTPS"], dst_ip_address="172.16.0.3", # DMZ API server IP position=11 ) @@ -289,9 +289,9 @@ To facilitate restricted access from the internal network to DMZ-hosted services # Corresponding rule in DMZ inbound ACL to allow the traffic from the specific internal server firewall.dmz_inbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="192.168.1.30", # Ensuring this specific source is allowed - dst_port=Port.HTTPS, + dst_port=Port["HTTPS"], dst_ip_address="172.16.0.3", # DMZ API server IP position=13 ) @@ -301,7 +301,7 @@ To facilitate restricted access from the internal network to DMZ-hosted services action=ACLAction.DENY, src_ip_address="192.168.1.0", src_wildcard_mask="0.0.0.255", - dst_port=Port.HTTPS, + dst_port=Port["HTTPS"], dst_ip_address="172.16.0.3", # DMZ API server IP position=14 ) @@ -315,8 +315,8 @@ To block all SSH access attempts from the external network: # Deny all SSH traffic from any external source firewall.external_inbound_acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol.TCP, - dst_port=Port.SSH, + protocol=IPProtocol["TCP"], + dst_port=Port["SSH"], position=1 ) @@ -329,8 +329,8 @@ To allow the internal network to initiate HTTP connections to the external netwo # Permit outgoing HTTP traffic from the internal network to any external destination firewall.external_outbound_acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, - dst_port=Port.HTTP, + protocol=IPProtocol["TCP"], + dst_port=Port["HTTP"], position=2 ) diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index c78c8419..bd361afa 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -49,7 +49,7 @@ additional steps to configure wireless settings: wireless_router.configure_wireless_access_point( port=1, ip_address="192.168.2.1", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency.WIFI_2_4, + frequency=AirSpaceFrequency["WIFI_2_4"], ) @@ -102,8 +102,8 @@ ICMP traffic, ensuring basic network connectivity and ping functionality. network.connect(pc_a.network_interface[1], router_1.router_interface) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) # Configure PC B pc_b = Computer( @@ -130,13 +130,13 @@ ICMP traffic, ensuring basic network connectivity and ping functionality. port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency.WIFI_2_4, + frequency=AirSpaceFrequency["WIFI_2_4"], ) router_2.configure_wireless_access_point( port=1, ip_address="192.168.1.2", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency.WIFI_2_4, + frequency=AirSpaceFrequency["WIFI_2_4"], ) # Configure routes for inter-router communication diff --git a/docs/source/simulation_components/network/transport_to_data_link_layer.rst b/docs/source/simulation_components/network/transport_to_data_link_layer.rst index cc546021..02bfdcdc 100644 --- a/docs/source/simulation_components/network/transport_to_data_link_layer.rst +++ b/docs/source/simulation_components/network/transport_to_data_link_layer.rst @@ -104,7 +104,7 @@ address of 'aa:bb:cc:dd:ee:ff' to port 8080 on the host 10.0.0.10 which has a NI ip_packet = IPPacket( src_ip_address="192.168.0.100", dst_ip_address="10.0.0.10", - protocol=IPProtocol.TCP + protocol=IPProtocol["TCP"] ) # Data Link Layer ethernet_header = EthernetHeader( diff --git a/docs/source/simulation_components/system/applications/nmap.rst b/docs/source/simulation_components/system/applications/nmap.rst index 1e7f5ea4..e2cd474e 100644 --- a/docs/source/simulation_components/system/applications/nmap.rst +++ b/docs/source/simulation_components/system/applications/nmap.rst @@ -165,8 +165,8 @@ Perform a horizontal port scan on port 5432 across multiple IP addresses: { IPv4Address('192.168.1.12'): { - : [ - + : [ + ] } } @@ -192,7 +192,7 @@ Perform a vertical port scan on multiple ports on a single IP address: vertical_scan_results = pc_1_nmap.port_scan( target_ip_address=[IPv4Address("192.168.1.12")], - target_port=[Port(21), Port(22), Port(80), Port(443)] + target_port=[21, 22, 80, 443] ) .. code-block:: python @@ -200,9 +200,9 @@ Perform a vertical port scan on multiple ports on a single IP address: { IPv4Address('192.168.1.12'): { - : [ - , - + : [ + , + ] } } @@ -233,7 +233,7 @@ Perform a box scan on multiple ports across multiple IP addresses: box_scan_results = pc_1_nmap.port_scan( target_ip_address=[IPv4Address("192.168.1.12"), IPv4Address("192.168.1.13")], - target_port=[Port(21), Port(22), Port(80), Port(443)] + target_port=[21, 22, 80, 443] ) .. code-block:: python @@ -241,15 +241,15 @@ Perform a box scan on multiple ports across multiple IP addresses: { IPv4Address('192.168.1.13'): { - : [ - , - + : [ + , + ] }, IPv4Address('192.168.1.12'): { - : [ - , - + : [ + , + ] } } @@ -289,36 +289,36 @@ Perform a full box scan on all ports, over both TCP and UDP, on a whole subnet: { IPv4Address('192.168.1.11'): { - : [ - + : [ + ] }, IPv4Address('192.168.1.1'): { - : [ - + : [ + ] }, IPv4Address('192.168.1.12'): { - : [ - , - , - , - + : [ + , + , + , + ], - : [ - , - + : [ + , + ] }, IPv4Address('192.168.1.13'): { - : [ - , - , - + : [ + , + , + ], - : [ - , - + : [ + , + ] } } diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index fdf9cfcf..0c9a781c 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -15,7 +15,7 @@ Key features - Connects to the :ref:`FTPServer` via the ``SoftwareManager``. - Simulates FTP requests and FTPPacket transfer across a network - Allows the emulation of FTP commands between an FTP client and server: - - PORT: specifies the port that server should connect to on the client (currently only uses ``Port.FTP``) + - PORT: specifies the port that server should connect to on the client (currently only uses ``Port["FTP"]``) - STOR: stores a file from client to server - RETR: retrieves a file from the FTP server - QUIT: disconnect from server diff --git a/notebooks/test.ipynb b/notebooks/test.ipynb new file mode 100644 index 00000000..5afe04b0 --- /dev/null +++ b/notebooks/test.ipynb @@ -0,0 +1,157 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "from primaite.game.game import PrimaiteGame\n", + "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", + "from primaite.simulator.network.hardware.nodes.host.server import Server\n", + "from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection\n", + "from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot\n", + "from primaite.simulator.system.services.database.database_service import DatabaseService\n", + "from primaite import getLogger, PRIMAITE_PATHS" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with open(PRIMAITE_PATHS.user_config_path / \"example_config\" / \"data_manipulation.yaml\") as f:\n", + " cfg = yaml.safe_load(f)\n", + "game = PrimaiteGame.from_config(cfg)\n", + "uc2_network = game.simulation.network" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "client_1: Computer = uc2_network.get_node_by_hostname(\"client_1\")\n", + "db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get(\"DataManipulationBot\")\n", + "\n", + "database_server: Server = uc2_network.get_node_by_hostname(\"database_server\")\n", + "db_service: DatabaseService = database_server.software_manager.software.get(\"DatabaseService\")\n", + "\n", + "web_server: Server = uc2_network.get_node_by_hostname(\"web_server\")\n", + "db_client: DatabaseClient = web_server.software_manager.software.get(\"DatabaseClient\")\n", + "db_connection: DatabaseClientConnection = db_client.get_new_connection()\n", + "db_service.backup_database()\n", + "\n", + "# First check that the DB client on the web_server can successfully query the users table on the database\n", + "assert db_connection.query(\"SELECT\")\n", + "\n", + "db_manipulation_bot.data_manipulation_p_of_success = 1.0\n", + "db_manipulation_bot.port_scan_p_of_success = 1.0\n", + "\n", + "# Now we run the DataManipulationBot\n", + "db_manipulation_bot.attack()\n", + "\n", + "# Now check that the DB client on the web_server cannot query the users table on the database\n", + "assert not db_connection.query(\"SELECT\")\n", + "\n", + "# Now restore the database\n", + "db_service.restore_backup()\n", + "\n", + "# Now check that the DB client on the web_server can successfully query the users table on the database\n", + "assert db_connection.query(\"SELECT\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "router = uc2_network.get_node_by_hostname(\"router_1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-----------------------------------------------------------------------------------------------------------------+\n", + "| router_1 Access Control List |\n", + "+-------+--------+----------+--------+--------------+-------------+--------+--------------+-------------+---------+\n", + "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", + "+-------+--------+----------+--------+--------------+-------------+--------+--------------+-------------+---------+\n", + "| 18 | PERMIT | ANY | ANY | ANY | 5432 (5432) | ANY | ANY | 5432 (5432) | 4 |\n", + "| 19 | PERMIT | ANY | ANY | ANY | 53 (53) | ANY | ANY | 53 (53) | 0 |\n", + "| 20 | PERMIT | ANY | ANY | ANY | 21 (21) | ANY | ANY | 21 (21) | 0 |\n", + "| 21 | PERMIT | ANY | ANY | ANY | 80 (80) | ANY | ANY | 80 (80) | 0 |\n", + "| 22 | PERMIT | ANY | ANY | ANY | 219 (219) | ANY | ANY | 219 (219) | 9 |\n", + "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "+-------+--------+----------+--------+--------------+-------------+--------+--------------+-------------+---------+\n" + ] + } + ], + "source": [ + "router.acl.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "AccessControlList.is_permitted() missing 1 required positional argument: 'frame'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[6], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mrouter\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43macl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mis_permitted\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: AccessControlList.is_permitted() missing 1 required positional argument: 'frame'" + ] + } + ], + "source": [ + "router.acl.is_permitted()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index 41af5a8f..abb6f1f8 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -10,6 +10,8 @@ from gymnasium.core import ObsType from primaite import getLogger from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port _LOGGER = getLogger(__name__) @@ -61,7 +63,7 @@ class ACLObservation(AbstractObservation, identifier="ACL"): self.ip_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(ip_list)} self.wildcard_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(wildcard_list)} self.port_to_id: Dict[int, int] = {p: i + 2 for i, p in enumerate(port_list)} - self.protocol_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(protocol_list)} + self.protocol_to_id: Dict[str, int] = {IPProtocol[p]: i + 2 for i, p in enumerate(protocol_list)} self.default_observation: Dict = { i + 1: { diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 4419ccc7..05b25952 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -5,6 +5,7 @@ from typing import Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType +from pydantic import field_validator from primaite import getLogger from primaite.game.agent.observations.file_system_observations import FolderObservation @@ -12,6 +13,8 @@ from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.software_observation import ApplicationObservation, ServiceObservation from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port _LOGGER = getLogger(__name__) @@ -55,6 +58,21 @@ class HostObservation(AbstractObservation, identifier="HOST"): include_users: Optional[bool] = True """If True, report user session information.""" + @field_validator('monitored_traffic', mode='before') + def traffic_lookup(cls, val:Optional[Dict]) -> Optional[Dict]: + if val is None: + return val + new_val = {} + for proto, port_list in val.items(): + # convert protocol, for instance ICMP becomes "icmp" + proto = IPProtocol[proto] if proto in IPProtocol else proto + new_val[proto] = [] + for port in port_list: + # convert ports, for instance "HTTP" becomes 80 + port = Port[port] if port in Port else port + new_val[proto].append(port) + return new_val + def __init__( self, where: WhereType, diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 002ee4da..200187f5 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -5,9 +5,11 @@ from typing import Dict, Optional from gymnasium import spaces from gymnasium.core import ObsType +from pydantic import field_validator from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -24,6 +26,22 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): monitored_traffic: Optional[Dict] = None """A dict containing which traffic types are to be included in the observation.""" + @field_validator('monitored_traffic', mode='before') + def traffic_lookup(cls, val:Optional[Dict]) -> Optional[Dict]: + if val is None: + return val + new_val = {} + for proto, port_list in val.items(): + # convert protocol, for instance ICMP becomes "icmp" + proto = IPProtocol[proto] if proto in IPProtocol else proto + new_val[proto] = [] + for port in port_list: + # convert ports, for instance "HTTP" becomes 80 + port = Port[port] if port in Port else port + new_val[proto].append(port) + return new_val + + def __init__(self, where: WhereType, include_nmne: bool, monitored_traffic: Optional[Dict] = None) -> None: """ Initialise a network interface observation instance. @@ -67,7 +85,7 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): else: default_traffic_obs["TRAFFIC"][protocol] = {} for port in monitored_traffic_config[protocol]: - default_traffic_obs["TRAFFIC"][protocol][Port[port].value] = {"inbound": 0, "outbound": 0} + default_traffic_obs["TRAFFIC"][protocol] = {"inbound": 0, "outbound": 0} return default_traffic_obs @@ -142,17 +160,16 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): } else: for port in self.monitored_traffic[protocol]: - port_enum = Port[port] - obs["TRAFFIC"][protocol][port_enum.value] = {} + obs["TRAFFIC"][protocol][port] = {} traffic = {"inbound": 0, "outbound": 0} - if nic_state["traffic"][protocol].get(port_enum.value) is not None: - traffic = nic_state["traffic"][protocol][port_enum.value] + if nic_state["traffic"][protocol].get(port) is not None: + traffic = nic_state["traffic"][protocol][port] - obs["TRAFFIC"][protocol][port_enum.value]["inbound"] = self._categorise_traffic( + obs["TRAFFIC"][protocol][port]["inbound"] = self._categorise_traffic( traffic_value=traffic["inbound"], nic_state=nic_state ) - obs["TRAFFIC"][protocol][port_enum.value]["outbound"] = self._categorise_traffic( + obs["TRAFFIC"][protocol][port]["outbound"] = self._categorise_traffic( traffic_value=traffic["outbound"], nic_state=nic_state ) @@ -162,7 +179,7 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): obs["TRAFFIC"]["icmp"] = {"inbound": 0, "outbound": 0} else: for port in self.monitored_traffic[protocol]: - obs["TRAFFIC"][protocol][Port[port].value] = {"inbound": 0, "outbound": 0} + obs["TRAFFIC"][protocol][port] = {"inbound": 0, "outbound": 0} if self.include_nmne: obs.update({"NMNE": {}}) @@ -201,7 +218,7 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): else: space["TRAFFIC"][protocol] = spaces.Dict({}) for port in self.monitored_traffic[protocol]: - space["TRAFFIC"][protocol][Port[port].value] = spaces.Dict( + space["TRAFFIC"][protocol][port] = spaces.Dict( {"inbound": spaces.Discrete(11), "outbound": spaces.Discrete(11)} ) diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index e263cadb..3e51c3b3 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -5,13 +5,15 @@ from typing import Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType -from pydantic import model_validator +from pydantic import field_validator, model_validator from primaite import getLogger from primaite.game.agent.observations.firewall_observation import FirewallObservation from primaite.game.agent.observations.host_observations import HostObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.router_observation import RouterObservation +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port _LOGGER = getLogger(__name__) @@ -61,6 +63,21 @@ class NodesObservation(AbstractObservation, identifier="NODES"): num_rules: Optional[int] = None """Number of rules ACL rules to show.""" + @field_validator('monitored_traffic', mode='before') + def traffic_lookup(cls, val:Optional[Dict]) -> Optional[Dict]: + if val is None: + return val + new_val = {} + for proto, port_list in val.items(): + # convert protocol, for instance ICMP becomes "icmp" + proto = IPProtocol[proto] if proto in IPProtocol else proto + new_val[proto] = [] + for port in port_list: + # convert ports, for instance "HTTP" becomes 80 + port = Port[port] if port in Port else port + new_val[proto].append(port) + return new_val + @model_validator(mode="after") def force_optional_fields(self) -> NodesObservation.ConfigSchema: """Check that options are specified only if they are needed for the nodes that are part of the config.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 11c968af..e8329c63 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -4,7 +4,7 @@ from ipaddress import IPv4Address from typing import Dict, List, Optional, Union import numpy as np -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, field_validator from primaite import DEFAULT_BANDWIDTH, getLogger from primaite.game.agent.actions import ActionManager @@ -82,13 +82,40 @@ class PrimaiteGameOptions(BaseModel): """Random number seed for RNGs.""" max_episode_length: int = 256 """Maximum number of episodes for the PrimAITE game.""" - ports: List[str] + ports: List[int] """A whitelist of available ports in the simulation.""" protocols: List[str] """A whitelist of available protocols in the simulation.""" thresholds: Optional[Dict] = {} """A dict containing the thresholds used for determining what is acceptable during observations.""" + @field_validator('ports', mode='before') + def ports_str2int(cls, vals:Union[List[str],List[int]]) -> List[int]: + """ + Convert named port strings to port integer values. Integer ports remain unaffected. + + This is necessary to retain backwards compatibility with configs written for PrimAITE<=3.3. + :warning: This will be deprecated in PrimAITE 4.0 and configs will need to be converted. + """ + for i, port_val in enumerate(vals): + if port_val in Port: + vals[i] = Port[port_val] + return vals + + @field_validator('protocols', mode='before') + def protocols_str2int(cls, vals:List[str]) -> List[str]: + """ + Convert old-style named protocols to their proper values. + + This is necessary to retain backwards compatibility with configs written for PrimAITE<=3.3. + :warning: This will be deprecated in PrimAITE 4.0 and configs will need to be converted. + """ + for i, proto_val in enumerate(vals): + if proto_val in IPProtocol: + vals[i] = IPProtocol[proto_val] + return vals + + class PrimaiteGame: """ @@ -358,7 +385,7 @@ class PrimaiteGame: for port_id in set(software_cfg.get("options", {}).get("listen_on_ports", [])): port = None if isinstance(port_id, int): - port = Port(port_id) + port = port_id elif isinstance(port_id, str): port = Port[port_id] if port: @@ -475,7 +502,7 @@ class PrimaiteGame: opt = application_cfg["options"] new_application.configure( target_ip_address=IPv4Address(opt.get("target_ip_address")), - target_port=Port(opt.get("target_port", Port.POSTGRES_SERVER.value)), + target_port = Port[opt.get("target_port", "POSTGRES_SERVER")], payload=opt.get("payload"), repeat=bool(opt.get("repeat")), port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")), @@ -488,8 +515,8 @@ class PrimaiteGame: new_application.configure( c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), keep_alive_frequency=(opt.get("keep_alive_frequency", 5)), - masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol", IPProtocol.TCP))], - masquerade_port=Port[(opt.get("masquerade_port", Port.HTTP))], + masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol", IPProtocol["TCP"]))], + masquerade_port=Port[(opt.get("masquerade_port", Port["HTTP"]))], ) if "network_interfaces" in node_cfg: for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index b6b13f28..a5cc385b 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -1783,7 +1783,7 @@ "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", "from primaite.simulator.network.transmission.transport_layer import Port\n", "# As we're configuring via the PrimAITE API we need to pass the actual IPProtocol/Port (Agents leverage the simulation via the game layer and thus can pass strings).\n", - "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol.UDP, masquerade_port=Port.DNS)\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol["UDP"], masquerade_port=Port["DNS"])\n", "c2_beacon.establish()\n", "c2_beacon.show()" ] diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb index 77ac4842..f573f251 100644 --- a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb +++ b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb @@ -182,7 +182,7 @@ "metadata": {}, "outputs": [], "source": [ - "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=Port.HTTP, protocol = IPProtocol.NONE,operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" + "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=Port["HTTP"], protocol = IPProtocol["NONE"],operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" ] }, { diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb index 17a0f796..2d5b4772 100644 --- a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb +++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb @@ -537,7 +537,7 @@ "from primaite.simulator.network.hardware.nodes.network.router import ACLAction\n", "network.get_node_by_hostname(\"router_1\").acl.add_rule(\n", " action=ACLAction.DENY,\n", - " protocol=IPProtocol.ICMP,\n", + " protocol=IPProtocol["ICMP"],\n", " src_ip_address=\"192.168.10.22\",\n", " position=1\n", ")" diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index cdb01514..29326df8 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -3,10 +3,12 @@ from __future__ import annotations from abc import ABC, abstractmethod from enum import Enum -from typing import Any, Dict, List +from typing import Any, ClassVar, Dict, List, Type +from pydantic._internal._generics import PydanticGenericMetadata +from typing_extensions import Unpack from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from primaite import getLogger from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface @@ -40,51 +42,29 @@ def format_hertz(hertz: float, format_terahertz: bool = False, decimals: int = 3 else: # Hertz return format_str.format(hertz) + " Hz" +AirSpaceFrequencyRegistry: Dict[str,Dict] = { + "WIFI_2_4" : {'frequency': 2.4e9, 'data_rate_bps':100_000_000.0}, + "WIFI_5" : {'frequency': 5e9, 'data_rate_bps':500_000_000.0}, +} -class AirSpaceFrequency(Enum): - """Enumeration representing the operating frequencies for wireless communications.""" +def register_frequency(freq_name: str, freq_hz: int, data_rate_bps: int) -> None: + if freq_name in AirSpaceFrequencyRegistry: + raise RuntimeError(f"Cannot register new frequency {freq_name} because it's already registered.") + AirSpaceFrequencyRegistry.update({freq_name:{'frequency': freq_hz, 'data_rate_bps':data_rate_bps}}) - WIFI_2_4 = 2.4e9 - """WiFi 2.4 GHz. Known for its extensive range and ability to penetrate solid objects effectively.""" - WIFI_5 = 5e9 - """WiFi 5 GHz. Known for its higher data transmission speeds and reduced interference from other devices.""" +def maximum_data_rate_mbps(frequency_name:str) -> float: + """ + Retrieves the maximum data transmission rate in megabits per second (Mbps). - def __str__(self) -> str: - hertz_str = format_hertz(hertz=self.value) - if self == AirSpaceFrequency.WIFI_2_4: - return f"WiFi {hertz_str}" - if self == AirSpaceFrequency.WIFI_5: - return f"WiFi {hertz_str}" - return "Unknown Frequency" + This is derived by converting the maximum data rate from bits per second, as defined + in `maximum_data_rate_bps`, to megabits per second. - @property - def maximum_data_rate_bps(self) -> float: - """ - Retrieves the maximum data transmission rate in bits per second (bps). + :return: The maximum data rate in megabits per second. + """ + return AirSpaceFrequencyRegistry[frequency_name]['data_rate_bps'] + return data_rate / 1_000_000.0 - The maximum rates are predefined for frequencies.: - - WIFI 2.4 supports 100,000,000 bps - - WIFI 5 supports 500,000,000 bps - :return: The maximum data rate in bits per second. - """ - if self == AirSpaceFrequency.WIFI_2_4: - return 100_000_000.0 # 100 Megabits per second - if self == AirSpaceFrequency.WIFI_5: - return 500_000_000.0 # 500 Megabits per second - return 0.0 - - @property - def maximum_data_rate_mbps(self) -> float: - """ - Retrieves the maximum data transmission rate in megabits per second (Mbps). - - This is derived by converting the maximum data rate from bits per second, as defined - in `maximum_data_rate_bps`, to megabits per second. - - :return: The maximum data rate in megabits per second. - """ - return self.maximum_data_rate_bps / 1_000_000.0 class AirSpace(BaseModel): @@ -97,13 +77,13 @@ class AirSpace(BaseModel): """ wireless_interfaces: Dict[str, WirelessNetworkInterface] = Field(default_factory=lambda: {}) - wireless_interfaces_by_frequency: Dict[AirSpaceFrequency, List[WirelessNetworkInterface]] = Field( + wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field( default_factory=lambda: {} ) - bandwidth_load: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) - frequency_max_capacity_mbps_: Dict[AirSpaceFrequency, float] = Field(default_factory=lambda: {}) + bandwidth_load: Dict[int, float] = Field(default_factory=lambda: {}) + frequency_max_capacity_mbps_: Dict[int, float] = Field(default_factory=lambda: {}) - def get_frequency_max_capacity_mbps(self, frequency: AirSpaceFrequency) -> float: + def get_frequency_max_capacity_mbps(self, frequency: str) -> float: """ Retrieves the maximum data transmission capacity for a specified frequency. @@ -117,9 +97,9 @@ class AirSpace(BaseModel): """ if frequency in self.frequency_max_capacity_mbps_: return self.frequency_max_capacity_mbps_[frequency] - return frequency.maximum_data_rate_mbps + return maximum_data_rate_mbps(frequency) - def set_frequency_max_capacity_mbps(self, cfg: Dict[AirSpaceFrequency, float]): + def set_frequency_max_capacity_mbps(self, cfg: Dict[int, float]): """ Sets custom maximum data transmission capacities for multiple frequencies. @@ -150,7 +130,7 @@ class AirSpace(BaseModel): load_percent = load / maximum_capacity if maximum_capacity > 0 else 0.0 if load_percent > 1.0: load_percent = 1.0 - table.add_row([format_hertz(frequency.value), f"{load_percent:.0%}", f"{maximum_capacity:.3f}"]) + table.add_row([format_hertz(frequency), f"{load_percent:.0%}", f"{maximum_capacity:.3f}"]) print(table) def show_wireless_interfaces(self, markdown: bool = False): @@ -182,7 +162,7 @@ class AirSpace(BaseModel): interface.mac_address, interface.ip_address if hasattr(interface, "ip_address") else None, interface.subnet_mask if hasattr(interface, "subnet_mask") else None, - format_hertz(interface.frequency.value), + format_hertz(interface.frequency), f"{interface.speed:.3f}", status, ] @@ -298,7 +278,7 @@ class WirelessNetworkInterface(NetworkInterface, ABC): """ airspace: AirSpace - frequency: AirSpaceFrequency = AirSpaceFrequency.WIFI_2_4 + frequency: str = "WIFI_2_4" def enable(self): """Attempt to enable the network interface.""" @@ -430,7 +410,7 @@ class IPWirelessNetworkInterface(WirelessNetworkInterface, Layer3Interface, ABC) # Update the state with information from Layer3Interface state.update(Layer3Interface.describe_state(self)) - state["frequency"] = self.frequency.value + state["frequency"] = self.frequency return state diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 61a37a90..c2524b4b 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -98,8 +98,8 @@ def create_office_lan( default_gateway = IPv4Address(f"192.168.{subnet_base}.1") router = Router(hostname=f"router_{lan_name}", start_up_duration=0) router.power_on() - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) network.add_node(router) router.configure_port(port=1, ip_address=default_gateway, subnet_mask="255.255.255.0") router.enable_port(1) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index bf230e07..4154cc08 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -203,16 +203,16 @@ class NetworkInterface(SimComponent, ABC): # Initialise basic frame data variables direction = "inbound" if inbound else "outbound" # Direction of the traffic ip_address = str(frame.ip.src_ip_address if inbound else frame.ip.dst_ip_address) # Source or destination IP - protocol = frame.ip.protocol.name # Network protocol used in the frame + protocol = frame.ip.protocol # Network protocol used in the frame # Initialise port variable; will be determined based on protocol type port = None # Determine the source or destination port based on the protocol (TCP/UDP) if frame.tcp: - port = frame.tcp.src_port.value if inbound else frame.tcp.dst_port.value + port = frame.tcp.src_port if inbound else frame.tcp.dst_port elif frame.udp: - port = frame.udp.src_port.value if inbound else frame.udp.dst_port.value + port = frame.udp.src_port if inbound else frame.udp.dst_port # Convert frame payload to string for keyword checking frame_str = str(frame.payload) @@ -274,20 +274,20 @@ class NetworkInterface(SimComponent, ABC): # Identify the protocol and port from the frame if frame.tcp: - protocol = IPProtocol.TCP + protocol = IPProtocol["TCP"] port = frame.tcp.dst_port elif frame.udp: - protocol = IPProtocol.UDP + protocol = IPProtocol["UDP"] port = frame.udp.dst_port elif frame.icmp: - protocol = IPProtocol.ICMP + protocol = IPProtocol["ICMP"] # Ensure the protocol is in the capture dict if protocol not in self.traffic: self.traffic[protocol] = {} # Handle non-ICMP protocols that use ports - if protocol != IPProtocol.ICMP: + if protocol != IPProtocol["ICMP"]: if port not in self.traffic[protocol]: self.traffic[protocol][port] = {"inbound": 0, "outbound": 0} self.traffic[protocol][port][direction] += frame.size_Mbits @@ -843,8 +843,8 @@ class UserManager(Service): :param password: The password for the default admin user """ kwargs["name"] = "UserManager" - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.NONE + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["NONE"] super().__init__(**kwargs) self.start() @@ -1166,8 +1166,8 @@ class UserSessionManager(Service): :param password: The password for the default admin user """ kwargs["name"] = "UserSessionManager" - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.NONE + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["NONE"] super().__init__(**kwargs) self.start() @@ -1312,7 +1312,7 @@ class UserSessionManager(Service): software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( payload={"type": "user_timeout", "connection_id": session.uuid}, - dest_port=Port.SSH, + dest_port=Port["SSH"], dest_ip_address=session.remote_ip_address, ) @@ -1845,8 +1845,9 @@ class Node(SimComponent): table.align = "l" table.title = f"{self.hostname} Open Ports" for port in self.software_manager.get_open_ports(): - if port.value > 0: - table.add_row([port.value, port.name]) + if port > 0: + # TODO: do a reverse lookup for port name, or change this to only show port int + table.add_row([port, port]) print(table.get_string(sortby="Port")) @property diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 4510eac0..6d8e084d 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -58,8 +58,8 @@ class Firewall(Router): >>> # Permit HTTP traffic to the DMZ >>> firewall.dmz_inbound_acl.add_rule( ... action=ACLAction.PERMIT, - ... protocol=IPProtocol.TCP, - ... dst_port=Port.HTTP, + ... protocol=IPProtocol["TCP"], + ... dst_port=Port["HTTP"], ... src_ip_address="0.0.0.0", ... src_wildcard_mask="0.0.0.0", ... dst_ip_address="172.16.0.0", diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index ceb91695..013c473e 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import validate_call +from pydantic import field_validator, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -106,7 +106,7 @@ class ACLRule(SimComponent): :ivar ACLAction action: Specifies whether to `PERMIT` or `DENY` the traffic that matches the rule conditions. The default action is `DENY`. - :ivar Optional[IPProtocol] protocol: The network protocol (e.g., TCP, UDP, ICMP) to match. If `None`, the rule + :ivar Optional[str] protocol: The network protocol (e.g., TCP, UDP, ICMP) to match. If `None`, the rule applies to all protocols. :ivar Optional[IPV4Address] src_ip_address: The source IP address to match. If combined with `src_wildcard_mask`, it specifies the start of an IP range. @@ -116,20 +116,33 @@ class ACLRule(SimComponent): `dst_wildcard_mask`, it specifies the start of an IP range. :ivar Optional[IPv4Address] dst_wildcard_mask: The wildcard mask for the destination IP address, defining the range of addresses to match. - :ivar Optional[Port] src_port: The source port number to match. Relevant for TCP/UDP protocols. - :ivar Optional[Port] dst_port: The destination port number to match. Relevant for TCP/UDP protocols. + :ivar Optional[int] src_port: The source port number to match. Relevant for TCP/UDP protocols. + :ivar Optional[int] dst_port: The destination port number to match. Relevant for TCP/UDP protocols. """ action: ACLAction = ACLAction.DENY - protocol: Optional[IPProtocol] = None + protocol: Optional[str] = None src_ip_address: Optional[IPV4Address] = None src_wildcard_mask: Optional[IPV4Address] = None dst_ip_address: Optional[IPV4Address] = None dst_wildcard_mask: Optional[IPV4Address] = None - src_port: Optional[Port] = None - dst_port: Optional[Port] = None + src_port: Optional[int] = None + dst_port: Optional[int] = None match_count: int = 0 + @field_validator('protocol', mode='before') + def protocol_valid(cls, val:Optional[str]) -> Optional[str]: + if val is not None: + assert val in IPProtocol.values(), f"Cannot create ACL rule with invalid protocol {val}" + return val + + @field_validator('src_port', 'dst_port', mode='before') + def ports_valid(cls, val:Optional[int]) -> Optional[int]: + if val is not None: + assert val in Port.values(), f"Cannot create ACL rule with invalid port {val}" + return val + + def __str__(self) -> str: rule_strings = [] for key, value in self.model_dump(exclude={"uuid", "request_manager"}).items(): @@ -149,13 +162,13 @@ class ACLRule(SimComponent): """ state = super().describe_state() state["action"] = self.action.value - state["protocol"] = self.protocol.name if self.protocol else None + state["protocol"] = self.protocol if self.protocol else None state["src_ip_address"] = str(self.src_ip_address) if self.src_ip_address else None state["src_wildcard_mask"] = str(self.src_wildcard_mask) if self.src_wildcard_mask else None - state["src_port"] = self.src_port.name if self.src_port else None + state["src_port"] = self.src_port if self.src_port else None state["dst_ip_address"] = str(self.dst_ip_address) if self.dst_ip_address else None state["dst_wildcard_mask"] = str(self.dst_wildcard_mask) if self.dst_wildcard_mask else None - state["dst_port"] = self.dst_port.name if self.dst_port else None + state["dst_port"] = self.dst_port if self.dst_port else None state["match_count"] = self.match_count return state @@ -265,7 +278,7 @@ class AccessControlList(SimComponent): >>> acl = AccessControlList() >>> acl.add_rule( ... action=ACLAction.PERMIT, - ... protocol=IPProtocol.TCP, + ... protocol=IPProtocol["TCP"], ... src_ip_address="192.168.1.0", ... src_wildcard_mask="0.0.0.255", ... dst_ip_address="192.168.2.0", @@ -323,13 +336,13 @@ class AccessControlList(SimComponent): func=lambda request, context: RequestResponse.from_bool( self.add_rule( action=ACLAction[request[0]], - protocol=None if request[1] == "ALL" else IPProtocol[request[1]], + protocol=None if request[1] == "ALL" else request[1], src_ip_address=None if request[2] == "ALL" else IPv4Address(request[2]), src_wildcard_mask=None if request[3] == "NONE" else IPv4Address(request[3]), - src_port=None if request[4] == "ALL" else Port[request[4]], + src_port=None if request[4] == "ALL" else request[4], dst_ip_address=None if request[5] == "ALL" else IPv4Address(request[5]), dst_wildcard_mask=None if request[6] == "NONE" else IPv4Address(request[6]), - dst_port=None if request[7] == "ALL" else Port[request[7]], + dst_port=None if request[7] == "ALL" else request[7], position=int(request[8]), ) ) @@ -377,13 +390,13 @@ class AccessControlList(SimComponent): def add_rule( self, action: ACLAction = ACLAction.DENY, - protocol: Optional[IPProtocol] = None, + protocol: Optional[str] = None, src_ip_address: Optional[IPV4Address] = None, src_wildcard_mask: Optional[IPV4Address] = None, dst_ip_address: Optional[IPV4Address] = None, dst_wildcard_mask: Optional[IPV4Address] = None, - src_port: Optional[Port] = None, - dst_port: Optional[Port] = None, + src_port: Optional[int] = None, + dst_port: Optional[int] = None, position: int = 0, ) -> bool: """ @@ -399,11 +412,11 @@ class AccessControlList(SimComponent): >>> router = Router("router") >>> router.add_rule( ... action=ACLAction.DENY, - ... protocol=IPProtocol.TCP, + ... protocol=IPProtocol["TCP"], ... src_ip_address="192.168.1.0", ... src_wildcard_mask="0.0.0.255", ... dst_ip_address="10.10.10.5", - ... dst_port=Port.SSH, + ... dst_port=Port["SSH"], ... position=5 ... ) >>> # This permits SSH traffic from the 192.168.1.0/24 subnet to the 10.10.10.5 server. @@ -411,10 +424,10 @@ class AccessControlList(SimComponent): >>> # Then if we want to allow a specific IP address from this subnet to SSH into the server >>> router.add_rule( ... action=ACLAction.PERMIT, - ... protocol=IPProtocol.TCP, + ... protocol=IPProtocol["TCP"], ... src_ip_address="192.168.1.25", ... dst_ip_address="10.10.10.5", - ... dst_port=Port.SSH, + ... dst_port=Port["SSH"], ... position=4 ... ) @@ -485,11 +498,11 @@ class AccessControlList(SimComponent): def get_relevant_rules( self, - protocol: IPProtocol, + protocol: str, src_ip_address: Union[str, IPv4Address], - src_port: Port, + src_port: int, dst_ip_address: Union[str, IPv4Address], - dst_port: Port, + dst_port: int, ) -> List[ACLRule]: """ Get the list of relevant rules for a packet with given properties. @@ -552,13 +565,13 @@ class AccessControlList(SimComponent): [ index, rule.action.name if rule.action else "ANY", - rule.protocol.name if rule.protocol else "ANY", + rule.protocol if rule.protocol else "ANY", rule.src_ip_address if rule.src_ip_address else "ANY", rule.src_wildcard_mask if rule.src_wildcard_mask else "ANY", - f"{rule.src_port.value} ({rule.src_port.name})" if rule.src_port else "ANY", + f"{rule.src_port} ({rule.src_port})" if rule.src_port else "ANY", rule.dst_ip_address if rule.dst_ip_address else "ANY", rule.dst_wildcard_mask if rule.dst_wildcard_mask else "ANY", - f"{rule.dst_port.value} ({rule.dst_port.name})" if rule.dst_port else "ANY", + f"{rule.dst_port} ({rule.dst_port})" if rule.dst_port else "ANY", rule.match_count, ] ) @@ -1088,17 +1101,17 @@ class RouterSessionManager(SessionManager): def resolve_outbound_transmission_details( self, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[Port] = None, - dst_port: Optional[Port] = None, - protocol: Optional[IPProtocol] = None, + src_port: Optional[int] = None, + dst_port: Optional[int] = None, + protocol: Optional[str] = None, session_id: Optional[str] = None, ) -> Tuple[ Optional[RouterInterface], Optional[str], IPv4Address, - Optional[Port], - Optional[Port], - Optional[IPProtocol], + Optional[int], + Optional[int], + Optional[str], bool, ]: """ @@ -1118,19 +1131,19 @@ class RouterSessionManager(SessionManager): treats the transmission as a broadcast to that network. Optional. :type dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] :param src_port: The source port number for the transmission. Optional. - :type src_port: Optional[Port] + :type src_port: Optional[int] :param dst_port: The destination port number for the transmission. Optional. - :type dst_port: Optional[Port] + :type dst_port: Optional[int] :param protocol: The IP protocol to be used for the transmission. Optional. - :type protocol: Optional[IPProtocol] + :type protocol: Optional[str] :param session_id: The session ID associated with the transmission. If provided, the session details override other parameters. Optional. :type session_id: Optional[str] :return: A tuple containing the resolved outbound network interface, destination MAC address, destination IP address, source port, destination port, protocol, and a boolean indicating whether the transmission is a broadcast. - :rtype: Tuple[Optional[RouterInterface], Optional[str], IPv4Address, Optional[Port], Optional[Port], - Optional[IPProtocol], bool] + :rtype: Tuple[Optional[RouterInterface], Optional[str], IPv4Address, Optional[int], Optional[int], + Optional[str], bool] """ if dst_ip_address and not isinstance(dst_ip_address, (IPv4Address, IPv4Network)): dst_ip_address = IPv4Address(dst_ip_address) @@ -1257,8 +1270,8 @@ class Router(NetworkNode): Initializes the router's ACL (Access Control List) with default rules, permitting essential protocols like ARP and ICMP, which are necessary for basic network operations and diagnostics. """ - self.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - self.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + self.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + self.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) def setup_for_episode(self, episode: int): """ @@ -1357,9 +1370,9 @@ class Router(NetworkNode): """ dst_ip_address = frame.ip.dst_ip_address dst_port = None - if frame.ip.protocol == IPProtocol.TCP: + if frame.ip.protocol == IPProtocol["TCP"]: dst_port = frame.tcp.dst_port - elif frame.ip.protocol == IPProtocol.UDP: + elif frame.ip.protocol == IPProtocol["UDP"]: dst_port = frame.udp.dst_port if self.ip_is_router_interface(dst_ip_address) and ( diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 3cb4c515..d73bc756 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -116,7 +116,7 @@ class WirelessRouter(Router): >>> wireless_router.configure_wireless_access_point( ... ip_address="10.10.10.1", ... subnet_mask="255.255.255.0" - ... frequency=AirSpaceFrequency.WIFI_2_4 + ... frequency=AirSpaceFrequency["WIFI_2_4"] ... ) """ @@ -153,7 +153,7 @@ class WirelessRouter(Router): self, ip_address: IPV4Address, subnet_mask: IPV4Address, - frequency: Optional[AirSpaceFrequency] = AirSpaceFrequency.WIFI_2_4, + frequency: Optional[int] = AirSpaceFrequency["WIFI_2_4"], ): """ Configures a wireless access point (WAP). @@ -168,10 +168,10 @@ class WirelessRouter(Router): :param subnet_mask: The subnet mask associated with the IP address :param frequency: The operating frequency of the wireless access point, defined by the AirSpaceFrequency enum. This determines the frequency band (e.g., 2.4 GHz or 5 GHz) the access point will use for wireless - communication. Default is AirSpaceFrequency.WIFI_2_4. + communication. Default is AirSpaceFrequency["WIFI_2_4"]. """ if not frequency: - frequency = AirSpaceFrequency.WIFI_2_4 + frequency = AirSpaceFrequency["WIFI_2_4"] self.sys_log.info("Configuring wireless access point") self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index cb0965eb..a73f3b12 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -79,9 +79,9 @@ def client_server_routed() -> Network: server_1.power_on() network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1]) - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) return network @@ -271,23 +271,23 @@ def arcd_uc2_network() -> Network: security_suite.connect_nic(NIC(ip_address="192.168.10.110", subnet_mask="255.255.255.0")) network.connect(endpoint_b=security_suite.network_interface[2], endpoint_a=switch_2.network_interface[7]) - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) # Allow PostgreSQL requests router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=0 + action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 ) # Allow DNS requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=1) # Allow FTP requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=2) # Open port 80 for web server - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=3) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) return network diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index e2a7b6a0..ef060bc7 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -8,9 +8,9 @@ from primaite.simulator.network.protocols.packet import DataPacket class MasqueradePacket(DataPacket): """Represents an generic malicious packet that is masquerading as another protocol.""" - masquerade_protocol: Enum # The 'Masquerade' protocol that is currently in use + masquerade_protocol: str # The 'Masquerade' protocol that is currently in use - masquerade_port: Enum # The 'Masquerade' port that is currently in use + masquerade_port: int # The 'Masquerade' port that is currently in use class C2Packet(MasqueradePacket): diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index 159eca7f..b9bc48d9 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -70,15 +70,15 @@ class Frame(BaseModel): msg = "Network Frame cannot have both a TCP header and a UDP header" _LOGGER.error(msg) raise ValueError(msg) - if kwargs["ip"].protocol == IPProtocol.TCP and not kwargs.get("tcp"): + if kwargs["ip"].protocol == IPProtocol["TCP"] and not kwargs.get("tcp"): msg = "Cannot build a Frame using the TCP IP Protocol without a TCPHeader" _LOGGER.error(msg) raise ValueError(msg) - if kwargs["ip"].protocol == IPProtocol.UDP and not kwargs.get("udp"): + if kwargs["ip"].protocol == IPProtocol["UDP"] and not kwargs.get("udp"): msg = "Cannot build a Frame using the UDP IP Protocol without a UDPHeader" _LOGGER.error(msg) raise ValueError(msg) - if kwargs["ip"].protocol == IPProtocol.ICMP and not kwargs.get("icmp"): + if kwargs["ip"].protocol == IPProtocol["ICMP"] and not kwargs.get("icmp"): msg = "Cannot build a Frame using the ICMP IP Protocol without a ICMPPacket" _LOGGER.error(msg) raise ValueError(msg) @@ -165,7 +165,7 @@ class Frame(BaseModel): :return: True if the Frame is an ARP packet, otherwise False. """ - return self.udp.dst_port == Port.ARP + return self.udp.dst_port == Port["ARP"] @property def is_icmp(self) -> bool: diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index d493cbdf..36ff2751 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -9,25 +9,32 @@ from primaite.utils.validators import IPV4Address _LOGGER = getLogger(__name__) -class IPProtocol(Enum): - """ - Enum representing transport layer protocols in IP header. +IPProtocol : dict[str, str] = dict( + NONE = "none", + TCP = "tcp", + UDP = "udp", + ICMP = "icmp", +) - .. _List of IPProtocols: - """ +# class IPProtocol(Enum): +# """ +# Enum representing transport layer protocols in IP header. - NONE = "none" - """Placeholder for a non-protocol.""" - TCP = "tcp" - """Transmission Control Protocol.""" - UDP = "udp" - """User Datagram Protocol.""" - ICMP = "icmp" - """Internet Control Message Protocol.""" +# .. _List of IPProtocols: +# """ - def model_dump(self) -> str: - """Return as JSON-serialisable string.""" - return self.name +# NONE = "none" +# """Placeholder for a non-protocol.""" +# TCP = "tcp" +# """Transmission Control Protocol.""" +# UDP = "udp" +# """User Datagram Protocol.""" +# ICMP = "icmp" +# """Internet Control Message Protocol.""" + +# def model_dump(self) -> str: +# """Return as JSON-serialisable string.""" +# return self.name class Precedence(Enum): @@ -81,7 +88,7 @@ class IPPacket(BaseModel): >>> ip_packet = IPPacket( ... src_ip_address=IPv4Address('192.168.0.1'), ... dst_ip_address=IPv4Address('10.0.0.1'), - ... protocol=IPProtocol.TCP, + ... protocol=IPProtocol["TCP"], ... ttl=64, ... precedence=Precedence.CRITICAL ... ) @@ -91,7 +98,7 @@ class IPPacket(BaseModel): "Source IP address." dst_ip_address: IPV4Address "Destination IP address." - protocol: IPProtocol = IPProtocol.TCP + protocol: str = IPProtocol["TCP"] "IPProtocol." ttl: int = 64 "Time to Live (TTL) for the packet." diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index 7f0d2d7a..c77ef532 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -5,76 +5,112 @@ from typing import List, Union from pydantic import BaseModel -class Port(Enum): - """ - Enumeration of common known TCP/UDP ports used by protocols for operation of network applications. +Port: dict[str, int] = dict( + UNUSED = -1, + NONE = 0, + WOL = 9, + FTP_DATA = 20, + FTP = 21, + SSH = 22, + SMTP = 25, + DNS = 53, + HTTP = 80, + POP3 = 110, + SFTP = 115, + NTP = 123, + IMAP = 143, + SNMP = 161, + SNMP_TRAP = 162, + ARP = 219, + LDAP = 389, + HTTPS = 443, + SMB = 445, + IPP = 631, + SQL_SERVER = 1433, + MYSQL = 3306, + RDP = 3389, + RTP = 5004, + RTP_ALT = 5005, + DNS_ALT = 5353, + HTTP_ALT = 8080, + HTTPS_ALT = 8443, + POSTGRES_SERVER = 5432, +) - .. _List of Ports: - """ +# class Port(): +# def __getattr__() - UNUSED = -1 - "An unused port stub." - NONE = 0 - "Place holder for a non-port." - WOL = 9 - "Wake-on-Lan (WOL) - Used to turn or awaken a computer from sleep mode by a network message." - FTP_DATA = 20 - "File Transfer [Default Data]" - FTP = 21 - "File Transfer Protocol (FTP) - FTP control (command)" - SSH = 22 - "Secure Shell (SSH) - Used for secure remote access and command execution." - SMTP = 25 - "Simple Mail Transfer Protocol (SMTP) - Used for email delivery between servers." - DNS = 53 - "Domain Name System (DNS) - Used for translating domain names to IP addresses." - HTTP = 80 - "HyperText Transfer Protocol (HTTP) - Used for web traffic." - POP3 = 110 - "Post Office Protocol version 3 (POP3) - Used for retrieving emails from a mail server." - SFTP = 115 - "Secure File Transfer Protocol (SFTP) - Used for secure file transfer over SSH." - NTP = 123 - "Network Time Protocol (NTP) - Used for clock synchronization between computer systems." - IMAP = 143 - "Internet Message Access Protocol (IMAP) - Used for retrieving emails from a mail server." - SNMP = 161 - "Simple Network Management Protocol (SNMP) - Used for network device management." - SNMP_TRAP = 162 - "SNMP Trap - Used for sending SNMP notifications (traps) to a network management system." - ARP = 219 - "Address resolution Protocol - Used to connect a MAC address to an IP address." - LDAP = 389 - "Lightweight Directory Access Protocol (LDAP) - Used for accessing and modifying directory information." - HTTPS = 443 - "HyperText Transfer Protocol Secure (HTTPS) - Used for secure web traffic." - SMB = 445 - "Server Message Block (SMB) - Used for file sharing and printer sharing in Windows environments." - IPP = 631 - "Internet Printing Protocol (IPP) - Used for printing over the internet or an intranet." - SQL_SERVER = 1433 - "Microsoft SQL Server Database Engine - Used for communication with the SQL Server." - MYSQL = 3306 - "MySQL Database Server - Used for MySQL database communication." - RDP = 3389 - "Remote Desktop Protocol (RDP) - Used for remote desktop access to Windows machines." - RTP = 5004 - "Real-time Transport Protocol (RTP) - Used for transmitting real-time media, e.g., audio and video." - RTP_ALT = 5005 - "Alternative port for RTP (RTP_ALT) - Used in some configurations for transmitting real-time media." - DNS_ALT = 5353 - "Alternative port for DNS (DNS_ALT) - Used in some configurations for DNS service." - HTTP_ALT = 8080 - "Alternative port for HTTP (HTTP_ALT) - Often used as an alternative HTTP port for web applications." - HTTPS_ALT = 8443 - "Alternative port for HTTPS (HTTPS_ALT) - Used in some configurations for secure web traffic." - POSTGRES_SERVER = 5432 - "Postgres SQL Server." +# class Port(Enum): +# """ +# Enumeration of common known TCP/UDP ports used by protocols for operation of network applications. - def model_dump(self) -> str: - """Return a json-serialisable string.""" - return self.name +# .. _List of Ports: +# """ + +# UNUSED = -1 +# "An unused port stub." + +# NONE = 0 +# "Place holder for a non-port." +# WOL = 9 +# "Wake-on-Lan (WOL) - Used to turn or awaken a computer from sleep mode by a network message." +# FTP_DATA = 20 +# "File Transfer [Default Data]" +# FTP = 21 +# "File Transfer Protocol (FTP) - FTP control (command)" +# SSH = 22 +# "Secure Shell (SSH) - Used for secure remote access and command execution." +# SMTP = 25 +# "Simple Mail Transfer Protocol (SMTP) - Used for email delivery between servers." +# DNS = 53 +# "Domain Name System (DNS) - Used for translating domain names to IP addresses." +# HTTP = 80 +# "HyperText Transfer Protocol (HTTP) - Used for web traffic." +# POP3 = 110 +# "Post Office Protocol version 3 (POP3) - Used for retrieving emails from a mail server." +# SFTP = 115 +# "Secure File Transfer Protocol (SFTP) - Used for secure file transfer over SSH." +# NTP = 123 +# "Network Time Protocol (NTP) - Used for clock synchronization between computer systems." +# IMAP = 143 +# "Internet Message Access Protocol (IMAP) - Used for retrieving emails from a mail server." +# SNMP = 161 +# "Simple Network Management Protocol (SNMP) - Used for network device management." +# SNMP_TRAP = 162 +# "SNMP Trap - Used for sending SNMP notifications (traps) to a network management system." +# ARP = 219 +# "Address resolution Protocol - Used to connect a MAC address to an IP address." +# LDAP = 389 +# "Lightweight Directory Access Protocol (LDAP) - Used for accessing and modifying directory information." +# HTTPS = 443 +# "HyperText Transfer Protocol Secure (HTTPS) - Used for secure web traffic." +# SMB = 445 +# "Server Message Block (SMB) - Used for file sharing and printer sharing in Windows environments." +# IPP = 631 +# "Internet Printing Protocol (IPP) - Used for printing over the internet or an intranet." +# SQL_SERVER = 1433 +# "Microsoft SQL Server Database Engine - Used for communication with the SQL Server." +# MYSQL = 3306 +# "MySQL Database Server - Used for MySQL database communication." +# RDP = 3389 +# "Remote Desktop Protocol (RDP) - Used for remote desktop access to Windows machines." +# RTP = 5004 +# "Real-time Transport Protocol (RTP) - Used for transmitting real-time media, e.g., audio and video." +# RTP_ALT = 5005 +# "Alternative port for RTP (RTP_ALT) - Used in some configurations for transmitting real-time media." +# DNS_ALT = 5353 +# "Alternative port for DNS (DNS_ALT) - Used in some configurations for DNS service." +# HTTP_ALT = 8080 +# "Alternative port for HTTP (HTTP_ALT) - Often used as an alternative HTTP port for web applications." +# HTTPS_ALT = 8443 +# "Alternative port for HTTPS (HTTPS_ALT) - Used in some configurations for secure web traffic." +# POSTGRES_SERVER = 5432 +# "Postgres SQL Server." + +# def model_dump(self) -> str: +# """Return a json-serialisable string.""" +# return self.name class UDPHeader(BaseModel): @@ -87,13 +123,13 @@ class UDPHeader(BaseModel): :Example: >>> udp_header = UDPHeader( - ... src_port=Port.HTTP_ALT, - ... dst_port=Port.HTTP, + ... src_port=Port["HTTP_ALT"], + ... dst_port=Port["HTTP"], ... ) """ - src_port: Union[Port, int] - dst_port: Union[Port, int] + src_port: int + dst_port: int class TCPFlags(Enum): @@ -126,12 +162,12 @@ class TCPHeader(BaseModel): :Example: >>> tcp_header = TCPHeader( - ... src_port=Port.HTTP_ALT, - ... dst_port=Port.HTTP, + ... src_port=Port["HTTP_ALT"], + ... dst_port=Port["HTTP"], ... flags=[TCPFlags.SYN, TCPFlags.ACK] ... ) """ - src_port: Port - dst_port: Port + src_port: int + dst_port: int flags: List[TCPFlags] = [TCPFlags.SYN] diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 3f80c745..170e2647 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -90,8 +90,8 @@ class DatabaseClient(Application, identifier="DatabaseClient"): def __init__(self, **kwargs): kwargs["name"] = "DatabaseClient" - kwargs["port"] = Port.POSTGRES_SERVER - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["POSTGRES_SERVER"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) def _init_request_manager(self) -> RequestManager: diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index c87eaaf5..74bce85d 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -24,8 +24,8 @@ class PortScanPayload(SimComponent): """ ip_address: IPV4Address - port: Port - protocol: IPProtocol + port: int + protocol: str request: bool = True def describe_state(self) -> Dict: @@ -37,8 +37,8 @@ class PortScanPayload(SimComponent): """ state = super().describe_state() state["ip_address"] = str(self.ip_address) - state["port"] = self.port.value - state["protocol"] = self.protocol.value + state["port"] = self.port + state["protocol"] = self.protocol state["request"] = self.request return state @@ -64,8 +64,8 @@ class NMAP(Application, identifier="NMAP"): def __init__(self, **kwargs): kwargs["name"] = "NMAP" - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.NONE + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["NONE"] super().__init__(**kwargs) def _can_perform_network_action(self) -> bool: @@ -218,14 +218,14 @@ class NMAP(Application, identifier="NMAP"): print(table.get_string(sortby="IP Address")) return active_nodes - def _determine_port_scan_type(self, target_ip_addresses: List[IPV4Address], target_ports: List[Port]) -> str: + def _determine_port_scan_type(self, target_ip_addresses: List[IPV4Address], target_ports: List[int]) -> str: """ Determine the type of port scan based on the number of target IP addresses and ports. :param target_ip_addresses: The list of target IP addresses. :type target_ip_addresses: List[IPV4Address] :param target_ports: The list of target ports. - :type target_ports: List[Port] + :type target_ports: List[int] :return: The type of port scan. :rtype: str @@ -238,8 +238,8 @@ class NMAP(Application, identifier="NMAP"): def _check_port_open_on_ip_address( self, ip_address: IPv4Address, - port: Port, - protocol: IPProtocol, + port: int, + protocol: str, is_re_attempt: bool = False, port_scan_uuid: Optional[str] = None, ) -> bool: @@ -251,7 +251,7 @@ class NMAP(Application, identifier="NMAP"): :param port: The target port. :type port: Port :param protocol: The protocol used for the port scan. - :type protocol: IPProtocol + :type protocol: str :param is_re_attempt: Flag indicating if this is a reattempt. Defaults to False. :type is_re_attempt: bool :param port_scan_uuid: The UUID of the port scan payload. Defaults to None. @@ -272,8 +272,8 @@ class NMAP(Application, identifier="NMAP"): payload = PortScanPayload(ip_address=ip_address, port=port, protocol=protocol) self._active_port_scans[payload.uuid] = payload self.sys_log.info( - f"{self.name}: Sending port scan request over {payload.protocol.name} on port {payload.port.value} " - f"({payload.port.name}) to {payload.ip_address}" + f"{self.name}: Sending port scan request over {payload.protocol} on port {payload.port} " + f"({payload.port}) to {payload.ip_address}" ) self.software_manager.send_payload_to_session_manager( payload=payload, dest_ip_address=ip_address, src_port=port, dest_port=port, ip_protocol=protocol @@ -295,8 +295,8 @@ class NMAP(Application, identifier="NMAP"): self._active_port_scans.pop(payload.uuid) self._port_scan_responses[payload.uuid] = payload self.sys_log.info( - f"{self.name}: Received port scan response from {payload.ip_address} on port {payload.port.value} " - f"({payload.port.name}) over {payload.protocol.name}" + f"{self.name}: Received port scan response from {payload.ip_address} on port {payload.port} " + f"({payload.port}) over {payload.protocol}" ) def _process_port_scan_request(self, payload: PortScanPayload, session_id: str) -> None: @@ -311,8 +311,8 @@ class NMAP(Application, identifier="NMAP"): if self.software_manager.check_port_is_open(port=payload.port, protocol=payload.protocol): payload.request = False self.sys_log.info( - f"{self.name}: Responding to port scan request for port {payload.port.value} " - f"({payload.port.name}) over {payload.protocol.name}", + f"{self.name}: Responding to port scan request for port {payload.port} " + f"({payload.port}) over {payload.protocol}", ) self.software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id) @@ -320,20 +320,20 @@ class NMAP(Application, identifier="NMAP"): def port_scan( self, target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]], - target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] = None, - target_port: Optional[Union[Port, List[Port]]] = None, + target_protocol: Optional[Union[str, List[str]]] = None, + target_port: Optional[Union[int, List[int]]] = None, show: bool = True, json_serializable: bool = False, - ) -> Dict[IPv4Address, Dict[IPProtocol, List[Port]]]: + ) -> Dict[IPv4Address, Dict[str, List[int]]]: """ Perform a port scan on the target IP address(es). :param target_ip_address: The target IP address(es) or network(s) for the port scan. :type target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]] :param target_protocol: The protocol(s) to use for the port scan. Defaults to None, which includes TCP and UDP. - :type target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] + :type target_protocol: Optional[Union[str, List[str]]] :param target_port: The port(s) to scan. Defaults to None, which includes all valid ports. - :type target_port: Optional[Union[Port, List[Port]]] + :type target_port: Optional[Union[int, List[int]]] :param show: Flag indicating whether to display the scan results. Defaults to True. :type show: bool :param json_serializable: Flag indicating whether the return value should be JSON serializable. Defaults to @@ -341,19 +341,19 @@ class NMAP(Application, identifier="NMAP"): :type json_serializable: bool :return: A dictionary mapping IP addresses to protocols and lists of open ports. - :rtype: Dict[IPv4Address, Dict[IPProtocol, List[Port]]] + :rtype: Dict[IPv4Address, Dict[str, List[int]]] """ ip_addresses = self._explode_ip_address_network_array(target_ip_address) - if isinstance(target_port, Port): + if isinstance(target_port, int): target_port = [target_port] elif target_port is None: - target_port = [port for port in Port if port not in {Port.NONE, Port.UNUSED}] + target_port = [port for port in Port if port not in {Port["NONE"], Port["UNUSED"]}] - if isinstance(target_protocol, IPProtocol): + if isinstance(target_protocol, str): target_protocol = [target_protocol] elif target_protocol is None: - target_protocol = [IPProtocol.TCP, IPProtocol.UDP] + target_protocol = [IPProtocol["TCP"], IPProtocol["UDP"]] scan_type = self._determine_port_scan_type(list(ip_addresses), target_port) active_ports = {} @@ -372,10 +372,10 @@ class NMAP(Application, identifier="NMAP"): if port_open: if show: - table.add_row([ip_address, port.value, port.name, protocol.name]) + table.add_row([ip_address, port, port, protocol]) _ip_address = ip_address if not json_serializable else str(ip_address) - _protocol = protocol if not json_serializable else protocol.value - _port = port if not json_serializable else port.value + _protocol = protocol if not json_serializable else protocol + _port = port if not json_serializable else port if _ip_address not in active_ports: active_ports[_ip_address] = dict() if _protocol not in active_ports[_ip_address]: @@ -390,12 +390,12 @@ class NMAP(Application, identifier="NMAP"): def network_service_recon( self, target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]], - target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] = None, - target_port: Optional[Union[Port, List[Port]]] = None, + target_protocol: Optional[Union[str, List[str]]] = None, + target_port: Optional[Union[int, List[int]]] = None, show: bool = True, show_online_only: bool = True, json_serializable: bool = False, - ) -> Dict[IPv4Address, Dict[IPProtocol, List[Port]]]: + ) -> Dict[IPv4Address, Dict[str, List[int]]]: """ Perform a network service reconnaissance which includes a ping scan followed by a port scan. @@ -408,9 +408,9 @@ class NMAP(Application, identifier="NMAP"): :param target_ip_address: The target IP address(es) or network(s) for the port scan. :type target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]] :param target_protocol: The protocol(s) to use for the port scan. Defaults to None, which includes TCP and UDP. - :type target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] + :type target_protocol: Optional[Union[str, List[str]]] :param target_port: The port(s) to scan. Defaults to None, which includes all valid ports. - :type target_port: Optional[Union[Port, List[Port]]] + :type target_port: Optional[Union[int, List[int]]] :param show: Flag indicating whether to display the scan results. Defaults to True. :type show: bool :param show_online_only: Flag indicating whether to show only the online hosts. Defaults to True. @@ -420,7 +420,7 @@ class NMAP(Application, identifier="NMAP"): :type json_serializable: bool :return: A dictionary mapping IP addresses to protocols and lists of open ports. - :rtype: Dict[IPv4Address, Dict[IPProtocol, List[Port]]] + :rtype: Dict[IPv4Address, Dict[str, List[int]]] """ ping_scan_results = self.ping_scan( target_ip_address=target_ip_address, show=show, show_online_only=show_online_only, json_serializable=False diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 5d4cc8e0..d442d968 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -81,10 +81,10 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_frequency: int = Field(default=5, ge=1) """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" - masquerade_protocol: IPProtocol = Field(default=IPProtocol.TCP) + masquerade_protocol: str = Field(default=IPProtocol["TCP"]) """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - masquerade_port: Port = Field(default=Port.HTTP) + masquerade_port: int = Field(default=Port["HTTP"]) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" c2_config: _C2Opts = _C2Opts() @@ -142,9 +142,9 @@ class AbstractC2(Application, identifier="AbstractC2"): def __init__(self, **kwargs): """Initialise the C2 applications to by default listen for HTTP traffic.""" - kwargs["listen_on_ports"] = {Port.HTTP, Port.FTP, Port.DNS} - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.TCP + kwargs["listen_on_ports"] = {Port["HTTP"], Port["FTP"], Port["DNS"]} + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) @property @@ -367,7 +367,7 @@ class AbstractC2(Application, identifier="AbstractC2"): :rtype: bool """ # Validating that they are valid Enums. - if not isinstance(payload.masquerade_port, Port) or not isinstance(payload.masquerade_protocol, IPProtocol): + if not isinstance(payload.masquerade_port, int) or not isinstance(payload.masquerade_protocol, str): self.sys_log.warning( f"{self.name}: Received invalid Masquerade Values within Keep Alive." f"Port: {payload.masquerade_port} Protocol: {payload.masquerade_protocol}." @@ -410,8 +410,8 @@ class AbstractC2(Application, identifier="AbstractC2"): self.keep_alive_inactivity = 0 self.keep_alive_frequency = 5 self.c2_remote_connection = None - self.c2_config.masquerade_port = Port.HTTP - self.c2_config.masquerade_protocol = IPProtocol.TCP + self.c2_config.masquerade_port = Port["HTTP"] + self.c2_config.masquerade_protocol = IPProtocol["TCP"] @abstractmethod def _confirm_remote_connection(self, timestep: int) -> bool: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index fa0271e5..06453330 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -130,8 +130,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self, c2_server_ip_address: IPv4Address = None, keep_alive_frequency: int = 5, - masquerade_protocol: Enum = IPProtocol.TCP, - masquerade_port: Enum = Port.HTTP, + masquerade_protocol: str = IPProtocol["TCP"], + masquerade_port: int = Port["HTTP"], ) -> bool: """ Configures the C2 beacon to communicate with the C2 server. diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index fefb22c3..d74ae384 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -50,8 +50,8 @@ class DataManipulationBot(Application, identifier="DataManipulationBot"): def __init__(self, **kwargs): kwargs["name"] = "DataManipulationBot" - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.NONE + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["NONE"] super().__init__(**kwargs) self._db_connection: Optional[DatabaseClientConnection] = None diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index fcad3b3e..2cc99c4a 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -35,7 +35,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): target_ip_address: Optional[IPv4Address] = None """IP address of the target service.""" - target_port: Optional[Port] = None + target_port: Optional[int] = None """Port of the target service.""" payload: Optional[str] = None @@ -94,7 +94,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): def configure( self, target_ip_address: IPv4Address, - target_port: Optional[Port] = Port.POSTGRES_SERVER, + target_port: Optional[int] = Port["POSTGRES_SERVER"], payload: Optional[str] = None, repeat: bool = False, port_scan_p_of_success: float = 0.1, @@ -105,7 +105,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): Configure the Denial of Service bot. :param: target_ip_address: The IP address of the Node containing the target service. - :param: target_port: The port of the target service. Optional - Default is `Port.HTTP` + :param: target_port: The port of the target service. Optional - Default is `Port["HTTP"]` :param: payload: The payload the DoS Bot will throw at the target service. Optional - Default is `None` :param: repeat: If True, the bot will maintain the attack. Optional - Default is `True` :param: port_scan_p_of_success: The chance of the port scan being successful. Optional - Default is 0.1 (10%) diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 2046affc..a819190c 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -27,8 +27,8 @@ class RansomwareScript(Application, identifier="RansomwareScript"): def __init__(self, **kwargs): kwargs["name"] = "RansomwareScript" - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.NONE + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["NONE"] super().__init__(**kwargs) self._db_connection: Optional[DatabaseClientConnection] = None diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 73791676..6707fa52 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -43,10 +43,10 @@ class WebBrowser(Application, identifier="WebBrowser"): def __init__(self, **kwargs): kwargs["name"] = "WebBrowser" - kwargs["protocol"] = IPProtocol.TCP + kwargs["protocol"] = IPProtocol["TCP"] # default for web is port 80 if kwargs.get("port") is None: - kwargs["port"] = Port.HTTP + kwargs["port"] = Port["HTTP"] super().__init__(**kwargs) self.run() @@ -126,7 +126,7 @@ class WebBrowser(Application, identifier="WebBrowser"): if self.send( payload=payload, dest_ip_address=self.domain_name_ip_address, - dest_port=parsed_url.port if parsed_url.port else Port.HTTP, + dest_port=parsed_url.port if parsed_url.port else Port["HTTP"], ): self.sys_log.info( f"{self.name}: Received HTTP {payload.request_method.name} " @@ -154,7 +154,7 @@ class WebBrowser(Application, identifier="WebBrowser"): self, payload: HttpRequestPacket, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = Port.HTTP, + dest_port: Optional[int] = Port["HTTP"], session_id: Optional[str] = None, **kwargs, ) -> bool: diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index b7e2c021..172be453 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -34,14 +34,14 @@ class Session(SimComponent): :param connected: A flag indicating whether the session is connected. """ - protocol: IPProtocol + protocol: str with_ip_address: IPv4Address - src_port: Optional[Port] - dst_port: Optional[Port] + src_port: Optional[int] + dst_port: Optional[int] connected: bool = False @classmethod - def from_session_key(cls, session_key: Tuple[IPProtocol, IPv4Address, Optional[Port], Optional[Port]]) -> Session: + def from_session_key(cls, session_key: Tuple[str, IPv4Address, Optional[int], Optional[int]]) -> Session: """ Create a Session instance from a session key tuple. @@ -77,7 +77,7 @@ class SessionManager: def __init__(self, sys_log: SysLog): self.sessions_by_key: Dict[ - Tuple[IPProtocol, IPv4Address, IPv4Address, Optional[Port], Optional[Port]], Session + Tuple[str, IPv4Address, IPv4Address, Optional[int], Optional[int]], Session ] = {} self.sessions_by_uuid: Dict[str, Session] = {} self.sys_log: SysLog = sys_log @@ -103,7 +103,7 @@ class SessionManager: @staticmethod def _get_session_key( frame: Frame, inbound_frame: bool = True - ) -> Tuple[IPProtocol, IPv4Address, Optional[Port], Optional[Port]]: + ) -> Tuple[str, IPv4Address, Optional[int], Optional[int]]: """ Extracts the session key from the given frame. @@ -111,15 +111,15 @@ class SessionManager: - IPProtocol: The transport protocol (e.g. TCP, UDP, ICMP). - IPv4Address: The source IP address. - IPv4Address: The destination IP address. - - Optional[Port]: The source port number (if applicable). - - Optional[Port]: The destination port number (if applicable). + - Optional[int]: The source port number (if applicable). + - Optional[int]: The destination port number (if applicable). :param frame: The network frame from which to extract the session key. :return: A tuple containing the session key. """ protocol = frame.ip.protocol with_ip_address = frame.ip.src_ip_address - if protocol == IPProtocol.TCP: + if protocol == IPProtocol["TCP"]: if inbound_frame: src_port = frame.tcp.src_port dst_port = frame.tcp.dst_port @@ -127,7 +127,7 @@ class SessionManager: dst_port = frame.tcp.src_port src_port = frame.tcp.dst_port with_ip_address = frame.ip.dst_ip_address - elif protocol == IPProtocol.UDP: + elif protocol == IPProtocol["UDP"]: if inbound_frame: src_port = frame.udp.src_port dst_port = frame.udp.dst_port @@ -167,17 +167,17 @@ class SessionManager: def resolve_outbound_transmission_details( self, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[Port] = None, - dst_port: Optional[Port] = None, - protocol: Optional[IPProtocol] = None, + src_port: Optional[int] = None, + dst_port: Optional[int] = None, + protocol: Optional[str] = None, session_id: Optional[str] = None, ) -> Tuple[ Optional["NetworkInterface"], Optional[str], IPv4Address, - Optional[Port], - Optional[Port], - Optional[IPProtocol], + Optional[int], + Optional[int], + Optional[str], bool, ]: """ @@ -196,19 +196,19 @@ class SessionManager: treats the transmission as a broadcast to that network. Optional. :type dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] :param src_port: The source port number for the transmission. Optional. - :type src_port: Optional[Port] + :type src_port: Optional[int] :param dst_port: The destination port number for the transmission. Optional. - :type dst_port: Optional[Port] + :type dst_port: Optional[int] :param protocol: The IP protocol to be used for the transmission. Optional. - :type protocol: Optional[IPProtocol] + :type protocol: Optional[str] :param session_id: The session ID associated with the transmission. If provided, the session details override other parameters. Optional. :type session_id: Optional[str] :return: A tuple containing the resolved outbound network interface, destination MAC address, destination IP address, source port, destination port, protocol, and a boolean indicating whether the transmission is a broadcast. - :rtype: Tuple[Optional["NetworkInterface"], Optional[str], IPv4Address, Optional[Port], Optional[Port], - Optional[IPProtocol], bool] + :rtype: Tuple[Optional["NetworkInterface"], Optional[str], IPv4Address, Optional[int], Optional[int], + Optional[str], bool] """ if dst_ip_address and not isinstance(dst_ip_address, (IPv4Address, IPv4Network)): dst_ip_address = IPv4Address(dst_ip_address) @@ -259,10 +259,10 @@ class SessionManager: self, payload: Any, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[Port] = None, - dst_port: Optional[Port] = None, + src_port: Optional[int] = None, + dst_port: Optional[int] = None, session_id: Optional[str] = None, - ip_protocol: IPProtocol = IPProtocol.TCP, + ip_protocol: str = IPProtocol["TCP"], icmp_packet: Optional[ICMPPacket] = None, ) -> Union[Any, None]: """ @@ -286,7 +286,7 @@ class SessionManager: dst_mac_address = payload.target_mac_addr outbound_network_interface = self.resolve_outbound_network_interface(payload.target_ip_address) is_broadcast = payload.request - ip_protocol = IPProtocol.UDP + ip_protocol = IPProtocol["UDP"] else: vals = self.resolve_outbound_transmission_details( dst_ip_address=dst_ip_address, @@ -311,26 +311,26 @@ class SessionManager: if not outbound_network_interface or not dst_mac_address: return False - if not (src_port or dst_port): + if src_port is None and dst_port is None: raise ValueError( "Failed to resolve src or dst port. Have you sent the port from the service or application?" ) tcp_header = None udp_header = None - if ip_protocol == IPProtocol.TCP: + if ip_protocol == IPProtocol["TCP"]: tcp_header = TCPHeader( src_port=dst_port, dst_port=dst_port, ) - elif ip_protocol == IPProtocol.UDP: + elif ip_protocol == IPProtocol["UDP"]: udp_header = UDPHeader( src_port=dst_port, dst_port=dst_port, ) # TODO: Only create IP packet if not ARP # ip_packet = None - # if dst_port != Port.ARP: + # if dst_port != Port["ARP"]: # IPPacket( # src_ip_address=outbound_network_interface.ip_address, # dst_ip_address=dst_ip_address, @@ -387,7 +387,7 @@ class SessionManager: elif frame.udp: dst_port = frame.udp.dst_port elif frame.icmp: - dst_port = Port.NONE + dst_port = Port["NONE"] self.software_manager.receive_payload_from_session_manager( payload=frame.payload, port=dst_port, @@ -413,5 +413,5 @@ class SessionManager: table.align = "l" table.title = f"{self.sys_log.hostname} Session Manager" for session in self.sessions_by_key.values(): - table.add_row([session.dst_ip_address, session.dst_port.value, session.protocol.name]) + table.add_row([session.dst_ip_address, session.dst_port, session.protocol]) print(table) diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index d45611ed..8eac33fa 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -52,7 +52,7 @@ class SoftwareManager: self.session_manager = session_manager self.software: Dict[str, Union[Service, Application]] = {} self._software_class_to_name_map: Dict[Type[IOSoftware], str] = {} - self.port_protocol_mapping: Dict[Tuple[Port, IPProtocol], Union[Service, Application]] = {} + self.port_protocol_mapping: Dict[Tuple[int, str], Union[Service, Application]] = {} self.sys_log: SysLog = sys_log self.file_system: FileSystem = file_system self.dns_server: Optional[IPv4Address] = dns_server @@ -67,7 +67,7 @@ class SoftwareManager: """Provides access to the ICMP service instance, if installed.""" return self.software.get("ICMP") # noqa - def get_open_ports(self) -> List[Port]: + def get_open_ports(self) -> List[int]: """ Get a list of open ports. @@ -81,7 +81,7 @@ class SoftwareManager: open_ports += list(software.listen_on_ports) return open_ports - def check_port_is_open(self, port: Port, protocol: IPProtocol) -> bool: + def check_port_is_open(self, port: int, protocol: str) -> bool: """ Check if a specific port is open and running a service using the specified protocol. @@ -93,7 +93,7 @@ class SoftwareManager: :param port: The port to check. :type port: Port :param protocol: The protocol to check (e.g., TCP, UDP). - :type protocol: IPProtocol + :type protocol: str :return: True if the port is open and a service is running on it using the specified protocol, False otherwise. :rtype: bool """ @@ -189,9 +189,9 @@ class SoftwareManager: self, payload: Any, dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[Port] = None, - dest_port: Optional[Port] = None, - ip_protocol: IPProtocol = IPProtocol.TCP, + src_port: Optional[int] = None, + dest_port: Optional[int] = None, + ip_protocol: str = IPProtocol["TCP"], session_id: Optional[str] = None, ) -> bool: """ @@ -219,8 +219,8 @@ class SoftwareManager: def receive_payload_from_session_manager( self, payload: Any, - port: Port, - protocol: IPProtocol, + port: int, + protocol: str, session_id: str, from_network_interface: "NIC", frame: Frame, @@ -275,8 +275,8 @@ class SoftwareManager: software_type, software.operating_state.name, software.health_state_actual.name, - software.port.value if software.port != Port.NONE else None, - software.protocol.value, + software.port if software.port != Port["NONE"] else None, + software.protocol, ] ) print(table) diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index efadf189..b8dd5f89 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -26,8 +26,8 @@ class ARP(Service): def __init__(self, **kwargs): kwargs["name"] = "ARP" - kwargs["port"] = Port.ARP - kwargs["protocol"] = IPProtocol.UDP + kwargs["port"] = Port["ARP"] + kwargs["protocol"] = IPProtocol["UDP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index b38e87b4..11ca9eb2 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -38,8 +38,8 @@ class DatabaseService(Service): def __init__(self, **kwargs): kwargs["name"] = "DatabaseService" - kwargs["port"] = Port.POSTGRES_SERVER - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["POSTGRES_SERVER"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self._create_db_file() diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index d7ba0cd4..62f14366 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -22,11 +22,11 @@ class DNSClient(Service): def __init__(self, **kwargs): kwargs["name"] = "DNSClient" - kwargs["port"] = Port.DNS + kwargs["port"] = Port["DNS"] # DNS uses UDP by default # it switches to TCP when the bytes exceed 512 (or 4096) bytes # TCP for now - kwargs["protocol"] = IPProtocol.TCP + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self.start() @@ -95,7 +95,7 @@ class DNSClient(Service): # send a request to check if domain name exists in the DNS Server software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=self.dns_server, dest_port=Port.DNS + payload=payload, dest_ip_address=self.dns_server, dest_port=Port["DNS"] ) # recursively re-call the function passing is_reattempt=True @@ -110,7 +110,7 @@ class DNSClient(Service): payload: DNSPacket, session_id: Optional[str] = None, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = None, + dest_port: Optional[int] = None, **kwargs, ) -> bool: """ diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 8a4bbaed..93895825 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -21,11 +21,11 @@ class DNSServer(Service): def __init__(self, **kwargs): kwargs["name"] = "DNSServer" - kwargs["port"] = Port.DNS + kwargs["port"] = Port["DNS"] # DNS uses UDP by default # it switches to TCP when the bytes exceed 512 (or 4096) bytes # TCP for now - kwargs["protocol"] = IPProtocol.TCP + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self.start() diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index f823e42c..1fce4133 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -25,8 +25,8 @@ class FTPClient(FTPServiceABC): def __init__(self, **kwargs): kwargs["name"] = "FTPClient" - kwargs["port"] = Port.FTP - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["FTP"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self.start() @@ -104,7 +104,7 @@ class FTPClient(FTPServiceABC): def _connect_to_server( self, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = Port.FTP, + dest_port: Optional[int] = Port["FTP"], session_id: Optional[str] = None, is_reattempt: Optional[bool] = False, ) -> bool: @@ -114,7 +114,7 @@ class FTPClient(FTPServiceABC): :param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional. :type: dest_ip_address: Optional[IPv4Address] :param: dest_port: Port of the FTP server the client needs to connect to. Optional. - :type: dest_port: Optional[Port] + :type: dest_port: Optional[int] :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False. :type: is_reattempt: Optional[bool] """ @@ -124,13 +124,13 @@ class FTPClient(FTPServiceABC): # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now # create FTP packet - payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=Port.FTP) + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=Port["FTP"]) if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id): if payload.status_code == FTPStatusCode.OK: self.sys_log.info( f"{self.name}: Successfully connected to FTP Server " - f"{dest_ip_address} via port {payload.ftp_command_args.value}" + f"{dest_ip_address} via port {payload.ftp_command_args}" ) self.add_connection(connection_id="server_connection", session_id=session_id) return True @@ -139,7 +139,7 @@ class FTPClient(FTPServiceABC): # reattempt failed self.sys_log.warning( f"{self.name}: Unable to connect to FTP Server " - f"{dest_ip_address} via port {payload.ftp_command_args.value}" + f"{dest_ip_address} via port {payload.ftp_command_args}" ) return False else: @@ -152,7 +152,7 @@ class FTPClient(FTPServiceABC): return False def _disconnect_from_server( - self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP + self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[int] = Port["FTP"] ) -> bool: """ Connects the client from a given FTP server. @@ -160,7 +160,7 @@ class FTPClient(FTPServiceABC): :param: dest_ip_address: IP address of the FTP server the client needs to disconnect from. Optional. :type: dest_ip_address: Optional[IPv4Address] :param: dest_port: Port of the FTP server the client needs to disconnect from. Optional. - :type: dest_port: Optional[Port] + :type: dest_port: Optional[int] :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. :type: is_reattempt: Optional[bool] """ @@ -179,7 +179,7 @@ class FTPClient(FTPServiceABC): src_file_name: str, dest_folder_name: str, dest_file_name: str, - dest_port: Optional[Port] = Port.FTP, + dest_port: Optional[int] = Port["FTP"], session_id: Optional[str] = None, ) -> bool: """ @@ -203,8 +203,8 @@ class FTPClient(FTPServiceABC): :param: dest_file_name: The name of the file to be saved on the FTP Server. :type: dest_file_name: str - :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. - :type: dest_port: Optional[Port] + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. + :type: dest_port: Optional[int] :param: session_id: The id of the session :type: session_id: Optional[str] @@ -241,7 +241,7 @@ class FTPClient(FTPServiceABC): src_file_name: str, dest_folder_name: str, dest_file_name: str, - dest_port: Optional[Port] = Port.FTP, + dest_port: Optional[int] = Port["FTP"], ) -> bool: """ Request a file from a target IP address. @@ -263,8 +263,8 @@ class FTPClient(FTPServiceABC): :param: dest_file_name: The name of the file to be saved on the FTP Server. :type: dest_file_name: str - :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. - :type: dest_port: Optional[Port] + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. + :type: dest_port: Optional[int] """ # check if FTP is currently connected to IP self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index f02d01f4..701bff79 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -23,8 +23,8 @@ class FTPServer(FTPServiceABC): def __init__(self, **kwargs): kwargs["name"] = "FTPServer" - kwargs["port"] = Port.FTP - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["FTP"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self.start() @@ -52,7 +52,7 @@ class FTPServer(FTPServiceABC): # process server specific commands, otherwise call super if payload.ftp_command == FTPCommand.PORT: # check that the port is valid - if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): + if isinstance(payload.ftp_command_args, int) and (0 <= payload.ftp_command_args < 65535): # return successful connection self.add_connection(connection_id=session_id, session_id=session_id) payload.status_code = FTPStatusCode.OK diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 689a3da7..36245e0f 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -78,7 +78,7 @@ class FTPServiceABC(Service, ABC): dest_folder_name: str, dest_file_name: str, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = None, + dest_port: Optional[int] = None, session_id: Optional[str] = None, is_response: bool = False, ) -> bool: @@ -97,8 +97,8 @@ class FTPServiceABC(Service, ABC): :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. :type: dest_ip_address: Optional[IPv4Address] - :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. - :type: dest_port: Optional[Port] + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. + :type: dest_port: Optional[int] :param: session_id: session ID linked to the FTP Packet. Optional. :type: session_id: Optional[str] @@ -168,7 +168,7 @@ class FTPServiceABC(Service, ABC): payload: FTPPacket, session_id: Optional[str] = None, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = None, + dest_port: Optional[int] = None, **kwargs, ) -> bool: """ diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 6741d86a..a2dfac0d 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -26,8 +26,8 @@ class ICMP(Service): def __init__(self, **kwargs): kwargs["name"] = "ICMP" - kwargs["port"] = Port.NONE - kwargs["protocol"] = IPProtocol.ICMP + kwargs["port"] = Port["NONE"] + kwargs["protocol"] = IPProtocol["ICMP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/icmp/router_icmp.py b/src/primaite/simulator/system/services/icmp/router_icmp.py index 4fdc6baa..19c0ac2d 100644 --- a/src/primaite/simulator/system/services/icmp/router_icmp.py +++ b/src/primaite/simulator/system/services/icmp/router_icmp.py @@ -36,13 +36,13 @@ # self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}") # target_mac_address = self.arp.get_arp_cache_mac_address(frame.ip.src_ip_address) # src_nic = self.arp.get_arp_cache_network_interface(frame.ip.src_ip_address) -# tcp_header = TCPHeader(src_port=Port.ARP, dst_port=Port.ARP) +# tcp_header = TCPHeader(src_port=Port["ARP"], dst_port=Port["ARP"]) # # # Network Layer # ip_packet = IPPacket( # src_ip_address=network_interface.ip_address, # dst_ip_address=frame.ip.src_ip_address, -# protocol=IPProtocol.ICMP, +# protocol=IPProtocol["ICMP"], # ) # # Data Link Layer # ethernet_header = EthernetHeader( diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 8924a821..40b8d273 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -21,8 +21,8 @@ class NTPClient(Service): def __init__(self, **kwargs): kwargs["name"] = "NTPClient" - kwargs["port"] = Port.NTP - kwargs["protocol"] = IPProtocol.UDP + kwargs["port"] = Port["NTP"] + kwargs["protocol"] = IPProtocol["UDP"] super().__init__(**kwargs) self.start() @@ -55,7 +55,7 @@ class NTPClient(Service): payload: NTPPacket, session_id: Optional[str] = None, dest_ip_address: IPv4Address = None, - dest_port: Port = Port.NTP, + dest_port: int = Port["NTP"], **kwargs, ) -> bool: """Requests NTP data from NTP server. diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index 547bbc06..d9de40c6 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -16,8 +16,8 @@ class NTPServer(Service): def __init__(self, **kwargs): kwargs["name"] = "NTPServer" - kwargs["port"] = Port.NTP - kwargs["protocol"] = IPProtocol.UDP + kwargs["port"] = Port["NTP"] + kwargs["protocol"] = IPProtocol["UDP"] super().__init__(**kwargs) self.start() diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index e98e8555..41987aff 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -137,8 +137,8 @@ class Terminal(Service): def __init__(self, **kwargs): kwargs["name"] = "Terminal" - kwargs["port"] = Port.SSH - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["SSH"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 4fc64e1f..c021a86e 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -49,10 +49,10 @@ class WebServer(Service): def __init__(self, **kwargs): kwargs["name"] = "WebServer" - kwargs["protocol"] = IPProtocol.TCP + kwargs["protocol"] = IPProtocol["TCP"] # default for web is port 80 if kwargs.get("port") is None: - kwargs["port"] = Port.HTTP + kwargs["port"] = Port["HTTP"] super().__init__(**kwargs) self._install_web_files() @@ -145,7 +145,7 @@ class WebServer(Service): payload: HttpResponsePacket, session_id: Optional[str] = None, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = None, + dest_port: Optional[int] = None, **kwargs, ) -> bool: """ diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index f1d1b9a1..1880d244 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -251,11 +251,11 @@ class IOSoftware(Software): "Indicates if the software uses TCP protocol for communication. Default is True." udp: bool = True "Indicates if the software uses UDP protocol for communication. Default is True." - port: Port + port: int "The port to which the software is connected." - listen_on_ports: Set[Port] = Field(default_factory=set) + listen_on_ports: Set[int] = Field(default_factory=set) "The set of ports to listen on." - protocol: IPProtocol + protocol: str "The IP Protocol the Software operates on." _connections: Dict[str, Dict] = {} "Active connections." @@ -277,7 +277,7 @@ class IOSoftware(Software): "max_sessions": self.max_sessions, "tcp": self.tcp, "udp": self.udp, - "port": self.port.value, + "port": self.port, } ) return state @@ -386,8 +386,8 @@ class IOSoftware(Software): payload: Any, session_id: Optional[str] = None, dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - dest_port: Optional[Port] = None, - ip_protocol: IPProtocol = IPProtocol.TCP, + dest_port: Optional[int] = None, + ip_protocol: str = IPProtocol["TCP"], **kwargs, ) -> bool: """ diff --git a/tests/conftest.py b/tests/conftest.py index 1bbff8f2..1ffa2146 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,8 +45,8 @@ class DummyService(Service): def __init__(self, **kwargs): kwargs["name"] = "DummyService" - kwargs["port"] = Port.HTTP - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["HTTP"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) def receive(self, payload: Any, session_id: str, **kwargs) -> bool: @@ -58,8 +58,8 @@ class DummyApplication(Application, identifier="DummyApplication"): def __init__(self, **kwargs): kwargs["name"] = "DummyApplication" - kwargs["port"] = Port.HTTP - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["HTTP"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: @@ -77,7 +77,7 @@ def uc2_network() -> Network: @pytest.fixture(scope="function") def service(file_system) -> DummyService: return DummyService( - name="DummyService", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="dummy_service") + name="DummyService", port=Port["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_service") ) @@ -90,7 +90,7 @@ def service_class(): def application(file_system) -> DummyApplication: return DummyApplication( name="DummyApplication", - port=Port.ARP, + port=Port["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_application"), ) @@ -350,10 +350,10 @@ def install_stuff_to_sim(sim: Simulation): network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2]) # 2: Configure base ACL - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=3) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=1) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) # 3: Install server software server_1.software_manager.install(DNSServer) @@ -379,13 +379,13 @@ def install_stuff_to_sim(sim: Simulation): r = sim.network.router_nodes[0] for i, acl_rule in enumerate(r.acl.acl): if i == 1: - assert acl_rule.src_port == acl_rule.dst_port == Port.DNS + assert acl_rule.src_port == acl_rule.dst_port == Port["DNS"] elif i == 3: - assert acl_rule.src_port == acl_rule.dst_port == Port.HTTP + assert acl_rule.src_port == acl_rule.dst_port == Port["HTTP"] elif i == 22: - assert acl_rule.src_port == acl_rule.dst_port == Port.ARP + assert acl_rule.src_port == acl_rule.dst_port == Port["ARP"] elif i == 23: - assert acl_rule.protocol == IPProtocol.ICMP + assert acl_rule.protocol == IPProtocol["ICMP"] elif i == 24: ... else: diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py index 457fdb42..d35e2ebb 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -68,44 +68,44 @@ def test_firewall_acl_rules_correctly_added(dmz_config): # ICMP and ARP should be allowed internal_inbound assert firewall.internal_inbound_acl.num_rules == 2 assert firewall.internal_inbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.internal_inbound_acl.acl[22].src_port == Port.ARP - assert firewall.internal_inbound_acl.acl[22].dst_port == Port.ARP + assert firewall.internal_inbound_acl.acl[22].src_port == Port["ARP"] + assert firewall.internal_inbound_acl.acl[22].dst_port == Port["ARP"] assert firewall.internal_inbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.internal_inbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.internal_inbound_acl.acl[23].protocol == IPProtocol["ICMP"] assert firewall.internal_inbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed internal_outbound assert firewall.internal_outbound_acl.num_rules == 2 assert firewall.internal_outbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.internal_outbound_acl.acl[22].src_port == Port.ARP - assert firewall.internal_outbound_acl.acl[22].dst_port == Port.ARP + assert firewall.internal_outbound_acl.acl[22].src_port == Port["ARP"] + assert firewall.internal_outbound_acl.acl[22].dst_port == Port["ARP"] assert firewall.internal_outbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.internal_outbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.internal_outbound_acl.acl[23].protocol == IPProtocol["ICMP"] assert firewall.internal_outbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed dmz_inbound assert firewall.dmz_inbound_acl.num_rules == 2 assert firewall.dmz_inbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.dmz_inbound_acl.acl[22].src_port == Port.ARP - assert firewall.dmz_inbound_acl.acl[22].dst_port == Port.ARP + assert firewall.dmz_inbound_acl.acl[22].src_port == Port["ARP"] + assert firewall.dmz_inbound_acl.acl[22].dst_port == Port["ARP"] assert firewall.dmz_inbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.dmz_inbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.dmz_inbound_acl.acl[23].protocol == IPProtocol["ICMP"] assert firewall.dmz_inbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed dmz_outbound assert firewall.dmz_outbound_acl.num_rules == 2 assert firewall.dmz_outbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.dmz_outbound_acl.acl[22].src_port == Port.ARP - assert firewall.dmz_outbound_acl.acl[22].dst_port == Port.ARP + assert firewall.dmz_outbound_acl.acl[22].src_port == Port["ARP"] + assert firewall.dmz_outbound_acl.acl[22].dst_port == Port["ARP"] assert firewall.dmz_outbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.dmz_outbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.dmz_outbound_acl.acl[23].protocol == IPProtocol["ICMP"] assert firewall.dmz_outbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed external_inbound assert firewall.external_inbound_acl.num_rules == 1 assert firewall.external_inbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.external_inbound_acl.acl[22].src_port == Port.ARP - assert firewall.external_inbound_acl.acl[22].dst_port == Port.ARP + assert firewall.external_inbound_acl.acl[22].src_port == Port["ARP"] + assert firewall.external_inbound_acl.acl[22].dst_port == Port["ARP"] # external_inbound should have implicit action PERMIT # ICMP does not have a provided ACL Rule but implicit action should allow anything assert firewall.external_inbound_acl.implicit_action == ACLAction.PERMIT @@ -113,8 +113,8 @@ def test_firewall_acl_rules_correctly_added(dmz_config): # ICMP and ARP should be allowed external_outbound assert firewall.external_outbound_acl.num_rules == 1 assert firewall.external_outbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.external_outbound_acl.acl[22].src_port == Port.ARP - assert firewall.external_outbound_acl.acl[22].dst_port == Port.ARP + assert firewall.external_outbound_acl.acl[22].src_port == Port["ARP"] + assert firewall.external_outbound_acl.acl[22].dst_port == Port["ARP"] # external_outbound should have implicit action PERMIT # ICMP does not have a provided ACL Rule but implicit action should allow anything assert firewall.external_outbound_acl.implicit_action == ACLAction.PERMIT diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index ccde3a02..16543565 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -63,8 +63,8 @@ def test_router_acl_rules_correctly_added(dmz_config): # ICMP and ARP should be allowed assert router_1.acl.num_rules == 2 assert router_1.acl.acl[22].action == ACLAction.PERMIT - assert router_1.acl.acl[22].src_port == Port.ARP - assert router_1.acl.acl[22].dst_port == Port.ARP + assert router_1.acl.acl[22].src_port == Port["ARP"] + assert router_1.acl.acl[22].dst_port == Port["ARP"] assert router_1.acl.acl[23].action == ACLAction.PERMIT - assert router_1.acl.acl[23].protocol == IPProtocol.ICMP + assert router_1.acl.acl[23].protocol == IPProtocol["ICMP"] assert router_1.acl.implicit_action == ACLAction.DENY diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index c9b3006d..8e3d33e1 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -44,10 +44,10 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): def __init__(self, **kwargs): kwargs["name"] = "ExtendedApplication" - kwargs["protocol"] = IPProtocol.TCP + kwargs["protocol"] = IPProtocol["TCP"] # default for web is port 80 if kwargs.get("port") is None: - kwargs["port"] = Port.HTTP + kwargs["port"] = Port["HTTP"] super().__init__(**kwargs) self.run() @@ -127,7 +127,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): if self.send( payload=payload, dest_ip_address=self.domain_name_ip_address, - dest_port=parsed_url.port if parsed_url.port else Port.HTTP, + dest_port=parsed_url.port if parsed_url.port else Port["HTTP"], ): self.sys_log.info( f"{self.name}: Received HTTP {payload.request_method.name} " @@ -155,7 +155,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): self, payload: HttpRequestPacket, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = Port.HTTP, + dest_port: Optional[int] = Port["HTTP"], session_id: Optional[str] = None, **kwargs, ) -> bool: diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index 3151571b..d4af600f 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -38,8 +38,8 @@ class ExtendedService(Service, identifier='extendedservice'): def __init__(self, **kwargs): kwargs["name"] = "ExtendedService" - kwargs["port"] = Port.POSTGRES_SERVER - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["POSTGRES_SERVER"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self._create_db_file() if kwargs.get('options'): diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 806ce063..17b0ba8c 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -26,9 +26,9 @@ def game_and_agent_fixture(game_and_agent): game, agent = game_and_agent router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=4) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=5) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=6) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=4) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=5) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=6) c2_server_host = game.simulation.network.get_node_by_hostname("client_1") c2_server_host.software_manager.install(software_class=C2Server) diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 0c9ec6f0..34ee25d6 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -200,7 +200,7 @@ class TestConfigureDoSBot: game.step() assert dos_bot.target_ip_address == IPv4Address("192.168.1.99") - assert dos_bot.target_port == Port.POSTGRES_SERVER + assert dos_bot.target_port == Port["POSTGRES_SERVER"] assert dos_bot.payload == "HACC" assert not dos_bot.repeat assert dos_bot.port_scan_p_of_success == 0.875 diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index d011c1e8..857edd26 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -20,7 +20,7 @@ def game_and_agent_fixture(game_and_agent): game, agent = game_and_agent router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=4) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=4) return (game, agent) diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index f1d9d416..398c43a9 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -33,7 +33,7 @@ def test_acl_observations(simulation): server.software_manager.install(NTPServer) # add router acl rule - router.acl.add_rule(action=ACLAction.PERMIT, dst_port=Port.NTP, src_port=Port.NTP, position=1) + router.acl.add_rule(action=ACLAction.PERMIT, dst_port=Port["NTP"], src_port=Port["NTP"], position=1) acl_obs = ACLObservation( where=["network", "nodes", router.hostname, "acl", "acl"], diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 34a37f5e..68506d59 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -62,13 +62,13 @@ def test_firewall_observation(): # add a rule to the internal inbound and check that the observation is correct firewall.internal_inbound_acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="10.0.0.1", src_wildcard_mask="0.0.0.1", dst_ip_address="10.0.0.2", dst_wildcard_mask="0.0.0.1", - src_port=Port.HTTP, - dst_port=Port.HTTP, + src_port=Port["HTTP"], + dst_port=Port["HTTP"], position=5, ) diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index ef789ba7..bd8dfc4e 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -152,7 +152,7 @@ def test_config_nic_categories(simulation): def test_nic_monitored_traffic(simulation): - monitored_traffic = {"icmp": ["NONE"], "tcp": ["DNS"]} + monitored_traffic = {"icmp": ["NONE"], "tcp": [53,]} pc: Computer = simulation.network.get_node_by_hostname("client_1") pc2: Computer = simulation.network.get_node_by_hostname("client_2") diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index 48d29cfb..937bb061 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -39,13 +39,13 @@ def test_router_observation(): # Add an ACL rule to the router router.acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="10.0.0.1", src_wildcard_mask="0.0.0.1", dst_ip_address="10.0.0.2", dst_wildcard_mask="0.0.0.1", - src_port=Port.HTTP, - dst_port=Port.HTTP, + src_port=Port["HTTP"], + dst_port=Port["HTTP"], position=5, ) # Observe the state using the RouterObservation instance diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index ca5e2543..6ca4bc9e 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -15,7 +15,7 @@ def env_with_ssh() -> PrimaiteGymEnv: env = PrimaiteGymEnv(DATA_MANIPULATION_CONFIG) env.agent.flatten_obs = False router: Router = env.game.simulation.network.get_node_by_hostname("router_1") - router.acl.add_rule(ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=3) + router.acl.add_rule(ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=3) return env diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index a1005f34..c3e86263 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -608,9 +608,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.internal_outbound_acl.acl[1].action.name == "DENY" assert firewall.internal_outbound_acl.acl[1].src_ip_address == IPv4Address("192.168.0.10") assert firewall.internal_outbound_acl.acl[1].dst_ip_address is None - assert firewall.internal_outbound_acl.acl[1].dst_port == Port.DNS - assert firewall.internal_outbound_acl.acl[1].src_port == Port.ARP - assert firewall.internal_outbound_acl.acl[1].protocol == IPProtocol.ICMP + assert firewall.internal_outbound_acl.acl[1].dst_port == Port["DNS"] + assert firewall.internal_outbound_acl.acl[1].src_port == Port["ARP"] + assert firewall.internal_outbound_acl.acl[1].protocol == IPProtocol["ICMP"] env.step(4) # Remove ACL rule from Internal Outbound assert firewall.internal_outbound_acl.num_rules == 2 @@ -620,9 +620,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.dmz_inbound_acl.acl[1].action.name == "DENY" assert firewall.dmz_inbound_acl.acl[1].src_ip_address == IPv4Address("192.168.10.10") assert firewall.dmz_inbound_acl.acl[1].dst_ip_address == IPv4Address("192.168.0.10") - assert firewall.dmz_inbound_acl.acl[1].dst_port == Port.HTTP - assert firewall.dmz_inbound_acl.acl[1].src_port == Port.HTTP - assert firewall.dmz_inbound_acl.acl[1].protocol == IPProtocol.UDP + assert firewall.dmz_inbound_acl.acl[1].dst_port == Port["HTTP"] + assert firewall.dmz_inbound_acl.acl[1].src_port == Port["HTTP"] + assert firewall.dmz_inbound_acl.acl[1].protocol == IPProtocol["UDP"] env.step(6) # Remove ACL rule from DMZ Inbound assert firewall.dmz_inbound_acl.num_rules == 2 @@ -632,9 +632,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.dmz_outbound_acl.acl[2].action.name == "DENY" assert firewall.dmz_outbound_acl.acl[2].src_ip_address == IPv4Address("192.168.10.10") assert firewall.dmz_outbound_acl.acl[2].dst_ip_address == IPv4Address("192.168.0.10") - assert firewall.dmz_outbound_acl.acl[2].dst_port == Port.HTTP - assert firewall.dmz_outbound_acl.acl[2].src_port == Port.HTTP - assert firewall.dmz_outbound_acl.acl[2].protocol == IPProtocol.TCP + assert firewall.dmz_outbound_acl.acl[2].dst_port == Port["HTTP"] + assert firewall.dmz_outbound_acl.acl[2].src_port == Port["HTTP"] + assert firewall.dmz_outbound_acl.acl[2].protocol == IPProtocol["TCP"] env.step(8) # Remove ACL rule from DMZ Outbound assert firewall.dmz_outbound_acl.num_rules == 2 @@ -644,9 +644,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.external_inbound_acl.acl[10].action.name == "DENY" assert firewall.external_inbound_acl.acl[10].src_ip_address == IPv4Address("192.168.20.10") assert firewall.external_inbound_acl.acl[10].dst_ip_address == IPv4Address("192.168.10.10") - assert firewall.external_inbound_acl.acl[10].dst_port == Port.POSTGRES_SERVER - assert firewall.external_inbound_acl.acl[10].src_port == Port.POSTGRES_SERVER - assert firewall.external_inbound_acl.acl[10].protocol == IPProtocol.ICMP + assert firewall.external_inbound_acl.acl[10].dst_port == Port["POSTGRES_SERVER"] + assert firewall.external_inbound_acl.acl[10].src_port == Port["POSTGRES_SERVER"] + assert firewall.external_inbound_acl.acl[10].protocol == IPProtocol["ICMP"] env.step(10) # Remove ACL rule from External Inbound assert firewall.external_inbound_acl.num_rules == 1 diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 58783d70..d872c2b0 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -42,7 +42,7 @@ def test_WebpageUnavailablePenalty(game_and_agent): # Block the web traffic, check that failing to fetch the webpage yields a reward of -0.7 router: Router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(action=ACLAction.DENY, protocol=IPProtocol.TCP, src_port=Port.HTTP, dst_port=Port.HTTP) + router.acl.add_rule(action=ACLAction.DENY, protocol=IPProtocol["TCP"], src_port=Port["HTTP"], dst_port=Port["HTTP"]) agent.store_action(("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0})) game.step() assert agent.reward_function.current_reward == -0.7 @@ -65,7 +65,7 @@ def test_uc2_rewards(game_and_agent): db_client.run() router: Router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=2) + router.acl.add_rule(ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=2) comp = GreenAdminDatabaseUnreachablePenalty("client_1") diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py index 78d00b47..1794c4bc 100644 --- a/tests/integration_tests/network/test_airspace_config.py +++ b/tests/integration_tests/network/test_airspace_config.py @@ -13,8 +13,8 @@ def test_override_freq_max_capacity_mbps(): config_dict = yaml.safe_load(f) network = PrimaiteGame.from_config(cfg=config_dict).simulation.network - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_2_4) == 123.45 - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_5) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_2_4"]) == 123.45 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_5"]) == 0.0 pc_a = network.get_node_by_hostname("pc_a") pc_b = network.get_node_by_hostname("pc_b") @@ -32,8 +32,8 @@ def test_override_freq_max_capacity_mbps_blocked(): config_dict = yaml.safe_load(f) network = PrimaiteGame.from_config(cfg=config_dict).simulation.network - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_2_4) == 0.0 - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency.WIFI_5) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_2_4"]) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_5"]) == 0.0 pc_a = network.get_node_by_hostname("pc_a") pc_b = network.get_node_by_hostname("pc_b") diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index 80007c46..da0af89d 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -20,8 +20,8 @@ class BroadcastTestService(Service): def __init__(self, **kwargs): # Set default service properties for broadcasting kwargs["name"] = "BroadcastService" - kwargs["port"] = Port.HTTP - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["HTTP"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: @@ -33,12 +33,12 @@ class BroadcastTestService(Service): super().send( payload="unicast", dest_ip_address=ip_address, - dest_port=Port.HTTP, + dest_port=Port["HTTP"], ) def broadcast(self, ip_network: IPv4Network): # Send a broadcast payload to an entire IP network - super().send(payload="broadcast", dest_ip_address=ip_network, dest_port=Port.HTTP, ip_protocol=self.protocol) + super().send(payload="broadcast", dest_ip_address=ip_network, dest_port=Port["HTTP"], ip_protocol=self.protocol) class BroadcastTestClient(Application, identifier="BroadcastTestClient"): @@ -49,8 +49,8 @@ class BroadcastTestClient(Application, identifier="BroadcastTestClient"): def __init__(self, **kwargs): # Set default client properties kwargs["name"] = "BroadcastTestClient" - kwargs["port"] = Port.HTTP - kwargs["protocol"] = IPProtocol.TCP + kwargs["port"] = Port["HTTP"] + kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index b15ee51a..8e06ccfb 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -53,28 +53,28 @@ def dmz_external_internal_network() -> Network: ) # Allow ICMP - firewall_node.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) - firewall_node.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) - firewall_node.external_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) - firewall_node.external_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) - firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) - firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + firewall_node.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + firewall_node.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + firewall_node.external_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + firewall_node.external_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) # Allow ARP firewall_node.internal_inbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22 + action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 ) firewall_node.internal_outbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22 + action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 ) firewall_node.external_inbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22 + action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 ) firewall_node.external_outbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22 + action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 ) - firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) # external node external_node = Computer( @@ -262,8 +262,8 @@ def test_service_allowed_with_rule(dmz_external_internal_network): assert not internal_ntp_client.time - firewall.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1) - firewall.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1) + firewall.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) + firewall.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) internal_ntp_client.request_time() @@ -271,8 +271,8 @@ def test_service_allowed_with_rule(dmz_external_internal_network): assert not dmz_ntp_client.time - firewall.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1) - firewall.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=1) + firewall.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) + firewall.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) dmz_ntp_client.request_time() diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 62b58cbd..641342e2 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -73,8 +73,8 @@ def multi_hop_network() -> Network: router_1.enable_port(2) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) # Configure PC B pc_b = Computer( @@ -197,8 +197,8 @@ def test_routing_services(multi_hop_network): router_1: Router = multi_hop_network.get_node_by_hostname("router_1") # noqa router_2: Router = multi_hop_network.get_node_by_hostname("router_2") # noqa - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=21) - router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.NTP, dst_port=Port.NTP, position=21) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=21) + router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=21) assert ntp_client.time is None ntp_client.request_time() diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index 733de6f6..2f1be930 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -37,8 +37,8 @@ def wireless_wan_network(): network.connect(pc_a.network_interface[1], router_1.network_interface[2]) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) # Configure PC B pc_b = Computer( diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 9d12f2cf..d819b511 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -227,7 +227,7 @@ def test_c2_suite_acl_block(basic_network): assert c2_beacon.c2_connection_active == True # Now we add a HTTP blocking acl (Thus preventing a keep alive) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) c2_beacon.apply_timestep(2) c2_beacon.apply_timestep(3) @@ -322,8 +322,8 @@ def test_c2_suite_acl_bypass(basic_network): ################ Confirm Default Setup ######################### # Permitting all HTTP & FTP traffic - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=1) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=1) c2_beacon.apply_timestep(0) assert c2_beacon.keep_alive_inactivity == 1 @@ -337,7 +337,7 @@ def test_c2_suite_acl_bypass(basic_network): ################ Denying HTTP Traffic ######################### # Now we add a HTTP blocking acl (Thus preventing a keep alive) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) blocking_acl: AccessControlList = router.acl.acl[0] # Asserts to show the C2 Suite is unable to maintain connection: @@ -359,8 +359,8 @@ def test_c2_suite_acl_bypass(basic_network): c2_beacon.configure( c2_server_ip_address="192.168.0.2", keep_alive_frequency=2, - masquerade_port=Port.FTP, - masquerade_protocol=IPProtocol.TCP, + masquerade_port=Port["FTP"], + masquerade_protocol=IPProtocol["TCP"], ) c2_beacon.establish() @@ -407,8 +407,8 @@ def test_c2_suite_acl_bypass(basic_network): ################ Denying FTP Traffic & Enable HTTP ######################### # Blocking FTP and re-permitting HTTP: - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port.FTP, dst_port=Port.FTP, position=1) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port["FTP"], dst_port=Port["FTP"], position=1) blocking_acl: AccessControlList = router.acl.acl[1] # Asserts to show the C2 Suite is unable to maintain connection: @@ -430,8 +430,8 @@ def test_c2_suite_acl_bypass(basic_network): c2_beacon.configure( c2_server_ip_address="192.168.0.2", keep_alive_frequency=2, - masquerade_port=Port.HTTP, - masquerade_protocol=IPProtocol.TCP, + masquerade_port=Port["HTTP"], + masquerade_protocol=IPProtocol["TCP"], ) c2_beacon.establish() diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 2e87578d..9c0760b7 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -52,7 +52,7 @@ def data_manipulation_db_server_green_client(example_network) -> Network: router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=0 + action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 ) client_1: Computer = network.get_node_by_hostname("client_1") diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index 68c1fbfe..709a417f 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -26,7 +26,7 @@ def dos_bot_and_db_server(client_server) -> Tuple[DoSBot, Computer, DatabaseServ dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") dos_bot.configure( target_ip_address=IPv4Address(server.network_interface[1].ip_address), - target_port=Port.POSTGRES_SERVER, + target_port=Port["POSTGRES_SERVER"], ) # Install DB Server service on server @@ -43,7 +43,7 @@ def dos_bot_db_server_green_client(example_network) -> Network: router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=0 + action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 ) client_1: Computer = network.get_node_by_hostname("client_1") @@ -56,7 +56,7 @@ def dos_bot_db_server_green_client(example_network) -> Network: dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot") dos_bot.configure( target_ip_address=IPv4Address(server.network_interface[1].ip_address), - target_port=Port.POSTGRES_SERVER, + target_port=Port["POSTGRES_SERVER"], ) # install db server service on server diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index 97abafb5..b34e9b30 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -47,7 +47,7 @@ def ransomware_script_db_server_green_client(example_network) -> Network: router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=0 + action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 ) client_1: Computer = network.get_node_by_hostname("client_1") diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index 2b8691cc..1064ed1b 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -73,10 +73,10 @@ def test_port_scan_one_node_one_port(example_network): client_2 = network.get_node_by_hostname("client_2") actual_result = client_1_nmap.port_scan( - target_ip_address=client_2.network_interface[1].ip_address, target_port=Port.DNS, target_protocol=IPProtocol.TCP + target_ip_address=client_2.network_interface[1].ip_address, target_port=Port["DNS"], target_protocol=IPProtocol["TCP"] ) - expected_result = {IPv4Address("192.168.10.22"): {IPProtocol.TCP: [Port.DNS]}} + expected_result = {IPv4Address("192.168.10.22"): {IPProtocol["TCP"]: [Port["DNS"]]}} assert actual_result == expected_result @@ -101,14 +101,14 @@ def test_port_scan_full_subnet_all_ports_and_protocols(example_network): actual_result = client_1_nmap.port_scan( target_ip_address=IPv4Network("192.168.10.0/24"), - target_port=[Port.ARP, Port.HTTP, Port.FTP, Port.DNS, Port.NTP], + target_port=[Port["ARP"], Port["HTTP"], Port["FTP"], Port["DNS"], Port["NTP"]], ) expected_result = { - IPv4Address("192.168.10.1"): {IPProtocol.UDP: [Port.ARP]}, + IPv4Address("192.168.10.1"): {IPProtocol["UDP"]: [Port["ARP"]]}, IPv4Address("192.168.10.22"): { - IPProtocol.TCP: [Port.HTTP, Port.FTP, Port.DNS], - IPProtocol.UDP: [Port.ARP, Port.NTP], + IPProtocol["TCP"]: [Port["HTTP"], Port["FTP"], Port["DNS"]], + IPProtocol["UDP"]: [Port["ARP"], Port["NTP"]], }, } @@ -122,10 +122,10 @@ def test_network_service_recon_all_ports_and_protocols(example_network): client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa actual_result = client_1_nmap.network_service_recon( - target_ip_address=IPv4Network("192.168.10.0/24"), target_port=Port.HTTP, target_protocol=IPProtocol.TCP + target_ip_address=IPv4Network("192.168.10.0/24"), target_port=Port["HTTP"], target_protocol=IPProtocol["TCP"] ) - expected_result = {IPv4Address("192.168.10.22"): {IPProtocol.TCP: [Port.HTTP]}} + expected_result = {IPv4Address("192.168.10.22"): {IPProtocol["TCP"]: [Port["HTTP"]]}} assert sort_dict(actual_result) == sort_dict(expected_result) 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 fd502a70..5226ab4a 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -16,9 +16,9 @@ from tests import TEST_ASSETS_ROOT class _DatabaseListener(Service): name: str = "DatabaseListener" - protocol: IPProtocol = IPProtocol.TCP - port: Port = Port.NONE - listen_on_ports: Set[Port] = {Port.POSTGRES_SERVER} + protocol: str = IPProtocol["TCP"] + port: int = Port["NONE"] + listen_on_ports: Set[int] = {Port["POSTGRES_SERVER"]} payloads_received: List[Any] = Field(default_factory=list) def receive(self, payload: Any, session_id: str, **kwargs) -> bool: @@ -51,8 +51,8 @@ def test_http_listener(client_server): computer.session_manager.receive_payload_from_software_manager( payload="masquerade as Database traffic", dst_ip_address=server.network_interface[1].ip_address, - dst_port=Port.POSTGRES_SERVER, - ip_protocol=IPProtocol.TCP, + dst_port=Port["POSTGRES_SERVER"], + ip_protocol=IPProtocol["TCP"], ) assert len(server_db_listener.payloads_received) == 1 @@ -76,9 +76,9 @@ def test_set_listen_on_ports_from_config(): 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() + 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}) + assert not web_browser.listen_on_ports.difference({Port["SMB"], Port["IPP"]}) diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 5a765763..6c37360f 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -24,17 +24,17 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer, # add rules to network router router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=0 + action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 ) # Allow DNS requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=1) # Allow FTP requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=2) # Open port 80 for web server - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=3) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) # Create Computer computer: Computer = example_network.get_node_by_hostname("client_1") @@ -148,7 +148,7 @@ class TestWebBrowserHistory: assert web_browser.history[-1].response_code == 200 router = network.get_node_by_hostname("router_1") - router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) assert not web_browser.get_webpage() assert len(web_browser.history) == 3 # with current NIC behaviour, even if you block communication, you won't get SERVER_UNREACHABLE because @@ -166,7 +166,7 @@ class TestWebBrowserHistory: web_browser.get_webpage() router = network.get_node_by_hostname("router_1") - router.acl.add_rule(action=ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) web_browser.get_webpage() state = computer.describe_state() diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index 95634cf1..ff73e621 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -171,7 +171,7 @@ class TestDataManipulationGreenRequests: assert client_1_browser_execute.status == "success" assert client_2_browser_execute.status == "success" - router.acl.add_rule(ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=3) + router.acl.add_rule(ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) assert client_1_browser_execute.status == "failure" @@ -182,7 +182,7 @@ class TestDataManipulationGreenRequests: assert client_1_db_client_execute.status == "success" assert client_2_db_client_execute.status == "success" - router.acl.add_rule(ACLAction.DENY, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER) + router.acl.add_rule(ACLAction.DENY, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"]) client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) assert client_1_db_client_execute.status == "failure" diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 9bc1abfd..4c471faa 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -28,20 +28,20 @@ def router_with_acl_rules(): # Add rules here as needed acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="192.168.1.1", - src_port=Port.HTTPS, + src_port=Port["HTTPS"], dst_ip_address="192.168.1.2", - dst_port=Port.HTTP, + dst_port=Port["HTTP"], position=1, ) acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="192.168.1.3", - src_port=Port(8080), + src_port=8080, dst_ip_address="192.168.1.4", - dst_port=Port(80), + dst_port=80, position=2, ) return router @@ -65,21 +65,21 @@ def router_with_wildcard_acl(): # Rule to permit traffic from a specific source IP and port to a specific destination IP and port acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="192.168.1.1", - src_port=Port(8080), + src_port=8080, dst_ip_address="10.1.1.2", - dst_port=Port(80), + dst_port=80, position=1, ) # Rule to deny traffic from an IP range to a specific destination IP and port acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], src_ip_address="192.168.1.0", src_wildcard_mask="0.0.0.255", dst_ip_address="10.1.1.3", - dst_port=Port(443), + dst_port=443, position=2, ) # Rule to permit any traffic to a range of destination IPs @@ -109,11 +109,11 @@ def test_add_rule(router_with_acl_rules): acl = router_with_acl_rules.acl assert acl.acl[1].action == ACLAction.PERMIT - assert acl.acl[1].protocol == IPProtocol.TCP + assert acl.acl[1].protocol == IPProtocol["TCP"] assert acl.acl[1].src_ip_address == IPv4Address("192.168.1.1") - assert acl.acl[1].src_port == Port.HTTPS + assert acl.acl[1].src_port == Port["HTTPS"] assert acl.acl[1].dst_ip_address == IPv4Address("192.168.1.2") - assert acl.acl[1].dst_port == Port.HTTP + assert acl.acl[1].dst_port == Port["HTTP"] def test_remove_rule(router_with_acl_rules): @@ -136,8 +136,8 @@ def test_traffic_permitted_by_specific_rule(router_with_acl_rules): acl = router_with_acl_rules.acl permitted_frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="192.168.1.2", protocol=IPProtocol.TCP), - tcp=TCPHeader(src_port=Port.HTTPS, dst_port=Port.HTTP), + ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="192.168.1.2", protocol=IPProtocol["TCP"]), + tcp=TCPHeader(src_port=Port["HTTPS"], dst_port=Port["HTTP"]), ) is_permitted, _ = acl.is_permitted(permitted_frame) assert is_permitted @@ -153,8 +153,8 @@ def test_traffic_denied_by_specific_rule(router_with_acl_rules): acl = router_with_acl_rules.acl not_permitted_frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.3", dst_ip_address="192.168.1.4", protocol=IPProtocol.TCP), - tcp=TCPHeader(src_port=Port(8080), dst_port=Port(80)), + ip=IPPacket(src_ip_address="192.168.1.3", dst_ip_address="192.168.1.4", protocol=IPProtocol["TCP"]), + tcp=TCPHeader(src_port=8080, dst_port=80), ) is_permitted, _ = acl.is_permitted(not_permitted_frame) assert not is_permitted @@ -173,8 +173,8 @@ def test_default_rule(router_with_acl_rules): acl = router_with_acl_rules.acl not_permitted_frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.5", dst_ip_address="192.168.1.12", protocol=IPProtocol.UDP), - udp=UDPHeader(src_port=Port.HTTPS, dst_port=Port.HTTP), + ip=IPPacket(src_ip_address="192.168.1.5", dst_ip_address="192.168.1.12", protocol=IPProtocol["UDP"]), + udp=UDPHeader(src_port=Port["HTTPS"], dst_port=Port["HTTP"]), ) is_permitted, rule = acl.is_permitted(not_permitted_frame) assert not is_permitted @@ -189,8 +189,8 @@ def test_direct_ip_match_with_acl(router_with_wildcard_acl): acl = router_with_wildcard_acl.acl frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="10.1.1.2", protocol=IPProtocol.TCP), - tcp=TCPHeader(src_port=Port(8080), dst_port=Port(80)), + ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="10.1.1.2", protocol=IPProtocol["TCP"]), + tcp=TCPHeader(src_port=8080, dst_port=80), ) assert acl.is_permitted(frame)[0], "Direct IP match should be permitted." @@ -204,8 +204,8 @@ def test_ip_range_match_denied_with_acl(router_with_wildcard_acl): acl = router_with_wildcard_acl.acl frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.100", dst_ip_address="10.1.1.3", protocol=IPProtocol.TCP), - tcp=TCPHeader(src_port=Port(8080), dst_port=Port(443)), + ip=IPPacket(src_ip_address="192.168.1.100", dst_ip_address="10.1.1.3", protocol=IPProtocol["TCP"]), + tcp=TCPHeader(src_port=8080, dst_port=443), ) assert not acl.is_permitted(frame)[0], "IP range match with wildcard mask should be denied." @@ -219,8 +219,8 @@ def test_traffic_permitted_to_destination_range_with_acl(router_with_wildcard_ac acl = router_with_wildcard_acl.acl frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=IPProtocol.UDP), - udp=UDPHeader(src_port=Port(1433), dst_port=Port(1433)), + ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=IPProtocol["UDP"]), + udp=UDPHeader(src_port=1433, dst_port=1433), ) assert acl.is_permitted(frame)[0], "Traffic to destination IP range should be permitted." @@ -253,23 +253,23 @@ def test_ip_traffic_from_specific_subnet(): permitted_frame_1 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=IPProtocol.TCP), - tcp=TCPHeader(src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER), + ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=IPProtocol["TCP"]), + tcp=TCPHeader(src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"]), ) assert acl.is_permitted(permitted_frame_1)[0] permitted_frame_2 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.10", dst_ip_address="85.199.214.101", protocol=IPProtocol.UDP), - udp=UDPHeader(src_port=Port.NTP, dst_port=Port.NTP), + ip=IPPacket(src_ip_address="192.168.1.10", dst_ip_address="85.199.214.101", protocol=IPProtocol["UDP"]), + udp=UDPHeader(src_port=Port["NTP"], dst_port=Port["NTP"]), ) assert acl.is_permitted(permitted_frame_2)[0] permitted_frame_3 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.200", dst_ip_address="192.168.1.1", protocol=IPProtocol.ICMP), + ip=IPPacket(src_ip_address="192.168.1.200", dst_ip_address="192.168.1.1", protocol=IPProtocol["ICMP"]), icmp=ICMPPacket(identifier=1), ) @@ -277,16 +277,16 @@ def test_ip_traffic_from_specific_subnet(): not_permitted_frame_1 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.0.50", dst_ip_address="10.2.200.200", protocol=IPProtocol.TCP), - tcp=TCPHeader(src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER), + ip=IPPacket(src_ip_address="192.168.0.50", dst_ip_address="10.2.200.200", protocol=IPProtocol["TCP"]), + tcp=TCPHeader(src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"]), ) assert not acl.is_permitted(not_permitted_frame_1)[0] not_permitted_frame_2 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.2.10", dst_ip_address="85.199.214.101", protocol=IPProtocol.UDP), - udp=UDPHeader(src_port=Port.NTP, dst_port=Port.NTP), + ip=IPPacket(src_ip_address="192.168.2.10", dst_ip_address="85.199.214.101", protocol=IPProtocol["UDP"]), + udp=UDPHeader(src_port=Port["NTP"], dst_port=Port["NTP"]), ) assert not acl.is_permitted(not_permitted_frame_2)[0] diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index d4e38ded..3551ce38 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -67,12 +67,12 @@ def test_wireless_router_from_config(): r0 = rt.acl.acl[0] assert r0.action == ACLAction.PERMIT - assert r0.src_port == r0.dst_port == Port.POSTGRES_SERVER + assert r0.src_port == r0.dst_port == Port["POSTGRES_SERVER"] assert r0.src_ip_address == r0.dst_ip_address == r0.dst_wildcard_mask == r0.src_wildcard_mask == r0.protocol == None r1 = rt.acl.acl[1] assert r1.action == ACLAction.PERMIT - assert r1.protocol == IPProtocol.ICMP + assert r1.protocol == IPProtocol["ICMP"] assert ( r1.src_ip_address == r1.dst_ip_address diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py index 92618baa..9fd39dfc 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py @@ -20,7 +20,7 @@ def test_frame_minimal_instantiation(): ) # Check network layer default values - assert frame.ip.protocol == IPProtocol.TCP + assert frame.ip.protocol == IPProtocol["TCP"] assert frame.ip.ttl == 64 assert frame.ip.precedence == Precedence.ROUTINE @@ -40,7 +40,7 @@ def test_frame_creation_fails_tcp_without_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol.TCP), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["TCP"]), ) @@ -49,7 +49,7 @@ def test_frame_creation_fails_udp_without_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol.UDP), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["UDP"]), ) @@ -58,7 +58,7 @@ def test_frame_creation_fails_tcp_with_udp_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol.TCP), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["TCP"]), udp=UDPHeader(src_port=8080, dst_port=80), ) @@ -68,7 +68,7 @@ def test_frame_creation_fails_udp_with_tcp_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol.UDP), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["UDP"]), udp=TCPHeader(src_port=8080, dst_port=80), ) @@ -77,7 +77,7 @@ def test_icmp_frame_creation(): """Tests Frame creation for ICMP.""" frame = Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol.ICMP), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["ICMP"]), icmp=ICMPPacket(), ) assert frame @@ -88,5 +88,5 @@ def test_icmp_frame_creation_fails_without_icmp_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol.ICMP), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["ICMP"]), ) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 885a3cb6..6e53aebc 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -129,19 +129,19 @@ def test_c2_handle_switching_port(basic_c2_network): # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. assert c2_beacon.c2_config.keep_alive_frequency is 2 - assert c2_beacon.c2_config.masquerade_port is Port.HTTP - assert c2_beacon.c2_config.masquerade_protocol is IPProtocol.TCP + assert c2_beacon.c2_config.masquerade_port is Port["HTTP"] + assert c2_beacon.c2_config.masquerade_protocol is IPProtocol["TCP"] assert c2_server.c2_config.keep_alive_frequency is 2 - assert c2_server.c2_config.masquerade_port is Port.HTTP - assert c2_server.c2_config.masquerade_protocol is IPProtocol.TCP + assert c2_server.c2_config.masquerade_port is Port["HTTP"] + assert c2_server.c2_config.masquerade_protocol is IPProtocol["TCP"] # Configuring the C2 Beacon. c2_beacon.configure( c2_server_ip_address="192.168.0.1", keep_alive_frequency=2, - masquerade_port=Port.FTP, - masquerade_protocol=IPProtocol.TCP, + masquerade_port=Port["FTP"], + masquerade_protocol=IPProtocol["TCP"], ) # Asserting that the c2 applications have established a c2 connection @@ -150,11 +150,11 @@ def test_c2_handle_switching_port(basic_c2_network): # Assert to confirm that both the C2 server and the C2 beacon # Have reconfigured their C2 settings. - assert c2_beacon.c2_config.masquerade_port is Port.FTP - assert c2_beacon.c2_config.masquerade_protocol is IPProtocol.TCP + assert c2_beacon.c2_config.masquerade_port is Port["FTP"] + assert c2_beacon.c2_config.masquerade_protocol is IPProtocol["TCP"] - assert c2_server.c2_config.masquerade_port is Port.FTP - assert c2_server.c2_config.masquerade_protocol is IPProtocol.TCP + assert c2_server.c2_config.masquerade_port is Port["FTP"] + assert c2_server.c2_config.masquerade_protocol is IPProtocol["TCP"] def test_c2_handle_switching_frequency(basic_c2_network): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index 0811d2a0..229f98fe 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -27,8 +27,8 @@ def test_create_dm_bot(dm_client): data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software.get("DataManipulationBot") assert data_manipulation_bot.name == "DataManipulationBot" - assert data_manipulation_bot.port == Port.NONE - assert data_manipulation_bot.protocol == IPProtocol.NONE + assert data_manipulation_bot.port == Port["NONE"] + assert data_manipulation_bot.protocol == IPProtocol["NONE"] assert data_manipulation_bot.payload == "DELETE" diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index ce98d164..c274c18e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -39,8 +39,8 @@ def test_create_web_client(): # Web Browser should be pre-installed in computer web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") assert web_browser.name is "WebBrowser" - assert web_browser.port is Port.HTTP - assert web_browser.protocol is IPProtocol.TCP + assert web_browser.port is Port["HTTP"] + assert web_browser.protocol is IPProtocol["TCP"] def test_receive_invalid_payload(web_browser): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index e9ce4884..1a51708d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -28,8 +28,8 @@ def test_create_dns_client(dns_client): assert dns_client is not None dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") assert dns_client_service.name is "DNSClient" - assert dns_client_service.port is Port.DNS - assert dns_client_service.protocol is IPProtocol.TCP + assert dns_client_service.port is Port["DNS"] + assert dns_client_service.protocol is IPProtocol["TCP"] def test_dns_client_add_domain_to_cache_when_not_running(dns_client): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 4658fe76..8cdb1b84 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -32,8 +32,8 @@ def test_create_dns_server(dns_server): assert dns_server is not None dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") assert dns_server_service.name is "DNSServer" - assert dns_server_service.port is Port.DNS - assert dns_server_service.protocol is IPProtocol.TCP + assert dns_server_service.port is Port["DNS"] + assert dns_server_service.protocol is IPProtocol["TCP"] def test_dns_server_domain_name_registration(dns_server): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 3ce4d8ee..3c1afb28 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -31,8 +31,8 @@ def test_create_ftp_client(ftp_client): assert ftp_client is not None ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") assert ftp_client_service.name is "FTPClient" - assert ftp_client_service.port is Port.FTP - assert ftp_client_service.protocol is IPProtocol.TCP + assert ftp_client_service.port is Port["FTP"] + assert ftp_client_service.protocol is IPProtocol["TCP"] def test_ftp_client_store_file(ftp_client): @@ -61,7 +61,7 @@ def test_ftp_should_not_process_commands_if_service_not_running(ftp_client): """Method _process_ftp_command should return false if service is not running.""" payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, - ftp_command_args=Port.FTP, + ftp_command_args=Port["FTP"], status_code=FTPStatusCode.OK, ) @@ -102,7 +102,7 @@ def test_offline_ftp_client_receives_request(ftp_client): payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, - ftp_command_args=Port.FTP, + ftp_command_args=Port["FTP"], status_code=FTPStatusCode.OK, ) @@ -119,7 +119,7 @@ def test_receive_should_ignore_payload_with_none_status_code(ftp_client): """Receive should ignore payload with no set status code to prevent infinite send/receive loops.""" payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, - ftp_command_args=Port.FTP, + ftp_command_args=Port["FTP"], status_code=None, ) ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index a1c2ba59..aa13ec5e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -30,8 +30,8 @@ def test_create_ftp_server(ftp_server): assert ftp_server is not None ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") assert ftp_server_service.name is "FTPServer" - assert ftp_server_service.port is Port.FTP - assert ftp_server_service.protocol is IPProtocol.TCP + assert ftp_server_service.port is Port["FTP"] + assert ftp_server_service.protocol is IPProtocol["TCP"] def test_ftp_server_store_file(ftp_server): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 41858b90..21ed839b 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -77,11 +77,11 @@ def wireless_wan_network(): network.connect(pc_a.network_interface[1], router_1.network_interface[2]) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) # add ACL rule to allow SSH traffic - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=21) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=21) # Configure PC B pc_b = Computer( @@ -329,7 +329,7 @@ def test_SSH_across_network(wireless_wan_network): terminal_a: Terminal = pc_a.software_manager.software.get("Terminal") terminal_b: Terminal = pc_b.software_manager.software.get("Terminal") - router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.SSH, dst_port=Port.SSH, position=21) + router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=21) assert len(terminal_a._connections) == 0 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 9af176be..c1df3857 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -33,8 +33,8 @@ def test_create_web_server(web_server): assert web_server is not None web_server_service: WebServer = web_server.software_manager.software.get("WebServer") assert web_server_service.name is "WebServer" - assert web_server_service.port is Port.HTTP - assert web_server_service.protocol is IPProtocol.TCP + assert web_server_service.port is Port["HTTP"] + assert web_server_service.protocol is IPProtocol["TCP"] def test_handling_get_request_not_found_path(web_server): diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 4cf83370..b7a663af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -19,10 +19,10 @@ class TestSoftware(Service): def software(file_system): return TestSoftware( name="TestSoftware", - port=Port.ARP, + port=Port["ARP"], file_system=file_system, sys_log=SysLog(hostname="test_service"), - protocol=IPProtocol.TCP, + protocol=IPProtocol["TCP"], ) diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index a8fb0a3a..4e40bbd8 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -11,7 +11,7 @@ def test_simple_conversion(): The original dictionary contains one level of nested dictionary with enums as keys. The expected output should have string values of enums as keys. """ - original_dict = {IPProtocol.UDP: {Port.ARP: {"inbound": 0, "outbound": 1016.0}}} + original_dict = {IPProtocol["UDP"]: {Port["ARP"]: {"inbound": 0, "outbound": 1016.0}}} expected_dict = {"udp": {219: {"inbound": 0, "outbound": 1016.0}}} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict @@ -36,8 +36,8 @@ def test_mixed_keys(): The expected output should have string values of enums and original string keys. """ original_dict = { - IPProtocol.TCP: {"port": {"inbound": 0, "outbound": 1016.0}}, - "protocol": {Port.HTTP: {"inbound": 10, "outbound": 2020.0}}, + IPProtocol["TCP"]: {"port": {"inbound": 0, "outbound": 1016.0}}, + "protocol": {Port["HTTP"]: {"inbound": 10, "outbound": 2020.0}}, } expected_dict = { "tcp": {"port": {"inbound": 0, "outbound": 1016.0}}, @@ -66,7 +66,7 @@ def test_nested_dicts(): The expected output should have string values of enums as keys at all levels. """ original_dict = { - IPProtocol.UDP: {Port.ARP: {"inbound": 0, "outbound": 1016.0, "details": {IPProtocol.TCP: {"latency": "low"}}}} + IPProtocol["UDP"]: {Port["ARP"]: {"inbound": 0, "outbound": 1016.0, "details": {IPProtocol["TCP"]: {"latency": "low"}}}} } expected_dict = {"udp": {219: {"inbound": 0, "outbound": 1016.0, "details": {"tcp": {"latency": "low"}}}}} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict @@ -79,6 +79,6 @@ def test_non_dict_values(): The original dictionary contains lists and tuples as values. The expected output should preserve these non-dictionary values while converting enum keys to string values. """ - original_dict = {IPProtocol.UDP: [Port.ARP, Port.HTTP], "protocols": (IPProtocol.TCP, IPProtocol.UDP)} - expected_dict = {"udp": [Port.ARP, Port.HTTP], "protocols": (IPProtocol.TCP, IPProtocol.UDP)} + original_dict = {IPProtocol["UDP"]: [Port["ARP"], Port["HTTP"]], "protocols": (IPProtocol["TCP"], IPProtocol["UDP"])} + expected_dict = {"udp": [Port["ARP"], Port["HTTP"]], "protocols": (IPProtocol["TCP"], IPProtocol["UDP"])} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict From 08f1cf1fbd67f5fa8158876915f9ea940def0c7c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 19 Sep 2024 15:06:29 +0100 Subject: [PATCH 003/224] Fix airspace and remaining port problems from refactor --- .../network/nodes/wireless_router.rst | 6 +- src/primaite/config/load.py | 5 +- .../agent/observations/acl_observation.py | 4 +- .../agent/observations/host_observations.py | 10 +- .../agent/observations/nic_observations.py | 17 +-- .../agent/observations/node_observations.py | 10 +- src/primaite/game/game.py | 26 ++--- src/primaite/simulator/network/airspace.py | 101 +++++++++++------- src/primaite/simulator/network/container.py | 8 +- .../network/hardware/nodes/host/host_node.py | 4 +- .../hardware/nodes/network/network_node.py | 4 +- .../network/hardware/nodes/network/router.py | 11 +- .../hardware/nodes/network/wireless_router.py | 14 +-- .../network/transmission/network_layer.py | 10 +- .../network/transmission/transport_layer.py | 61 ++++++----- .../system/applications/application.py | 4 +- .../red_applications/c2/c2_beacon.py | 1 - .../simulator/system/core/session_manager.py | 4 +- .../system/services/ftp/ftp_service.py | 1 - .../simulator/system/services/service.py | 4 +- src/primaite/simulator/system/software.py | 1 - .../extensions/nodes/giga_switch.py | 3 +- .../extensions/nodes/super_computer.py | 4 +- .../extensions/services/extended_service.py | 12 ++- .../extensions/test_extendable_config.py | 18 ++-- .../observations/test_acl_observations.py | 4 +- .../observations/test_firewall_observation.py | 4 +- .../observations/test_nic_observations.py | 7 +- .../observations/test_router_observation.py | 4 +- .../game_layer/test_rewards.py | 4 +- .../network/test_airspace_config.py | 9 +- .../network/test_firewall.py | 16 ++- tests/integration_tests/system/test_nmap.py | 4 +- .../_utils/test_dict_enum_keys_conversion.py | 9 +- 34 files changed, 227 insertions(+), 177 deletions(-) diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index bd361afa..436852ea 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -49,7 +49,7 @@ additional steps to configure wireless settings: wireless_router.configure_wireless_access_point( port=1, ip_address="192.168.2.1", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency["WIFI_2_4"], + frequency="WIFI_2_4", ) @@ -130,13 +130,13 @@ ICMP traffic, ensuring basic network connectivity and ping functionality. port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency["WIFI_2_4"], + frequency="WIFI_2_4", ) router_2.configure_wireless_access_point( port=1, ip_address="192.168.1.2", subnet_mask="255.255.255.0", - frequency=AirSpaceFrequency["WIFI_2_4"], + frequency="WIFI_2_4", ) # Configure routes for inter-router communication diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index b00c26f6..39040d76 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -60,9 +60,10 @@ def data_manipulation_marl_config_path() -> Path: raise FileNotFoundError(msg) return path + def get_extended_config_path() -> Path: """ - Get the path to an 'extended' example config that contains nodes using the extension framework + Get the path to an 'extended' example config that contains nodes using the extension framework. :return: Path to the extended example config :rtype: Path @@ -72,4 +73,4 @@ def get_extended_config_path() -> Path: msg = f"Example config does not exist: {path}. Have you run `primaite setup`?" _LOGGER.error(msg) raise FileNotFoundError(msg) - return path \ No newline at end of file + return path diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index abb6f1f8..41af5a8f 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -10,8 +10,6 @@ from gymnasium.core import ObsType from primaite import getLogger from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port _LOGGER = getLogger(__name__) @@ -63,7 +61,7 @@ class ACLObservation(AbstractObservation, identifier="ACL"): self.ip_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(ip_list)} self.wildcard_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(wildcard_list)} self.port_to_id: Dict[int, int] = {p: i + 2 for i, p in enumerate(port_list)} - self.protocol_to_id: Dict[str, int] = {IPProtocol[p]: i + 2 for i, p in enumerate(protocol_list)} + self.protocol_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(protocol_list)} self.default_observation: Dict = { i + 1: { diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 05b25952..30ccd195 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -58,8 +58,14 @@ class HostObservation(AbstractObservation, identifier="HOST"): include_users: Optional[bool] = True """If True, report user session information.""" - @field_validator('monitored_traffic', mode='before') - def traffic_lookup(cls, val:Optional[Dict]) -> Optional[Dict]: + @field_validator("monitored_traffic", mode="before") + def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: + """ + Convert monitored_traffic by lookup against Port and Protocol dicts. + + This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. + This method will be removed in PrimAITE >= 4.0 + """ if val is None: return val new_val = {} diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 200187f5..296ce04c 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -26,8 +26,14 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): monitored_traffic: Optional[Dict] = None """A dict containing which traffic types are to be included in the observation.""" - @field_validator('monitored_traffic', mode='before') - def traffic_lookup(cls, val:Optional[Dict]) -> Optional[Dict]: + @field_validator("monitored_traffic", mode="before") + def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: + """ + Convert monitored_traffic by lookup against Port and Protocol dicts. + + This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. + This method will be removed in PrimAITE >= 4.0 + """ if val is None: return val new_val = {} @@ -41,7 +47,6 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): new_val[proto].append(port) return new_val - def __init__(self, where: WhereType, include_nmne: bool, monitored_traffic: Optional[Dict] = None) -> None: """ Initialise a network interface observation instance. @@ -76,7 +81,7 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): def _default_monitored_traffic_observation(self, monitored_traffic_config: Dict) -> Dict: default_traffic_obs = {"TRAFFIC": {}} - for protocol in monitored_traffic_config: + for protocol in self.monitored_traffic: protocol = str(protocol).lower() default_traffic_obs["TRAFFIC"][protocol] = {} @@ -84,8 +89,8 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): default_traffic_obs["TRAFFIC"]["icmp"] = {"inbound": 0, "outbound": 0} else: default_traffic_obs["TRAFFIC"][protocol] = {} - for port in monitored_traffic_config[protocol]: - default_traffic_obs["TRAFFIC"][protocol] = {"inbound": 0, "outbound": 0} + for port in self.monitored_traffic[protocol]: + default_traffic_obs["TRAFFIC"][protocol][port] = {"inbound": 0, "outbound": 0} return default_traffic_obs diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 3e51c3b3..054ffcdb 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -63,8 +63,14 @@ class NodesObservation(AbstractObservation, identifier="NODES"): num_rules: Optional[int] = None """Number of rules ACL rules to show.""" - @field_validator('monitored_traffic', mode='before') - def traffic_lookup(cls, val:Optional[Dict]) -> Optional[Dict]: + @field_validator("monitored_traffic", mode="before") + def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: + """ + Convert monitored_traffic by lookup against Port and Protocol dicts. + + This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. + This method will be removed in PrimAITE >= 4.0 + """ if val is None: return val new_val = {} diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index e8329c63..8e0abb1e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -17,10 +17,9 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT -from primaite.simulator.network.airspace import AirSpaceFrequency from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.host.host_node import NIC, HostNode +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode @@ -89,8 +88,8 @@ class PrimaiteGameOptions(BaseModel): thresholds: Optional[Dict] = {} """A dict containing the thresholds used for determining what is acceptable during observations.""" - @field_validator('ports', mode='before') - def ports_str2int(cls, vals:Union[List[str],List[int]]) -> List[int]: + @field_validator("ports", mode="before") + def ports_str2int(cls, vals: Union[List[str], List[int]]) -> List[int]: """ Convert named port strings to port integer values. Integer ports remain unaffected. @@ -102,8 +101,8 @@ class PrimaiteGameOptions(BaseModel): vals[i] = Port[port_val] return vals - @field_validator('protocols', mode='before') - def protocols_str2int(cls, vals:List[str]) -> List[str]: + @field_validator("protocols", mode="before") + def protocols_str2int(cls, vals: List[str]) -> List[str]: """ Convert old-style named protocols to their proper values. @@ -116,7 +115,6 @@ class PrimaiteGameOptions(BaseModel): return vals - class PrimaiteGame: """ Primaite game encapsulates the simulation and agents which interact with it. @@ -294,10 +292,7 @@ class PrimaiteGame: network_config = simulation_config.get("network", {}) airspace_cfg = network_config.get("airspace", {}) frequency_max_capacity_mbps_cfg = airspace_cfg.get("frequency_max_capacity_mbps", {}) - - frequency_max_capacity_mbps_cfg = {AirSpaceFrequency[k]: v for k, v in frequency_max_capacity_mbps_cfg.items()} - - net.airspace.frequency_max_capacity_mbps_ = frequency_max_capacity_mbps_cfg + net.airspace.set_frequency_max_capacity_mbps(frequency_max_capacity_mbps_cfg) nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) @@ -318,11 +313,10 @@ class PrimaiteGame: dns_server=node_cfg.get("dns_server", None), operating_state=NodeOperatingState.ON if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()]) - elif n_type in NetworkNode._registry: - new_node = NetworkNode._registry[n_type]( - **node_cfg + else NodeOperatingState[p.upper()], ) + elif n_type in NetworkNode._registry: + new_node = NetworkNode._registry[n_type](**node_cfg) # Default PrimAITE nodes elif n_type == "computer": new_node = Computer( @@ -502,7 +496,7 @@ class PrimaiteGame: opt = application_cfg["options"] new_application.configure( target_ip_address=IPv4Address(opt.get("target_ip_address")), - target_port = Port[opt.get("target_port", "POSTGRES_SERVER")], + target_port=Port[opt.get("target_port", "POSTGRES_SERVER")], payload=opt.get("payload"), repeat=bool(opt.get("repeat")), port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")), diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 29326df8..65dceeb1 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -1,14 +1,12 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations +import copy from abc import ABC, abstractmethod -from enum import Enum -from typing import Any, ClassVar, Dict, List, Type -from pydantic._internal._generics import PydanticGenericMetadata -from typing_extensions import Unpack +from typing import Any, Dict, List from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, Field, validate_call from primaite import getLogger from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface @@ -42,29 +40,31 @@ def format_hertz(hertz: float, format_terahertz: bool = False, decimals: int = 3 else: # Hertz return format_str.format(hertz) + " Hz" -AirSpaceFrequencyRegistry: Dict[str,Dict] = { - "WIFI_2_4" : {'frequency': 2.4e9, 'data_rate_bps':100_000_000.0}, - "WIFI_5" : {'frequency': 5e9, 'data_rate_bps':500_000_000.0}, + +_default_frequency_set: Dict[str, Dict] = { + "WIFI_2_4": {"frequency": 2.4e9, "data_rate_bps": 100_000_000.0}, + "WIFI_5": {"frequency": 5e9, "data_rate_bps": 500_000_000.0}, } +"""Frequency configuration that is automatically used for any new airspace.""" -def register_frequency(freq_name: str, freq_hz: int, data_rate_bps: int) -> None: - if freq_name in AirSpaceFrequencyRegistry: - raise RuntimeError(f"Cannot register new frequency {freq_name} because it's already registered.") - AirSpaceFrequencyRegistry.update({freq_name:{'frequency': freq_hz, 'data_rate_bps':data_rate_bps}}) -def maximum_data_rate_mbps(frequency_name:str) -> float: +def register_default_frequency(freq_name: str, freq_hz: float, data_rate_bps: float): + """Add to the default frequency configuration. This is intended as a plugin hook. + + If your plugin makes use of bespoke frequencies for wireless communication, you should make a call to this method + whereever you define components that rely on the bespoke frequencies. That way, as soon as your components are + imported, this function automatically updates the default frequency set. + + This should also be run before instances of AirSpace are created. + + :param freq_name: The frequency name. If this clashes with an existing frequency name, it will be overwritten. + :type freq_name: str + :param freq_hz: The frequency itself, measured in Hertz. + :type freq_hz: float + :param data_rate_bps: The transmission capacity over this frequency, in bits per second. + :type data_rate_bps: float """ - Retrieves the maximum data transmission rate in megabits per second (Mbps). - - This is derived by converting the maximum data rate from bits per second, as defined - in `maximum_data_rate_bps`, to megabits per second. - - :return: The maximum data rate in megabits per second. - """ - return AirSpaceFrequencyRegistry[frequency_name]['data_rate_bps'] - return data_rate / 1_000_000.0 - - + _default_frequency_set.update({freq_name: {"frequency": freq_hz, "data_rate_bps": data_rate_bps}}) class AirSpace(BaseModel): @@ -77,27 +77,21 @@ class AirSpace(BaseModel): """ wireless_interfaces: Dict[str, WirelessNetworkInterface] = Field(default_factory=lambda: {}) - wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field( - default_factory=lambda: {} - ) + wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field(default_factory=lambda: {}) bandwidth_load: Dict[int, float] = Field(default_factory=lambda: {}) - frequency_max_capacity_mbps_: Dict[int, float] = Field(default_factory=lambda: {}) + frequencies: Dict[str, Dict] = Field(default_factory=lambda: copy.deepcopy(_default_frequency_set)) - def get_frequency_max_capacity_mbps(self, frequency: str) -> float: + @validate_call + def get_frequency_max_capacity_mbps(self, freq_name: str) -> float: """ Retrieves the maximum data transmission capacity for a specified frequency. - This method checks a dictionary holding custom maximum capacities. If the frequency is found, it returns the - custom set maximum capacity. If the frequency is not found in the dictionary, it defaults to the standard - maximum data rate associated with that frequency. - - :param frequency: The frequency for which the maximum capacity is queried. - + :param freq_name: The frequency for which the maximum capacity is queried. :return: The maximum capacity in Mbps for the specified frequency. """ - if frequency in self.frequency_max_capacity_mbps_: - return self.frequency_max_capacity_mbps_[frequency] - return maximum_data_rate_mbps(frequency) + if freq_name in self.frequencies: + return self.frequencies[freq_name]["data_rate_bps"] / (1024.0 * 1024.0) + return 0.0 def set_frequency_max_capacity_mbps(self, cfg: Dict[int, float]): """ @@ -105,10 +99,29 @@ class AirSpace(BaseModel): :param cfg: A dictionary mapping frequencies to their new maximum capacities in Mbps. """ - self.frequency_max_capacity_mbps_ = cfg for freq, mbps in cfg.items(): + self.frequencies[freq]["data_rate_bps"] = mbps * 1024 * 1024 print(f"Overriding {freq} max capacity as {mbps:.3f} mbps") + def register_frequency(self, freq_name: str, freq_hz: float, data_rate_bps: float) -> None: + """ + Define a new frequency for this airspace. + + :param freq_name: The frequency name. If this clashes with an existing frequency name, it will be overwritten. + :type freq_name: str + :param freq_hz: The frequency itself, measured in Hertz. + :type freq_hz: float + :param data_rate_bps: The transmission capacity over this frequency, in bits per second. + :type data_rate_bps: float + """ + if freq_name in self.frequencies: + _LOGGER.info( + f"Overwriting Air space frequency {freq_name}. " + f"Previous data rate: {self.frequencies[freq_name]['data_rate_bps']}. " + f"Current data rate: {data_rate_bps}." + ) + self.frequencies.update({freq_name: {"frequency": freq_hz, "data_rate_bps": data_rate_bps}}) + def show_bandwidth_load(self, markdown: bool = False): """ Prints a table of the current bandwidth load for each frequency on the airspace. @@ -130,7 +143,13 @@ class AirSpace(BaseModel): load_percent = load / maximum_capacity if maximum_capacity > 0 else 0.0 if load_percent > 1.0: load_percent = 1.0 - table.add_row([format_hertz(frequency), f"{load_percent:.0%}", f"{maximum_capacity:.3f}"]) + table.add_row( + [ + format_hertz(self.frequencies[frequency]["frequency"]), + f"{load_percent:.0%}", + f"{maximum_capacity:.3f}", + ] + ) print(table) def show_wireless_interfaces(self, markdown: bool = False): @@ -162,7 +181,7 @@ class AirSpace(BaseModel): interface.mac_address, interface.ip_address if hasattr(interface, "ip_address") else None, interface.subnet_mask if hasattr(interface, "subnet_mask") else None, - format_hertz(interface.frequency), + format_hertz(self.frequencies[interface.frequency]["frequency"]), f"{interface.speed:.3f}", status, ] diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 39fbe783..6e019f32 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -130,15 +130,15 @@ class Network(SimComponent): def firewall_nodes(self) -> List[Node]: """The Firewalls in the Network.""" return [node for node in self.nodes.values() if node.__class__.__name__ == "Firewall"] - + @property def extended_hostnodes(self) -> List[Node]: - """Extended nodes that inherited HostNode in the network""" + """Extended nodes that inherited HostNode in the network.""" return [node for node in self.nodes.values() if node.__class__.__name__.lower() in HostNode._registry] - + @property def extended_networknodes(self) -> List[Node]: - """Extended nodes that inherited NetworkNode in the network""" + """Extended nodes that inherited NetworkNode in the network.""" return [node for node in self.nodes.values() if node.__class__.__name__.lower() in NetworkNode._registry] @property diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index ea162e88..8a420e44 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -332,7 +332,7 @@ class HostNode(Node): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask)) - def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register a hostnode type. @@ -340,7 +340,7 @@ class HostNode(Node): :type identifier: str :raises ValueError: When attempting to register an hostnode with a name that is already allocated. """ - if identifier == 'default': + if identifier == "default": return # Enforce lowercase registry entries because it makes comparisons everywhere else much easier. identifier = identifier.lower() diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 6515bb02..a0cb63e1 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -19,7 +19,7 @@ class NetworkNode(Node): _registry: ClassVar[Dict[str, Type["NetworkNode"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" - def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register a networknode type. @@ -27,7 +27,7 @@ class NetworkNode(Node): :type identifier: str :raises ValueError: When attempting to register an networknode with a name that is already allocated. """ - if identifier == 'default': + if identifier == "default": return identifier = identifier.lower() super().__init_subclass__(**kwargs) diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 013c473e..fded23f9 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -130,19 +130,20 @@ class ACLRule(SimComponent): dst_port: Optional[int] = None match_count: int = 0 - @field_validator('protocol', mode='before') - def protocol_valid(cls, val:Optional[str]) -> Optional[str]: + @field_validator("protocol", mode="before") + def protocol_valid(cls, val: Optional[str]) -> Optional[str]: + """Assert that the protocol for the rule is predefined in the IPProtocol lookup.""" if val is not None: assert val in IPProtocol.values(), f"Cannot create ACL rule with invalid protocol {val}" return val - @field_validator('src_port', 'dst_port', mode='before') - def ports_valid(cls, val:Optional[int]) -> Optional[int]: + @field_validator("src_port", "dst_port", mode="before") + def ports_valid(cls, val: Optional[int]) -> Optional[int]: + """Assert that the port for the rule is predefined in the Port lookup.""" if val is not None: assert val in Port.values(), f"Cannot create ACL rule with invalid port {val}" return val - def __str__(self) -> str: rule_strings = [] for key, value in self.model_dump(exclude={"uuid", "request_manager"}).items(): diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index d73bc756..1969a121 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Union from pydantic import validate_call -from primaite.simulator.network.airspace import AirSpace, AirSpaceFrequency, IPWirelessNetworkInterface +from primaite.simulator.network.airspace import AirSpace, IPWirelessNetworkInterface from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router, RouterInterface from primaite.simulator.network.transmission.data_link_layer import Frame @@ -116,7 +116,7 @@ class WirelessRouter(Router): >>> wireless_router.configure_wireless_access_point( ... ip_address="10.10.10.1", ... subnet_mask="255.255.255.0" - ... frequency=AirSpaceFrequency["WIFI_2_4"] + ... frequency="WIFI_2_4" ... ) """ @@ -153,7 +153,7 @@ class WirelessRouter(Router): self, ip_address: IPV4Address, subnet_mask: IPV4Address, - frequency: Optional[int] = AirSpaceFrequency["WIFI_2_4"], + frequency: Optional[str] = "WIFI_2_4", ): """ Configures a wireless access point (WAP). @@ -166,12 +166,12 @@ class WirelessRouter(Router): :param ip_address: The IP address to be assigned to the wireless access point. :param subnet_mask: The subnet mask associated with the IP address - :param frequency: The operating frequency of the wireless access point, defined by the AirSpaceFrequency + :param frequency: The operating frequency of the wireless access point, defined by the air space frequency enum. This determines the frequency band (e.g., 2.4 GHz or 5 GHz) the access point will use for wireless - communication. Default is AirSpaceFrequency["WIFI_2_4"]. + communication. Default is "WIFI_2_4". """ if not frequency: - frequency = AirSpaceFrequency["WIFI_2_4"] + frequency = "WIFI_2_4" self.sys_log.info("Configuring wireless access point") self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration @@ -264,7 +264,7 @@ class WirelessRouter(Router): if "wireless_access_point" in cfg: ip_address = cfg["wireless_access_point"]["ip_address"] subnet_mask = cfg["wireless_access_point"]["subnet_mask"] - frequency = AirSpaceFrequency[cfg["wireless_access_point"]["frequency"]] + frequency = cfg["wireless_access_point"]["frequency"] router.configure_wireless_access_point(ip_address=ip_address, subnet_mask=subnet_mask, frequency=frequency) if "acl" in cfg: diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index 36ff2751..a01b7f42 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -9,11 +9,11 @@ from primaite.utils.validators import IPV4Address _LOGGER = getLogger(__name__) -IPProtocol : dict[str, str] = dict( - NONE = "none", - TCP = "tcp", - UDP = "udp", - ICMP = "icmp", +IPProtocol: dict[str, str] = dict( + NONE="none", + TCP="tcp", + UDP="udp", + ICMP="icmp", ) # class IPProtocol(Enum): diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index c77ef532..60f2f070 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -1,40 +1,39 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum -from typing import List, Union +from typing import List from pydantic import BaseModel - Port: dict[str, int] = dict( - UNUSED = -1, - NONE = 0, - WOL = 9, - FTP_DATA = 20, - FTP = 21, - SSH = 22, - SMTP = 25, - DNS = 53, - HTTP = 80, - POP3 = 110, - SFTP = 115, - NTP = 123, - IMAP = 143, - SNMP = 161, - SNMP_TRAP = 162, - ARP = 219, - LDAP = 389, - HTTPS = 443, - SMB = 445, - IPP = 631, - SQL_SERVER = 1433, - MYSQL = 3306, - RDP = 3389, - RTP = 5004, - RTP_ALT = 5005, - DNS_ALT = 5353, - HTTP_ALT = 8080, - HTTPS_ALT = 8443, - POSTGRES_SERVER = 5432, + UNUSED=-1, + NONE=0, + WOL=9, + FTP_DATA=20, + FTP=21, + SSH=22, + SMTP=25, + DNS=53, + HTTP=80, + POP3=110, + SFTP=115, + NTP=123, + IMAP=143, + SNMP=161, + SNMP_TRAP=162, + ARP=219, + LDAP=389, + HTTPS=443, + SMB=445, + IPP=631, + SQL_SERVER=1433, + MYSQL=3306, + RDP=3389, + RTP=5004, + RTP_ALT=5005, + DNS_ALT=5353, + HTTP_ALT=8080, + HTTPS_ALT=8443, + POSTGRES_SERVER=5432, ) # class Port(): diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index b5284968..a7871315 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -44,7 +44,7 @@ class Application(IOSoftware): _registry: ClassVar[Dict[str, Type["Application"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" - def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register an application type. @@ -52,7 +52,7 @@ class Application(IOSoftware): :type identifier: str :raises ValueError: When attempting to register an application with a name that is already allocated. """ - if identifier == 'default': + if identifier == "default": return super().__init_subclass__(**kwargs) if identifier in cls._registry: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 06453330..9178e68a 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -1,5 +1,4 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from enum import Enum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 172be453..33de3443 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -76,9 +76,7 @@ class SessionManager: """ def __init__(self, sys_log: SysLog): - self.sessions_by_key: Dict[ - Tuple[str, IPv4Address, IPv4Address, Optional[int], Optional[int]], Session - ] = {} + self.sessions_by_key: Dict[Tuple[str, IPv4Address, IPv4Address, Optional[int], Optional[int]], Session] = {} self.sessions_by_uuid: Dict[str, Session] = {} self.sys_log: SysLog = sys_log self.software_manager: SoftwareManager = None # Noqa diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 36245e0f..49678c82 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -5,7 +5,6 @@ from typing import Dict, Optional from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.service import Service diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 74dcb506..4f0b879c 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -52,7 +52,7 @@ class Service(IOSoftware): def __init__(self, **kwargs): super().__init__(**kwargs) - def __init_subclass__(cls, identifier: str = 'default', **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register a hostnode type. @@ -60,7 +60,7 @@ class Service(IOSoftware): :type identifier: str :raises ValueError: When attempting to register an hostnode with a name that is already allocated. """ - if identifier == 'default': + if identifier == "default": return # Enforce lowercase registry entries because it makes comparisons everywhere else much easier. identifier = identifier.lower() diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 1880d244..084bdaf6 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -14,7 +14,6 @@ from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.sys_log import SysLog diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index b86bea7d..e4100741 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -1,3 +1,4 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict from prettytable import MARKDOWN, PrettyTable @@ -27,7 +28,7 @@ class GigaSwitch(NetworkNode, identifier="gigaswitch"): "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." def __init__(self, **kwargs): - print('--- Extended Component: GigaSwitch ---') + print("--- Extended Component: GigaSwitch ---") super().__init__(**kwargs) for i in range(1, self.num_ports + 1): self.connect_nic(SwitchPort()) diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 8a1465e9..55bdce09 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict -from primaite.simulator.network.hardware.nodes.host.host_node import NIC, HostNode +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.utils.validators import IPV4Address @@ -37,7 +37,7 @@ class SuperComputer(HostNode, identifier="supercomputer"): SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): - print('--- Extended Component: SuperComputer ---') + print("--- Extended Component: SuperComputer ---") super().__init__(ip_address=ip_address, subnet_mask=subnet_mask, **kwargs) pass diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index d4af600f..b745b774 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -17,7 +17,7 @@ from primaite.simulator.system.software import SoftwareHealthState _LOGGER = getLogger(__name__) -class ExtendedService(Service, identifier='extendedservice'): +class ExtendedService(Service, identifier="extendedservice"): """ A copy of DatabaseService that uses the extension framework instead of being part of PrimAITE. @@ -42,7 +42,7 @@ class ExtendedService(Service, identifier='extendedservice'): kwargs["protocol"] = IPProtocol["TCP"] super().__init__(**kwargs) self._create_db_file() - if kwargs.get('options'): + if kwargs.get("options"): opt = kwargs["options"] self.password = opt.get("db_password", None) if "backup_server_ip" in opt: @@ -139,7 +139,9 @@ class ExtendedService(Service, identifier='extendedservice'): old_visible_state = SoftwareHealthState.GOOD # get db file regardless of whether or not it was deleted - db_file = self.file_system.get_file(folder_name="database", file_name="extended_service_database.db", include_deleted=True) + db_file = self.file_system.get_file( + folder_name="database", file_name="extended_service_database.db", include_deleted=True + ) if db_file is None: self.sys_log.warning("Database file not initialised.") @@ -153,7 +155,9 @@ class ExtendedService(Service, identifier='extendedservice'): self.file_system.delete_file(folder_name="database", file_name="extended_service_database.db") # replace db file - self.file_system.copy_file(src_folder_name="downloads", src_file_name="extended_service_database.db", dst_folder_name="database") + self.file_system.copy_file( + src_folder_name="downloads", src_file_name="extended_service_database.db", dst_folder_name="database" + ) if self.db_file is None: self.sys_log.error("Copying database backup failed.") diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 5d8af64d..8467151b 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -1,22 +1,22 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import os + from primaite.config.load import get_extended_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config -import os +from tests.integration_tests.extensions.applications.extended_application import ExtendedApplication +from tests.integration_tests.extensions.nodes.giga_switch import GigaSwitch # Import the extended components so that PrimAITE registers them from tests.integration_tests.extensions.nodes.super_computer import SuperComputer -from tests.integration_tests.extensions.nodes.giga_switch import GigaSwitch from tests.integration_tests.extensions.services.extended_service import ExtendedService -from tests.integration_tests.extensions.applications.extended_application import ExtendedApplication def test_extended_example_config(): - """Test that the example config can be parsed properly.""" - config_path = os.path.join( "tests", "assets", "configs", "extended_config.yaml") + config_path = os.path.join("tests", "assets", "configs", "extended_config.yaml") game = load_config(config_path) network: Network = game.simulation.network @@ -25,8 +25,8 @@ def test_extended_example_config(): assert len(network.router_nodes) == 1 # 1 router in network assert len(network.switch_nodes) == 1 # 1 switches in network assert len(network.server_nodes) == 5 # 5 servers in network - assert len(network.extended_hostnodes) == 1 # One extended node based on HostNode - assert len(network.extended_networknodes) == 1 # One extended node based on NetworkNode + assert len(network.extended_hostnodes) == 1 # One extended node based on HostNode + assert len(network.extended_networknodes) == 1 # One extended node based on NetworkNode - assert 'ExtendedApplication' in network.extended_hostnodes[0].software_manager.software - assert 'ExtendedService' in network.extended_hostnodes[0].software_manager.software + assert "ExtendedApplication" in network.extended_hostnodes[0].software_manager.software + assert "ExtendedService" in network.extended_hostnodes[0].software_manager.software diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index 398c43a9..28f9ac5a 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -38,8 +38,8 @@ def test_acl_observations(simulation): acl_obs = ACLObservation( where=["network", "nodes", router.hostname, "acl", "acl"], ip_list=[], - port_list=["NTP", "HTTP", "POSTGRES_SERVER"], - protocol_list=["TCP", "UDP", "ICMP"], + port_list=[123, 80, 5432], + protocol_list=["tcp", "udp", "icmp"], num_rules=10, wildcard_list=[], ) diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 68506d59..21fe4bed 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -31,8 +31,8 @@ def test_firewall_observation(): num_rules=7, ip_list=["10.0.0.1", "10.0.0.2"], wildcard_list=["0.0.0.255", "0.0.0.1"], - port_list=["HTTP", "DNS"], - protocol_list=["TCP"], + port_list=[80, 53], + protocol_list=["tcp"], include_users=False, ) diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index bd8dfc4e..8254dad2 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -152,7 +152,12 @@ def test_config_nic_categories(simulation): def test_nic_monitored_traffic(simulation): - monitored_traffic = {"icmp": ["NONE"], "tcp": [53,]} + monitored_traffic = { + "icmp": ["NONE"], + "tcp": [ + 53, + ], + } pc: Computer = simulation.network.get_node_by_hostname("client_1") pc2: Computer = simulation.network.get_node_by_hostname("client_2") diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index 937bb061..c28e1bb8 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -24,8 +24,8 @@ def test_router_observation(): num_rules=7, ip_list=["10.0.0.1", "10.0.0.2"], wildcard_list=["0.0.0.255", "0.0.0.1"], - port_list=["HTTP", "DNS"], - protocol_list=["TCP"], + port_list=[80, 53], + protocol_list=["tcp"], ) router_observation = RouterObservation(where=[], ports=ports, num_ports=8, acl=acl, include_users=False) diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index d872c2b0..570c4ad6 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -65,7 +65,9 @@ def test_uc2_rewards(game_and_agent): db_client.run() router: Router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=2) + router.acl.add_rule( + ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=2 + ) comp = GreenAdminDatabaseUnreachablePenalty("client_1") diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py index 1794c4bc..e000f6ae 100644 --- a/tests/integration_tests/network/test_airspace_config.py +++ b/tests/integration_tests/network/test_airspace_config.py @@ -2,7 +2,6 @@ import yaml from primaite.game.game import PrimaiteGame -from primaite.simulator.network.airspace import AirSpaceFrequency from tests import TEST_ASSETS_ROOT @@ -13,8 +12,8 @@ def test_override_freq_max_capacity_mbps(): config_dict = yaml.safe_load(f) network = PrimaiteGame.from_config(cfg=config_dict).simulation.network - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_2_4"]) == 123.45 - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_5"]) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps("WIFI_2_4") == 123.45 + assert network.airspace.get_frequency_max_capacity_mbps("WIFI_5") == 0.0 pc_a = network.get_node_by_hostname("pc_a") pc_b = network.get_node_by_hostname("pc_b") @@ -32,8 +31,8 @@ def test_override_freq_max_capacity_mbps_blocked(): config_dict = yaml.safe_load(f) network = PrimaiteGame.from_config(cfg=config_dict).simulation.network - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_2_4"]) == 0.0 - assert network.airspace.get_frequency_max_capacity_mbps(AirSpaceFrequency["WIFI_5"]) == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps("WIFI_2_4") == 0.0 + assert network.airspace.get_frequency_max_capacity_mbps("WIFI_5") == 0.0 pc_a = network.get_node_by_hostname("pc_a") pc_b = network.get_node_by_hostname("pc_b") diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 8e06ccfb..44b660cf 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -73,8 +73,12 @@ def dmz_external_internal_network() -> Network: firewall_node.external_outbound_acl.add_rule( action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 ) - firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + firewall_node.dmz_inbound_acl.add_rule( + action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + ) + firewall_node.dmz_outbound_acl.add_rule( + action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + ) # external node external_node = Computer( @@ -262,8 +266,12 @@ def test_service_allowed_with_rule(dmz_external_internal_network): assert not internal_ntp_client.time - firewall.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) - firewall.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) + firewall.internal_outbound_acl.add_rule( + action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1 + ) + firewall.internal_inbound_acl.add_rule( + action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1 + ) internal_ntp_client.request_time() diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index 1064ed1b..9d92b660 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -73,7 +73,9 @@ def test_port_scan_one_node_one_port(example_network): client_2 = network.get_node_by_hostname("client_2") actual_result = client_1_nmap.port_scan( - target_ip_address=client_2.network_interface[1].ip_address, target_port=Port["DNS"], target_protocol=IPProtocol["TCP"] + target_ip_address=client_2.network_interface[1].ip_address, + target_port=Port["DNS"], + target_protocol=IPProtocol["TCP"], ) expected_result = {IPv4Address("192.168.10.22"): {IPProtocol["TCP"]: [Port["DNS"]]}} diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index 4e40bbd8..8becc6ae 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -66,7 +66,9 @@ def test_nested_dicts(): The expected output should have string values of enums as keys at all levels. """ original_dict = { - IPProtocol["UDP"]: {Port["ARP"]: {"inbound": 0, "outbound": 1016.0, "details": {IPProtocol["TCP"]: {"latency": "low"}}}} + IPProtocol["UDP"]: { + Port["ARP"]: {"inbound": 0, "outbound": 1016.0, "details": {IPProtocol["TCP"]: {"latency": "low"}}} + } } expected_dict = {"udp": {219: {"inbound": 0, "outbound": 1016.0, "details": {"tcp": {"latency": "low"}}}}} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict @@ -79,6 +81,9 @@ def test_non_dict_values(): The original dictionary contains lists and tuples as values. The expected output should preserve these non-dictionary values while converting enum keys to string values. """ - original_dict = {IPProtocol["UDP"]: [Port["ARP"], Port["HTTP"]], "protocols": (IPProtocol["TCP"], IPProtocol["UDP"])} + original_dict = { + IPProtocol["UDP"]: [Port["ARP"], Port["HTTP"]], + "protocols": (IPProtocol["TCP"], IPProtocol["UDP"]), + } expected_dict = {"udp": [Port["ARP"], Port["HTTP"]], "protocols": (IPProtocol["TCP"], IPProtocol["UDP"])} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict From 695891f55c5f8b8cc9ff9cf893fb0c880618af2f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 20 Sep 2024 11:21:28 +0100 Subject: [PATCH 004/224] Add port and protocol custom validators --- .../agent/observations/host_observations.py | 44 +++++------ .../agent/observations/nic_observations.py | 8 +- .../agent/observations/node_observations.py | 8 +- src/primaite/game/game.py | 22 +++--- src/primaite/simulator/network/creation.py | 10 ++- .../simulator/network/hardware/base.py | 23 +++--- .../hardware/nodes/network/firewall.py | 41 +++++----- .../network/hardware/nodes/network/router.py | 25 +++--- .../hardware/nodes/network/wireless_router.py | 11 ++- src/primaite/simulator/network/networks.py | 29 ++++--- .../network/transmission/data_link_layer.py | 13 ++-- .../network/transmission/network_layer.py | 32 +------- .../network/transmission/transport_layer.py | 77 +------------------ .../system/applications/database_client.py | 9 +-- .../simulator/system/applications/nmap.py | 13 ++-- .../red_applications/c2/abstract_c2.py | 18 ++--- .../red_applications/c2/c2_beacon.py | 12 +-- .../red_applications/data_manipulation_bot.py | 8 +- .../applications/red_applications/dos_bot.py | 6 +- .../red_applications/ransomware_script.py | 8 +- .../system/applications/web_browser.py | 12 +-- .../simulator/system/core/session_manager.py | 19 ++--- .../simulator/system/core/software_manager.py | 8 +- .../simulator/system/services/arp/arp.py | 9 +-- .../services/database/database_service.py | 8 +- .../system/services/dns/dns_client.py | 10 +-- .../system/services/dns/dns_server.py | 8 +- .../system/services/ftp/ftp_client.py | 18 ++--- .../system/services/ftp/ftp_server.py | 8 +- .../simulator/system/services/icmp/icmp.py | 8 +- .../system/services/ntp/ntp_client.py | 10 +-- .../system/services/ntp/ntp_server.py | 8 +- .../system/services/terminal/terminal.py | 8 +- .../system/services/web_server/web_server.py | 8 +- src/primaite/simulator/system/software.py | 4 +- src/primaite/utils/validators.py | 42 +++++++++- tests/conftest.py | 32 ++++---- .../nodes/network/test_firewall_config.py | 36 ++++----- .../nodes/network/test_router_config.py | 10 +-- .../applications/extended_application.py | 12 +-- .../extensions/services/extended_service.py | 8 +- .../actions/test_c2_suite_actions.py | 8 +- .../actions/test_configure_actions.py | 4 +- .../actions/test_terminal_actions.py | 4 +- .../observations/test_acl_observations.py | 4 +- .../observations/test_firewall_observation.py | 10 +-- .../observations/test_router_observation.py | 10 +-- .../observations/test_user_observations.py | 4 +- .../game_layer/test_actions.py | 28 +++---- .../game_layer/test_rewards.py | 13 +++- .../network/test_broadcast.py | 18 +++-- .../network/test_firewall.py | 40 +++++----- .../integration_tests/network/test_routing.py | 18 +++-- .../network/test_wireless_router.py | 10 ++- .../test_c2_suite_integration.py | 24 +++--- .../test_data_manipulation_bot_and_server.py | 7 +- .../test_dos_bot_and_server.py | 11 ++- .../test_ransomware_script.py | 7 +- tests/integration_tests/system/test_nmap.py | 30 +++++--- .../system/test_service_listening_on_ports.py | 20 ++--- .../test_web_client_server_and_database.py | 23 ++++-- .../test_simulation/test_request_response.py | 8 +- .../_network/_hardware/nodes/test_acl.py | 57 +++++++------- .../_network/_hardware/nodes/test_router.py | 8 +- .../_transmission/test_data_link_layer.py | 19 ++--- .../_red_applications/test_c2_suite.py | 24 +++--- .../test_data_manipulation_bot.py | 8 +- .../_red_applications/test_dos_bot.py | 2 +- .../_system/_applications/test_web_browser.py | 8 +- .../_system/_services/test_dns_client.py | 8 +- .../_system/_services/test_dns_server.py | 8 +- .../_system/_services/test_ftp_client.py | 14 ++-- .../_system/_services/test_ftp_server.py | 8 +- .../_system/_services/test_terminal.py | 18 +++-- .../_system/_services/test_web_server.py | 8 +- .../_simulator/_system/test_software.py | 8 +- .../_utils/test_dict_enum_keys_conversion.py | 27 ++++--- 77 files changed, 616 insertions(+), 613 deletions(-) diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 30ccd195..0984f008 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -5,7 +5,6 @@ from typing import Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType -from pydantic import field_validator from primaite import getLogger from primaite.game.agent.observations.file_system_observations import FolderObservation @@ -13,8 +12,7 @@ from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.software_observation import ApplicationObservation, ServiceObservation from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.utils.validators import IPProtocol, Port _LOGGER = getLogger(__name__) @@ -47,7 +45,7 @@ class HostObservation(AbstractObservation, identifier="HOST"): """Number of spaces for network interface observations on this host.""" include_nmne: Optional[bool] = None """Whether network interface observations should include number of malicious network events.""" - monitored_traffic: Optional[Dict] = None + monitored_traffic: Optional[Dict[IPProtocol, List[Port]]] = None """A dict containing which traffic types are to be included in the observation.""" include_num_access: Optional[bool] = None """Whether to include the number of accesses to files observations on this host.""" @@ -58,26 +56,26 @@ class HostObservation(AbstractObservation, identifier="HOST"): include_users: Optional[bool] = True """If True, report user session information.""" - @field_validator("monitored_traffic", mode="before") - def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: - """ - Convert monitored_traffic by lookup against Port and Protocol dicts. + # @field_validator("monitored_traffic", mode="before") + # def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: + # """ + # Convert monitored_traffic by lookup against Port and Protocol dicts. - This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. - This method will be removed in PrimAITE >= 4.0 - """ - if val is None: - return val - new_val = {} - for proto, port_list in val.items(): - # convert protocol, for instance ICMP becomes "icmp" - proto = IPProtocol[proto] if proto in IPProtocol else proto - new_val[proto] = [] - for port in port_list: - # convert ports, for instance "HTTP" becomes 80 - port = Port[port] if port in Port else port - new_val[proto].append(port) - return new_val + # This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. + # This method will be removed in PrimAITE >= 4.0 + # """ + # if val is None: + # return val + # new_val = {} + # for proto, port_list in val.items(): + # # convert protocol, for instance ICMP becomes "icmp" + # proto = PROTOCOL_LOOKUP[proto] if proto in PROTOCOL_LOOKUP else proto + # new_val[proto] = [] + # for port in port_list: + # # convert ports, for instance "HTTP" becomes 80 + # port = PORT_LOOKUP[port] if port in PORT_LOOKUP else port + # new_val[proto].append(port) + # return new_val def __init__( self, diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 296ce04c..c51cb427 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -9,8 +9,8 @@ from pydantic import field_validator from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): @@ -39,11 +39,11 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): new_val = {} for proto, port_list in val.items(): # convert protocol, for instance ICMP becomes "icmp" - proto = IPProtocol[proto] if proto in IPProtocol else proto + proto = PROTOCOL_LOOKUP[proto] if proto in PROTOCOL_LOOKUP else proto new_val[proto] = [] for port in port_list: # convert ports, for instance "HTTP" becomes 80 - port = Port[port] if port in Port else port + port = PORT_LOOKUP[port] if port in PORT_LOOKUP else port new_val[proto].append(port) return new_val diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 054ffcdb..0bb8ea0f 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -12,8 +12,8 @@ from primaite.game.agent.observations.firewall_observation import FirewallObserv from primaite.game.agent.observations.host_observations import HostObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.router_observation import RouterObservation -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -76,11 +76,11 @@ class NodesObservation(AbstractObservation, identifier="NODES"): new_val = {} for proto, port_list in val.items(): # convert protocol, for instance ICMP becomes "icmp" - proto = IPProtocol[proto] if proto in IPProtocol else proto + proto = PROTOCOL_LOOKUP[proto] if proto in PROTOCOL_LOOKUP else proto new_val[proto] = [] for port in port_list: # convert ports, for instance "HTTP" becomes 80 - port = Port[port] if port in Port else port + port = PORT_LOOKUP[port] if port in PORT_LOOKUP else port new_val[proto].append(port) return new_val diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8e0abb1e..a0d2ceb4 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -27,8 +27,7 @@ from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from primaite.simulator.network.nmne import NMNEConfig -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient # noqa: F401 @@ -51,6 +50,7 @@ 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 +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -97,8 +97,8 @@ class PrimaiteGameOptions(BaseModel): :warning: This will be deprecated in PrimAITE 4.0 and configs will need to be converted. """ for i, port_val in enumerate(vals): - if port_val in Port: - vals[i] = Port[port_val] + if port_val in PORT_LOOKUP: + vals[i] = PORT_LOOKUP[port_val] return vals @field_validator("protocols", mode="before") @@ -110,8 +110,8 @@ class PrimaiteGameOptions(BaseModel): :warning: This will be deprecated in PrimAITE 4.0 and configs will need to be converted. """ for i, proto_val in enumerate(vals): - if proto_val in IPProtocol: - vals[i] = IPProtocol[proto_val] + if proto_val in PROTOCOL_LOOKUP: + vals[i] = PROTOCOL_LOOKUP[proto_val] return vals @@ -381,7 +381,7 @@ class PrimaiteGame: if isinstance(port_id, int): port = port_id elif isinstance(port_id, str): - port = Port[port_id] + port = PORT_LOOKUP[port_id] if port: listen_on_ports.append(port) software.listen_on_ports = set(listen_on_ports) @@ -496,7 +496,7 @@ class PrimaiteGame: opt = application_cfg["options"] new_application.configure( target_ip_address=IPv4Address(opt.get("target_ip_address")), - target_port=Port[opt.get("target_port", "POSTGRES_SERVER")], + target_port=PORT_LOOKUP[opt.get("target_port", "POSTGRES_SERVER")], payload=opt.get("payload"), repeat=bool(opt.get("repeat")), port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")), @@ -509,8 +509,10 @@ class PrimaiteGame: new_application.configure( c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), keep_alive_frequency=(opt.get("keep_alive_frequency", 5)), - masquerade_protocol=IPProtocol[(opt.get("masquerade_protocol", IPProtocol["TCP"]))], - masquerade_port=Port[(opt.get("masquerade_port", Port["HTTP"]))], + masquerade_protocol=PROTOCOL_LOOKUP[ + (opt.get("masquerade_protocol", PROTOCOL_LOOKUP["TCP"])) + ], + masquerade_port=PORT_LOOKUP[(opt.get("masquerade_port", PORT_LOOKUP["HTTP"]))], ) if "network_interfaces" in node_cfg: for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index c2524b4b..9e2e5502 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -6,8 +6,8 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP def num_of_switches_required(num_nodes: int, max_network_interface: int = 24) -> int: @@ -98,8 +98,10 @@ def create_office_lan( default_gateway = IPv4Address(f"192.168.{subnet_base}.1") router = Router(hostname=f"router_{lan_name}", start_up_duration=0) router.power_on() - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) + router.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) network.add_node(router) router.configure_port(port=1, ip_address=default_gateway, subnet_mask="255.255.255.0") router.enable_port(1) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 4154cc08..affaf3cc 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -21,8 +21,7 @@ from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.packet_capture import PacketCapture from primaite.simulator.system.core.session_manager import SessionManager @@ -33,7 +32,7 @@ from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.terminal.terminal import Terminal from primaite.simulator.system.software import IOSoftware, Software from primaite.utils.converters import convert_dict_enum_keys_to_enum_values -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware) @@ -274,20 +273,20 @@ class NetworkInterface(SimComponent, ABC): # Identify the protocol and port from the frame if frame.tcp: - protocol = IPProtocol["TCP"] + protocol = PROTOCOL_LOOKUP["TCP"] port = frame.tcp.dst_port elif frame.udp: - protocol = IPProtocol["UDP"] + protocol = PROTOCOL_LOOKUP["UDP"] port = frame.udp.dst_port elif frame.icmp: - protocol = IPProtocol["ICMP"] + protocol = PROTOCOL_LOOKUP["ICMP"] # Ensure the protocol is in the capture dict if protocol not in self.traffic: self.traffic[protocol] = {} # Handle non-ICMP protocols that use ports - if protocol != IPProtocol["ICMP"]: + if protocol != PROTOCOL_LOOKUP["ICMP"]: if port not in self.traffic[protocol]: self.traffic[protocol][port] = {"inbound": 0, "outbound": 0} self.traffic[protocol][port][direction] += frame.size_Mbits @@ -843,8 +842,8 @@ class UserManager(Service): :param password: The password for the default admin user """ kwargs["name"] = "UserManager" - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["NONE"] + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) self.start() @@ -1166,8 +1165,8 @@ class UserSessionManager(Service): :param password: The password for the default admin user """ kwargs["name"] = "UserSessionManager" - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["NONE"] + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) self.start() @@ -1312,7 +1311,7 @@ class UserSessionManager(Service): software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( payload={"type": "user_timeout", "connection_id": session.uuid}, - dest_port=Port["SSH"], + dest_port=PORT_LOOKUP["SSH"], dest_ip_address=session.remote_ip_address, ) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 6d8e084d..eed1132b 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -14,10 +14,9 @@ from primaite.simulator.network.hardware.nodes.network.router import ( RouterInterface, ) from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.sys_log import SysLog -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP EXTERNAL_PORT_ID: Final[int] = 1 """The Firewall port ID of the external port.""" @@ -596,9 +595,9 @@ class Firewall(Router): for r_num, r_cfg in cfg["acl"]["internal_inbound_acl"].items(): firewall.internal_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), @@ -611,9 +610,9 @@ class Firewall(Router): for r_num, r_cfg in cfg["acl"]["internal_outbound_acl"].items(): firewall.internal_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), @@ -626,9 +625,9 @@ class Firewall(Router): for r_num, r_cfg in cfg["acl"]["dmz_inbound_acl"].items(): firewall.dmz_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), @@ -641,9 +640,9 @@ class Firewall(Router): for r_num, r_cfg in cfg["acl"]["dmz_outbound_acl"].items(): firewall.dmz_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), @@ -656,9 +655,9 @@ class Firewall(Router): for r_num, r_cfg in cfg["acl"]["external_inbound_acl"].items(): firewall.external_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), @@ -671,9 +670,9 @@ class Firewall(Router): for r_num, r_cfg in cfg["acl"]["external_outbound_acl"].items(): firewall.external_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index fded23f9..46efe668 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -17,15 +17,14 @@ from primaite.simulator.network.hardware.nodes.network.network_node import Netwo from primaite.simulator.network.protocols.arp import ARPPacket from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.nmap import NMAP from primaite.simulator.system.core.session_manager import SessionManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.arp.arp import ARP from primaite.simulator.system.services.icmp.icmp import ICMP from primaite.simulator.system.services.terminal.terminal import Terminal -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP @validate_call() @@ -134,14 +133,14 @@ class ACLRule(SimComponent): def protocol_valid(cls, val: Optional[str]) -> Optional[str]: """Assert that the protocol for the rule is predefined in the IPProtocol lookup.""" if val is not None: - assert val in IPProtocol.values(), f"Cannot create ACL rule with invalid protocol {val}" + assert val in PROTOCOL_LOOKUP.values(), f"Cannot create ACL rule with invalid protocol {val}" return val @field_validator("src_port", "dst_port", mode="before") def ports_valid(cls, val: Optional[int]) -> Optional[int]: """Assert that the port for the rule is predefined in the Port lookup.""" if val is not None: - assert val in Port.values(), f"Cannot create ACL rule with invalid port {val}" + assert val in PORT_LOOKUP.values(), f"Cannot create ACL rule with invalid port {val}" return val def __str__(self) -> str: @@ -1271,8 +1270,10 @@ class Router(NetworkNode): Initializes the router's ACL (Access Control List) with default rules, permitting essential protocols like ARP and ICMP, which are necessary for basic network operations and diagnostics. """ - self.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - self.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + self.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) + self.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) def setup_for_episode(self, episode: int): """ @@ -1371,9 +1372,9 @@ class Router(NetworkNode): """ dst_ip_address = frame.ip.dst_ip_address dst_port = None - if frame.ip.protocol == IPProtocol["TCP"]: + if frame.ip.protocol == PROTOCOL_LOOKUP["TCP"]: dst_port = frame.tcp.dst_port - elif frame.ip.protocol == IPProtocol["UDP"]: + elif frame.ip.protocol == PROTOCOL_LOOKUP["UDP"]: dst_port = frame.udp.dst_port if self.ip_is_router_interface(dst_ip_address) and ( @@ -1646,9 +1647,9 @@ class Router(NetworkNode): for r_num, r_cfg in cfg["acl"].items(): router.acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), dst_ip_address=r_cfg.get("dst_ip"), diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 1969a121..3615ef54 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -8,9 +8,8 @@ from primaite.simulator.network.airspace import AirSpace, IPWirelessNetworkInter from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router, RouterInterface from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port -from primaite.utils.validators import IPV4Address +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP class WirelessAccessPoint(IPWirelessNetworkInterface): @@ -271,9 +270,9 @@ class WirelessRouter(Router): for r_num, r_cfg in cfg["acl"].items(): router.acl.add_rule( action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], src_ip_address=r_cfg.get("src_ip"), dst_ip_address=r_cfg.get("dst_ip"), src_wildcard_mask=r_cfg.get("src_wildcard_mask"), diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index a73f3b12..c3b4a341 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -12,14 +12,14 @@ from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -79,9 +79,11 @@ def client_server_routed() -> Network: server_1.power_on() network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1]) - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) return network @@ -271,23 +273,30 @@ def arcd_uc2_network() -> Network: security_suite.connect_nic(NIC(ip_address="192.168.10.110", subnet_mask="255.255.255.0")) network.connect(endpoint_b=security_suite.network_interface[2], endpoint_a=switch_2.network_interface[7]) - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Allow PostgreSQL requests router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 + action=ACLAction.PERMIT, + src_port=PORT_LOOKUP["POSTGRES_SERVER"], + dst_port=PORT_LOOKUP["POSTGRES_SERVER"], + position=0, ) # Allow DNS requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=1) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["DNS"], dst_port=PORT_LOOKUP["DNS"], position=1) # Allow FTP requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=2) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["FTP"], dst_port=PORT_LOOKUP["FTP"], position=2) # Open port 80 for web server - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=3 + ) return network diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index b9bc48d9..ca212c58 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -7,10 +7,11 @@ from pydantic import BaseModel from primaite import getLogger from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.protocols.packet import DataPacket -from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol +from primaite.simulator.network.transmission.network_layer import IPPacket from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader -from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader, UDPHeader +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPHeader, UDPHeader from primaite.simulator.network.utils import convert_bytes_to_megabits +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -70,15 +71,15 @@ class Frame(BaseModel): msg = "Network Frame cannot have both a TCP header and a UDP header" _LOGGER.error(msg) raise ValueError(msg) - if kwargs["ip"].protocol == IPProtocol["TCP"] and not kwargs.get("tcp"): + if kwargs["ip"].protocol == PROTOCOL_LOOKUP["TCP"] and not kwargs.get("tcp"): msg = "Cannot build a Frame using the TCP IP Protocol without a TCPHeader" _LOGGER.error(msg) raise ValueError(msg) - if kwargs["ip"].protocol == IPProtocol["UDP"] and not kwargs.get("udp"): + if kwargs["ip"].protocol == PROTOCOL_LOOKUP["UDP"] and not kwargs.get("udp"): msg = "Cannot build a Frame using the UDP IP Protocol without a UDPHeader" _LOGGER.error(msg) raise ValueError(msg) - if kwargs["ip"].protocol == IPProtocol["ICMP"] and not kwargs.get("icmp"): + if kwargs["ip"].protocol == PROTOCOL_LOOKUP["ICMP"] and not kwargs.get("icmp"): msg = "Cannot build a Frame using the ICMP IP Protocol without a ICMPPacket" _LOGGER.error(msg) raise ValueError(msg) @@ -165,7 +166,7 @@ class Frame(BaseModel): :return: True if the Frame is an ARP packet, otherwise False. """ - return self.udp.dst_port == Port["ARP"] + return self.udp.dst_port == PORT_LOOKUP["ARP"] @property def is_icmp(self) -> bool: diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index a01b7f42..47e8a032 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -4,39 +4,11 @@ from enum import Enum from pydantic import BaseModel from primaite import getLogger -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPProtocol, IPV4Address, PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) -IPProtocol: dict[str, str] = dict( - NONE="none", - TCP="tcp", - UDP="udp", - ICMP="icmp", -) - -# class IPProtocol(Enum): -# """ -# Enum representing transport layer protocols in IP header. - -# .. _List of IPProtocols: -# """ - -# NONE = "none" -# """Placeholder for a non-protocol.""" -# TCP = "tcp" -# """Transmission Control Protocol.""" -# UDP = "udp" -# """User Datagram Protocol.""" -# ICMP = "icmp" -# """Internet Control Message Protocol.""" - -# def model_dump(self) -> str: -# """Return as JSON-serialisable string.""" -# return self.name - - class Precedence(Enum): """ Enum representing the Precedence levels in Quality of Service (QoS) for IP packets. @@ -98,7 +70,7 @@ class IPPacket(BaseModel): "Source IP address." dst_ip_address: IPV4Address "Destination IP address." - protocol: str = IPProtocol["TCP"] + protocol: IPProtocol = PROTOCOL_LOOKUP["TCP"] "IPProtocol." ttl: int = 64 "Time to Live (TTL) for the packet." diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index 60f2f070..fbc4b5ad 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -4,7 +4,7 @@ from typing import List from pydantic import BaseModel -Port: dict[str, int] = dict( +PORT_LOOKUP: dict[str, int] = dict( UNUSED=-1, NONE=0, WOL=9, @@ -36,81 +36,6 @@ Port: dict[str, int] = dict( POSTGRES_SERVER=5432, ) -# class Port(): -# def __getattr__() - - -# class Port(Enum): -# """ -# Enumeration of common known TCP/UDP ports used by protocols for operation of network applications. - -# .. _List of Ports: -# """ - -# UNUSED = -1 -# "An unused port stub." - -# NONE = 0 -# "Place holder for a non-port." -# WOL = 9 -# "Wake-on-Lan (WOL) - Used to turn or awaken a computer from sleep mode by a network message." -# FTP_DATA = 20 -# "File Transfer [Default Data]" -# FTP = 21 -# "File Transfer Protocol (FTP) - FTP control (command)" -# SSH = 22 -# "Secure Shell (SSH) - Used for secure remote access and command execution." -# SMTP = 25 -# "Simple Mail Transfer Protocol (SMTP) - Used for email delivery between servers." -# DNS = 53 -# "Domain Name System (DNS) - Used for translating domain names to IP addresses." -# HTTP = 80 -# "HyperText Transfer Protocol (HTTP) - Used for web traffic." -# POP3 = 110 -# "Post Office Protocol version 3 (POP3) - Used for retrieving emails from a mail server." -# SFTP = 115 -# "Secure File Transfer Protocol (SFTP) - Used for secure file transfer over SSH." -# NTP = 123 -# "Network Time Protocol (NTP) - Used for clock synchronization between computer systems." -# IMAP = 143 -# "Internet Message Access Protocol (IMAP) - Used for retrieving emails from a mail server." -# SNMP = 161 -# "Simple Network Management Protocol (SNMP) - Used for network device management." -# SNMP_TRAP = 162 -# "SNMP Trap - Used for sending SNMP notifications (traps) to a network management system." -# ARP = 219 -# "Address resolution Protocol - Used to connect a MAC address to an IP address." -# LDAP = 389 -# "Lightweight Directory Access Protocol (LDAP) - Used for accessing and modifying directory information." -# HTTPS = 443 -# "HyperText Transfer Protocol Secure (HTTPS) - Used for secure web traffic." -# SMB = 445 -# "Server Message Block (SMB) - Used for file sharing and printer sharing in Windows environments." -# IPP = 631 -# "Internet Printing Protocol (IPP) - Used for printing over the internet or an intranet." -# SQL_SERVER = 1433 -# "Microsoft SQL Server Database Engine - Used for communication with the SQL Server." -# MYSQL = 3306 -# "MySQL Database Server - Used for MySQL database communication." -# RDP = 3389 -# "Remote Desktop Protocol (RDP) - Used for remote desktop access to Windows machines." -# RTP = 5004 -# "Real-time Transport Protocol (RTP) - Used for transmitting real-time media, e.g., audio and video." -# RTP_ALT = 5005 -# "Alternative port for RTP (RTP_ALT) - Used in some configurations for transmitting real-time media." -# DNS_ALT = 5353 -# "Alternative port for DNS (DNS_ALT) - Used in some configurations for DNS service." -# HTTP_ALT = 8080 -# "Alternative port for HTTP (HTTP_ALT) - Often used as an alternative HTTP port for web applications." -# HTTPS_ALT = 8443 -# "Alternative port for HTTPS (HTTPS_ALT) - Used in some configurations for secure web traffic." -# POSTGRES_SERVER = 5432 -# "Postgres SQL Server." - -# def model_dump(self) -> str: -# """Return a json-serialisable string.""" -# return self.name - class UDPHeader(BaseModel): """ diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 170e2647..4967f519 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -11,11 +11,10 @@ from pydantic import BaseModel from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.software_manager import SoftwareManager -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP class DatabaseClientConnection(BaseModel): @@ -90,8 +89,8 @@ class DatabaseClient(Application, identifier="DatabaseClient"): def __init__(self, **kwargs): kwargs["name"] = "DatabaseClient" - kwargs["port"] = Port["POSTGRES_SERVER"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) def _init_request_manager(self) -> RequestManager: diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 74bce85d..34433e65 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -7,10 +7,9 @@ from pydantic import validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP class PortScanPayload(SimComponent): @@ -64,8 +63,8 @@ class NMAP(Application, identifier="NMAP"): def __init__(self, **kwargs): kwargs["name"] = "NMAP" - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["NONE"] + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) def _can_perform_network_action(self) -> bool: @@ -348,12 +347,12 @@ class NMAP(Application, identifier="NMAP"): if isinstance(target_port, int): target_port = [target_port] elif target_port is None: - target_port = [port for port in Port if port not in {Port["NONE"], Port["UNUSED"]}] + target_port = [port for port in PORT_LOOKUP if port not in {PORT_LOOKUP["NONE"], PORT_LOOKUP["UNUSED"]}] if isinstance(target_protocol, str): target_protocol = [target_protocol] elif target_protocol is None: - target_protocol = [IPProtocol["TCP"], IPProtocol["UDP"]] + target_protocol = [PROTOCOL_LOOKUP["TCP"], PROTOCOL_LOOKUP["UDP"]] scan_type = self._determine_port_scan_type(list(ip_addresses), target_port) active_ports = {} diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index d442d968..b0cdefba 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -9,14 +9,14 @@ from pydantic import BaseModel, Field, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.protocols.masquerade import C2Packet -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.session_manager import Session 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.service import ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validators import PROTOCOL_LOOKUP class C2Command(Enum): @@ -81,10 +81,10 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_frequency: int = Field(default=5, ge=1) """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" - masquerade_protocol: str = Field(default=IPProtocol["TCP"]) + masquerade_protocol: str = Field(default=PROTOCOL_LOOKUP["TCP"]) """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - masquerade_port: int = Field(default=Port["HTTP"]) + masquerade_port: int = Field(default=PORT_LOOKUP["HTTP"]) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" c2_config: _C2Opts = _C2Opts() @@ -142,9 +142,9 @@ class AbstractC2(Application, identifier="AbstractC2"): def __init__(self, **kwargs): """Initialise the C2 applications to by default listen for HTTP traffic.""" - kwargs["listen_on_ports"] = {Port["HTTP"], Port["FTP"], Port["DNS"]} - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["listen_on_ports"] = {PORT_LOOKUP["HTTP"], PORT_LOOKUP["FTP"], PORT_LOOKUP["DNS"]} + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @property @@ -410,8 +410,8 @@ class AbstractC2(Application, identifier="AbstractC2"): self.keep_alive_inactivity = 0 self.keep_alive_frequency = 5 self.c2_remote_connection = None - self.c2_config.masquerade_port = Port["HTTP"] - self.c2_config.masquerade_protocol = IPProtocol["TCP"] + self.c2_config.masquerade_port = PORT_LOOKUP["HTTP"] + self.c2_config.masquerade_protocol = PROTOCOL_LOOKUP["TCP"] @abstractmethod def _confirm_remote_connection(self, timestep: int) -> bool: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 9178e68a..450c60ad 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -8,12 +8,12 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts, RansomwareOpts, TerminalOpts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.terminal.terminal import Terminal, TerminalClientConnection +from primaite.utils.validators import PROTOCOL_LOOKUP class C2Beacon(AbstractC2, identifier="C2Beacon"): @@ -111,8 +111,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.configure( c2_server_ip_address=c2_remote_ip, keep_alive_frequency=frequency, - masquerade_protocol=IPProtocol[protocol], - masquerade_port=Port[port], + masquerade_protocol=PROTOCOL_LOOKUP[protocol], + masquerade_port=PORT_LOOKUP[port], ) ) @@ -129,8 +129,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self, c2_server_ip_address: IPv4Address = None, keep_alive_frequency: int = 5, - masquerade_protocol: str = IPProtocol["TCP"], - masquerade_port: int = Port["HTTP"], + masquerade_protocol: str = PROTOCOL_LOOKUP["TCP"], + masquerade_port: int = PORT_LOOKUP["HTTP"], ) -> bool: """ Configures the C2 beacon to communicate with the C2 server. diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index d74ae384..c2d19160 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -7,10 +7,10 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -50,8 +50,8 @@ class DataManipulationBot(Application, identifier="DataManipulationBot"): def __init__(self, **kwargs): kwargs["name"] = "DataManipulationBot" - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["NONE"] + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) self._db_connection: Optional[DatabaseClientConnection] = None diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index 2cc99c4a..7e199b48 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -7,7 +7,7 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient _LOGGER = getLogger(__name__) @@ -85,7 +85,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): if "target_ip_address" in request[-1]: request[-1]["target_ip_address"] = IPv4Address(request[-1]["target_ip_address"]) if "target_port" in request[-1]: - request[-1]["target_port"] = Port[request[-1]["target_port"]] + request[-1]["target_port"] = PORT_LOOKUP[request[-1]["target_port"]] return RequestResponse.from_bool(self.configure(**request[-1])) rm.add_request("configure", request_type=RequestType(func=_configure)) @@ -94,7 +94,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): def configure( self, target_ip_address: IPv4Address, - target_port: Optional[int] = Port["POSTGRES_SERVER"], + target_port: Optional[int] = PORT_LOOKUP["POSTGRES_SERVER"], payload: Optional[str] = None, repeat: bool = False, port_scan_p_of_success: float = 0.1, diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index a819190c..56f885f4 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -6,10 +6,10 @@ from prettytable import MARKDOWN, PrettyTable from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection +from primaite.utils.validators import PROTOCOL_LOOKUP class RansomwareScript(Application, identifier="RansomwareScript"): @@ -27,8 +27,8 @@ class RansomwareScript(Application, identifier="RansomwareScript"): def __init__(self, **kwargs): kwargs["name"] = "RansomwareScript" - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["NONE"] + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) self._db_connection: Optional[DatabaseClientConnection] = None diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 6707fa52..faa7b5ec 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -15,10 +15,10 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.dns.dns_client import DNSClient +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -43,10 +43,10 @@ class WebBrowser(Application, identifier="WebBrowser"): def __init__(self, **kwargs): kwargs["name"] = "WebBrowser" - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] # default for web is port 80 if kwargs.get("port") is None: - kwargs["port"] = Port["HTTP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] super().__init__(**kwargs) self.run() @@ -126,7 +126,7 @@ class WebBrowser(Application, identifier="WebBrowser"): if self.send( payload=payload, dest_ip_address=self.domain_name_ip_address, - dest_port=parsed_url.port if parsed_url.port else Port["HTTP"], + dest_port=parsed_url.port if parsed_url.port else PORT_LOOKUP["HTTP"], ): self.sys_log.info( f"{self.name}: Received HTTP {payload.request_method.name} " @@ -154,7 +154,7 @@ class WebBrowser(Application, identifier="WebBrowser"): self, payload: HttpRequestPacket, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = Port["HTTP"], + dest_port: Optional[int] = PORT_LOOKUP["HTTP"], session_id: Optional[str] = None, **kwargs, ) -> bool: diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 33de3443..fcf07d9f 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -10,8 +10,9 @@ from primaite.simulator.core import SimComponent from primaite.simulator.network.protocols.arp import ARPPacket from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame -from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader, UDPHeader +from primaite.simulator.network.transmission.network_layer import IPPacket +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPHeader, UDPHeader +from primaite.utils.validators import PROTOCOL_LOOKUP if TYPE_CHECKING: from primaite.simulator.network.hardware.base import NetworkInterface @@ -117,7 +118,7 @@ class SessionManager: """ protocol = frame.ip.protocol with_ip_address = frame.ip.src_ip_address - if protocol == IPProtocol["TCP"]: + if protocol == PROTOCOL_LOOKUP["TCP"]: if inbound_frame: src_port = frame.tcp.src_port dst_port = frame.tcp.dst_port @@ -125,7 +126,7 @@ class SessionManager: dst_port = frame.tcp.src_port src_port = frame.tcp.dst_port with_ip_address = frame.ip.dst_ip_address - elif protocol == IPProtocol["UDP"]: + elif protocol == PROTOCOL_LOOKUP["UDP"]: if inbound_frame: src_port = frame.udp.src_port dst_port = frame.udp.dst_port @@ -260,7 +261,7 @@ class SessionManager: src_port: Optional[int] = None, dst_port: Optional[int] = None, session_id: Optional[str] = None, - ip_protocol: str = IPProtocol["TCP"], + ip_protocol: str = PROTOCOL_LOOKUP["TCP"], icmp_packet: Optional[ICMPPacket] = None, ) -> Union[Any, None]: """ @@ -284,7 +285,7 @@ class SessionManager: dst_mac_address = payload.target_mac_addr outbound_network_interface = self.resolve_outbound_network_interface(payload.target_ip_address) is_broadcast = payload.request - ip_protocol = IPProtocol["UDP"] + ip_protocol = PROTOCOL_LOOKUP["UDP"] else: vals = self.resolve_outbound_transmission_details( dst_ip_address=dst_ip_address, @@ -316,12 +317,12 @@ class SessionManager: tcp_header = None udp_header = None - if ip_protocol == IPProtocol["TCP"]: + if ip_protocol == PROTOCOL_LOOKUP["TCP"]: tcp_header = TCPHeader( src_port=dst_port, dst_port=dst_port, ) - elif ip_protocol == IPProtocol["UDP"]: + elif ip_protocol == PROTOCOL_LOOKUP["UDP"]: udp_header = UDPHeader( src_port=dst_port, dst_port=dst_port, @@ -385,7 +386,7 @@ class SessionManager: elif frame.udp: dst_port = frame.udp.dst_port elif frame.icmp: - dst_port = Port["NONE"] + dst_port = PORT_LOOKUP["NONE"] self.software_manager.receive_payload_from_session_manager( payload=frame.payload, port=dst_port, diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 8eac33fa..abf2ca3a 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -8,12 +8,12 @@ from prettytable import MARKDOWN, PrettyTable from primaite.simulator.core import RequestType from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import IOSoftware +from primaite.utils.validators import PROTOCOL_LOOKUP if TYPE_CHECKING: from primaite.simulator.system.core.session_manager import SessionManager @@ -191,7 +191,7 @@ class SoftwareManager: dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, src_port: Optional[int] = None, dest_port: Optional[int] = None, - ip_protocol: str = IPProtocol["TCP"], + ip_protocol: str = PROTOCOL_LOOKUP["TCP"], session_id: Optional[str] = None, ) -> bool: """ @@ -275,7 +275,7 @@ class SoftwareManager: software_type, software.operating_state.name, software.health_state_actual.name, - software.port if software.port != Port["NONE"] else None, + software.port if software.port != PORT_LOOKUP["NONE"] else None, software.protocol, ] ) diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index b8dd5f89..2641f1c8 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -8,10 +8,9 @@ from prettytable import MARKDOWN, PrettyTable from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service -from primaite.utils.validators import IPV4Address +from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP class ARP(Service): @@ -26,8 +25,8 @@ class ARP(Service): def __init__(self, **kwargs): kwargs["name"] = "ARP" - kwargs["port"] = Port["ARP"] - kwargs["protocol"] = IPProtocol["UDP"] + kwargs["port"] = PORT_LOOKUP["ARP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 11ca9eb2..f9a5d087 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -7,12 +7,12 @@ from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.file_system.folder import Folder -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -38,8 +38,8 @@ class DatabaseService(Service): def __init__(self, **kwargs): kwargs["name"] = "DatabaseService" - kwargs["port"] = Port["POSTGRES_SERVER"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self._create_db_file() diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 62f14366..316189a7 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -4,10 +4,10 @@ from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.service import Service +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -22,11 +22,11 @@ class DNSClient(Service): def __init__(self, **kwargs): kwargs["name"] = "DNSClient" - kwargs["port"] = Port["DNS"] + kwargs["port"] = PORT_LOOKUP["DNS"] # DNS uses UDP by default # it switches to TCP when the bytes exceed 512 (or 4096) bytes # TCP for now - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self.start() @@ -95,7 +95,7 @@ class DNSClient(Service): # send a request to check if domain name exists in the DNS Server software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=self.dns_server, dest_port=Port["DNS"] + payload=payload, dest_ip_address=self.dns_server, dest_port=PORT_LOOKUP["DNS"] ) # recursively re-call the function passing is_reattempt=True diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 93895825..e0786124 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -6,9 +6,9 @@ from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -21,11 +21,11 @@ class DNSServer(Service): def __init__(self, **kwargs): kwargs["name"] = "DNSServer" - kwargs["port"] = Port["DNS"] + kwargs["port"] = PORT_LOOKUP["DNS"] # DNS uses UDP by default # it switches to TCP when the bytes exceed 512 (or 4096) bytes # TCP for now - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self.start() diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 1fce4133..11a926cf 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -7,10 +7,10 @@ from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -25,8 +25,8 @@ class FTPClient(FTPServiceABC): def __init__(self, **kwargs): kwargs["name"] = "FTPClient" - kwargs["port"] = Port["FTP"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["FTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self.start() @@ -104,7 +104,7 @@ class FTPClient(FTPServiceABC): def _connect_to_server( self, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = Port["FTP"], + dest_port: Optional[int] = PORT_LOOKUP["FTP"], session_id: Optional[str] = None, is_reattempt: Optional[bool] = False, ) -> bool: @@ -124,7 +124,7 @@ class FTPClient(FTPServiceABC): # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now # create FTP packet - payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=Port["FTP"]) + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=PORT_LOOKUP["FTP"]) if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id): if payload.status_code == FTPStatusCode.OK: @@ -152,7 +152,7 @@ class FTPClient(FTPServiceABC): return False def _disconnect_from_server( - self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[int] = Port["FTP"] + self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[int] = PORT_LOOKUP["FTP"] ) -> bool: """ Connects the client from a given FTP server. @@ -179,7 +179,7 @@ class FTPClient(FTPServiceABC): src_file_name: str, dest_folder_name: str, dest_file_name: str, - dest_port: Optional[int] = Port["FTP"], + dest_port: Optional[int] = PORT_LOOKUP["FTP"], session_id: Optional[str] = None, ) -> bool: """ @@ -241,7 +241,7 @@ class FTPClient(FTPServiceABC): src_file_name: str, dest_folder_name: str, dest_file_name: str, - dest_port: Optional[int] = Port["FTP"], + dest_port: Optional[int] = PORT_LOOKUP["FTP"], ) -> bool: """ Request a file from a target IP address. diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 701bff79..38a253be 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -3,9 +3,9 @@ from typing import Any, Optional from primaite import getLogger from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -23,8 +23,8 @@ class FTPServer(FTPServiceABC): def __init__(self, **kwargs): kwargs["name"] = "FTPServer" - kwargs["port"] = Port["FTP"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["FTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self.start() diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index a2dfac0d..486ba2b0 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -7,9 +7,9 @@ from primaite import getLogger from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -26,8 +26,8 @@ class ICMP(Service): def __init__(self, **kwargs): kwargs["name"] = "ICMP" - kwargs["port"] = Port["NONE"] - kwargs["protocol"] = IPProtocol["ICMP"] + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["ICMP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 40b8d273..184833e1 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -5,9 +5,9 @@ from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service, ServiceOperatingState +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -21,8 +21,8 @@ class NTPClient(Service): def __init__(self, **kwargs): kwargs["name"] = "NTPClient" - kwargs["port"] = Port["NTP"] - kwargs["protocol"] = IPProtocol["UDP"] + kwargs["port"] = PORT_LOOKUP["NTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) self.start() @@ -55,7 +55,7 @@ class NTPClient(Service): payload: NTPPacket, session_id: Optional[str] = None, dest_ip_address: IPv4Address = None, - dest_port: int = Port["NTP"], + dest_port: int = PORT_LOOKUP["NTP"], **kwargs, ) -> bool: """Requests NTP data from NTP server. diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index d9de40c6..4764bffb 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -4,9 +4,9 @@ from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -16,8 +16,8 @@ class NTPServer(Service): def __init__(self, **kwargs): kwargs["name"] = "NTPServer" - kwargs["port"] = Port["NTP"] - kwargs["protocol"] = IPProtocol["UDP"] + kwargs["port"] = PORT_LOOKUP["NTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) self.start() diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 41987aff..2b0bc02b 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -17,10 +17,10 @@ from primaite.simulator.network.protocols.ssh import ( SSHTransportMessage, SSHUserCredentials, ) -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.service import Service, ServiceOperatingState +from primaite.utils.validators import PROTOCOL_LOOKUP # TODO 2824: Since remote terminal connections and remote user sessions are the same thing, we could refactor @@ -137,8 +137,8 @@ class Terminal(Service): def __init__(self, **kwargs): kwargs["name"] = "Terminal" - kwargs["port"] = Port["SSH"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["SSH"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index c021a86e..2805b1b2 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -10,11 +10,11 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClientConnection from primaite.simulator.system.services.service import Service from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -49,10 +49,10 @@ class WebServer(Service): def __init__(self, **kwargs): kwargs["name"] = "WebServer" - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] # default for web is port 80 if kwargs.get("port") is None: - kwargs["port"] = Port["HTTP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] super().__init__(**kwargs) self._install_web_files() diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 084bdaf6..d34678b9 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -13,9 +13,9 @@ from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.sys_log import SysLog +from primaite.utils.validators import PROTOCOL_LOOKUP if TYPE_CHECKING: from primaite.simulator.system.core.software_manager import SoftwareManager @@ -386,7 +386,7 @@ class IOSoftware(Software): session_id: Optional[str] = None, dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, dest_port: Optional[int] = None, - ip_protocol: str = IPProtocol["TCP"], + ip_protocol: str = PROTOCOL_LOOKUP["TCP"], **kwargs, ) -> bool: """ diff --git a/src/primaite/utils/validators.py b/src/primaite/utils/validators.py index 139d303c..f07b475d 100644 --- a/src/primaite/utils/validators.py +++ b/src/primaite/utils/validators.py @@ -6,6 +6,9 @@ from pydantic import BeforeValidator from typing_extensions import Annotated +# Define a custom type IPV4Address using the typing_extensions.Annotated. +# Annotated is used to attach metadata to type hints. In this case, it's used to associate the ipv4_validator +# with the IPv4Address type, ensuring that any usage of IPV4Address undergoes validation before assignment. def ipv4_validator(v: Any) -> IPv4Address: """ Validate the input and ensure it can be converted to an IPv4Address instance. @@ -24,9 +27,6 @@ def ipv4_validator(v: Any) -> IPv4Address: return IPv4Address(v) -# Define a custom type IPV4Address using the typing_extensions.Annotated. -# Annotated is used to attach metadata to type hints. In this case, it's used to associate the ipv4_validator -# with the IPv4Address type, ensuring that any usage of IPV4Address undergoes validation before assignment. IPV4Address: Final[Annotated] = Annotated[IPv4Address, BeforeValidator(ipv4_validator)] """ IPv4Address with with IPv4Address with with pre-validation and auto-conversion from str using ipv4_validator.. @@ -37,3 +37,39 @@ will automatically check and convert the input value to an instance of IPv4Addre any Pydantic model uses it. This ensures that any field marked with this type is not just an IPv4Address in form, but also valid according to the rules defined in ipv4_validator. """ + +# Define a custom port validator +Port: Final[Annotated] = Annotated[int, BeforeValidator(lambda n: 0 <= n <= 65535)] +"""Validates that network ports lie in the appropriate range of [0,65535].""" + +# Define a custom IP protocol validator +PROTOCOL_LOOKUP: dict[str, str] = dict( + NONE="none", + TCP="tcp", + UDP="udp", + ICMP="icmp", +) +""" +Lookup table used for compatibility with PrimAITE <= 3.3. Configs with the capitalised protocol names are converted +to lowercase at runtime. +""" +VALID_PROTOCOLS = ["none", "tcp", "udp", "icmp"] +"""Supported protocols.""" + + +def protocol_validator(v: Any) -> str: + """ + Validate that IP Protocols are chosen from the list of supported IP Protocols. + + The protocol list is dynamic because plugins are able to extend it, therefore it is necessary to use this custom + validator instead of being able to specify a union of string literals. + """ + if v in PROTOCOL_LOOKUP: + return PROTOCOL_LOOKUP(v) + if v in VALID_PROTOCOLS: + return v + raise ValueError(f"{v} is not a valid IP Protocol. It must be one of the following: {VALID_PROTOCOLS}") + + +IPProtocol: Final[Annotated] = Annotated[str, BeforeValidator(protocol_validator)] +"""Validates that IP Protocols used in the simulation belong to the list of supported protocols.""" diff --git a/tests/conftest.py b/tests/conftest.py index 1ffa2146..687bec92 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,8 +18,7 @@ from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.networks import arcd_uc2_network -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.web_browser import WebBrowser @@ -28,6 +27,7 @@ from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT rayinit() @@ -45,8 +45,8 @@ class DummyService(Service): def __init__(self, **kwargs): kwargs["name"] = "DummyService" - kwargs["port"] = Port["HTTP"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) def receive(self, payload: Any, session_id: str, **kwargs) -> bool: @@ -58,8 +58,8 @@ class DummyApplication(Application, identifier="DummyApplication"): def __init__(self, **kwargs): kwargs["name"] = "DummyApplication" - kwargs["port"] = Port["HTTP"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: @@ -77,7 +77,7 @@ def uc2_network() -> Network: @pytest.fixture(scope="function") def service(file_system) -> DummyService: return DummyService( - name="DummyService", port=Port["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_service") + name="DummyService", port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_service") ) @@ -90,7 +90,7 @@ def service_class(): def application(file_system) -> DummyApplication: return DummyApplication( name="DummyApplication", - port=Port["ARP"], + port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_application"), ) @@ -350,10 +350,10 @@ def install_stuff_to_sim(sim: Simulation): network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2]) # 2: Configure base ACL - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=1) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22) + router.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["DNS"], dst_port=PORT_LOOKUP["DNS"], position=1) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=3) # 3: Install server software server_1.software_manager.install(DNSServer) @@ -379,13 +379,13 @@ def install_stuff_to_sim(sim: Simulation): r = sim.network.router_nodes[0] for i, acl_rule in enumerate(r.acl.acl): if i == 1: - assert acl_rule.src_port == acl_rule.dst_port == Port["DNS"] + assert acl_rule.src_port == acl_rule.dst_port == PORT_LOOKUP["DNS"] elif i == 3: - assert acl_rule.src_port == acl_rule.dst_port == Port["HTTP"] + assert acl_rule.src_port == acl_rule.dst_port == PORT_LOOKUP["HTTP"] elif i == 22: - assert acl_rule.src_port == acl_rule.dst_port == Port["ARP"] + assert acl_rule.src_port == acl_rule.dst_port == PORT_LOOKUP["ARP"] elif i == 23: - assert acl_rule.protocol == IPProtocol["ICMP"] + assert acl_rule.protocol == PROTOCOL_LOOKUP["ICMP"] elif i == 24: ... else: diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py index d35e2ebb..6d0ef7b0 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -9,8 +9,8 @@ from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP from tests.integration_tests.configuration_file_parsing import BASIC_FIREWALL, DMZ_NETWORK, load_config @@ -68,44 +68,44 @@ def test_firewall_acl_rules_correctly_added(dmz_config): # ICMP and ARP should be allowed internal_inbound assert firewall.internal_inbound_acl.num_rules == 2 assert firewall.internal_inbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.internal_inbound_acl.acl[22].src_port == Port["ARP"] - assert firewall.internal_inbound_acl.acl[22].dst_port == Port["ARP"] + assert firewall.internal_inbound_acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert firewall.internal_inbound_acl.acl[22].dst_port == PORT_LOOKUP["ARP"] assert firewall.internal_inbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.internal_inbound_acl.acl[23].protocol == IPProtocol["ICMP"] + assert firewall.internal_inbound_acl.acl[23].protocol == PROTOCOL_LOOKUP["ICMP"] assert firewall.internal_inbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed internal_outbound assert firewall.internal_outbound_acl.num_rules == 2 assert firewall.internal_outbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.internal_outbound_acl.acl[22].src_port == Port["ARP"] - assert firewall.internal_outbound_acl.acl[22].dst_port == Port["ARP"] + assert firewall.internal_outbound_acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert firewall.internal_outbound_acl.acl[22].dst_port == PORT_LOOKUP["ARP"] assert firewall.internal_outbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.internal_outbound_acl.acl[23].protocol == IPProtocol["ICMP"] + assert firewall.internal_outbound_acl.acl[23].protocol == PROTOCOL_LOOKUP["ICMP"] assert firewall.internal_outbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed dmz_inbound assert firewall.dmz_inbound_acl.num_rules == 2 assert firewall.dmz_inbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.dmz_inbound_acl.acl[22].src_port == Port["ARP"] - assert firewall.dmz_inbound_acl.acl[22].dst_port == Port["ARP"] + assert firewall.dmz_inbound_acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert firewall.dmz_inbound_acl.acl[22].dst_port == PORT_LOOKUP["ARP"] assert firewall.dmz_inbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.dmz_inbound_acl.acl[23].protocol == IPProtocol["ICMP"] + assert firewall.dmz_inbound_acl.acl[23].protocol == PROTOCOL_LOOKUP["ICMP"] assert firewall.dmz_inbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed dmz_outbound assert firewall.dmz_outbound_acl.num_rules == 2 assert firewall.dmz_outbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.dmz_outbound_acl.acl[22].src_port == Port["ARP"] - assert firewall.dmz_outbound_acl.acl[22].dst_port == Port["ARP"] + assert firewall.dmz_outbound_acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert firewall.dmz_outbound_acl.acl[22].dst_port == PORT_LOOKUP["ARP"] assert firewall.dmz_outbound_acl.acl[23].action == ACLAction.PERMIT - assert firewall.dmz_outbound_acl.acl[23].protocol == IPProtocol["ICMP"] + assert firewall.dmz_outbound_acl.acl[23].protocol == PROTOCOL_LOOKUP["ICMP"] assert firewall.dmz_outbound_acl.implicit_action == ACLAction.DENY # ICMP and ARP should be allowed external_inbound assert firewall.external_inbound_acl.num_rules == 1 assert firewall.external_inbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.external_inbound_acl.acl[22].src_port == Port["ARP"] - assert firewall.external_inbound_acl.acl[22].dst_port == Port["ARP"] + assert firewall.external_inbound_acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert firewall.external_inbound_acl.acl[22].dst_port == PORT_LOOKUP["ARP"] # external_inbound should have implicit action PERMIT # ICMP does not have a provided ACL Rule but implicit action should allow anything assert firewall.external_inbound_acl.implicit_action == ACLAction.PERMIT @@ -113,8 +113,8 @@ def test_firewall_acl_rules_correctly_added(dmz_config): # ICMP and ARP should be allowed external_outbound assert firewall.external_outbound_acl.num_rules == 1 assert firewall.external_outbound_acl.acl[22].action == ACLAction.PERMIT - assert firewall.external_outbound_acl.acl[22].src_port == Port["ARP"] - assert firewall.external_outbound_acl.acl[22].dst_port == Port["ARP"] + assert firewall.external_outbound_acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert firewall.external_outbound_acl.acl[22].dst_port == PORT_LOOKUP["ARP"] # external_outbound should have implicit action PERMIT # ICMP does not have a provided ACL Rule but implicit action should allow anything assert firewall.external_outbound_acl.implicit_action == ACLAction.PERMIT diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index 16543565..c348ee81 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -6,8 +6,8 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config @@ -63,8 +63,8 @@ def test_router_acl_rules_correctly_added(dmz_config): # ICMP and ARP should be allowed assert router_1.acl.num_rules == 2 assert router_1.acl.acl[22].action == ACLAction.PERMIT - assert router_1.acl.acl[22].src_port == Port["ARP"] - assert router_1.acl.acl[22].dst_port == Port["ARP"] + assert router_1.acl.acl[22].src_port == PORT_LOOKUP["ARP"] + assert router_1.acl.acl[22].dst_port == PORT_LOOKUP["ARP"] assert router_1.acl.acl[23].action == ACLAction.PERMIT - assert router_1.acl.acl[23].protocol == IPProtocol["ICMP"] + assert router_1.acl.acl[23].protocol == PROTOCOL_LOOKUP["ICMP"] assert router_1.acl.implicit_action == ACLAction.DENY diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 8e3d33e1..28029b32 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -15,11 +15,11 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.dns.dns_client import DNSClient +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -44,10 +44,10 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): def __init__(self, **kwargs): kwargs["name"] = "ExtendedApplication" - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] # default for web is port 80 if kwargs.get("port") is None: - kwargs["port"] = Port["HTTP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] super().__init__(**kwargs) self.run() @@ -127,7 +127,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): if self.send( payload=payload, dest_ip_address=self.domain_name_ip_address, - dest_port=parsed_url.port if parsed_url.port else Port["HTTP"], + dest_port=parsed_url.port if parsed_url.port else PORT_LOOKUP["HTTP"], ): self.sys_log.info( f"{self.name}: Received HTTP {payload.request_method.name} " @@ -155,7 +155,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): self, payload: HttpRequestPacket, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = Port["HTTP"], + dest_port: Optional[int] = PORT_LOOKUP["HTTP"], session_id: Optional[str] = None, **kwargs, ) -> bool: diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index b745b774..70d47aaa 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -7,12 +7,12 @@ from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.file_system.folder import Folder -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validators import PROTOCOL_LOOKUP _LOGGER = getLogger(__name__) @@ -38,8 +38,8 @@ class ExtendedService(Service, identifier="extendedservice"): def __init__(self, **kwargs): kwargs["name"] = "ExtendedService" - kwargs["port"] = Port["POSTGRES_SERVER"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self._create_db_file() if kwargs.get("options"): diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 17b0ba8c..2c750621 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -11,7 +11,7 @@ from primaite.simulator.network.hardware.base import UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server from primaite.simulator.system.services.database.database_service import DatabaseService @@ -26,9 +26,9 @@ def game_and_agent_fixture(game_and_agent): game, agent = game_and_agent router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=4) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=5) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=6) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=4) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["DNS"], dst_port=PORT_LOOKUP["DNS"], position=5) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["FTP"], dst_port=PORT_LOOKUP["FTP"], position=6) c2_server_host = game.simulation.network.get_node_by_hostname("client_1") c2_server_host.software_manager.install(software_class=C2Server) diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 34ee25d6..b56a4b99 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -11,7 +11,7 @@ from primaite.game.agent.actions import ( ) from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot @@ -200,7 +200,7 @@ class TestConfigureDoSBot: game.step() assert dos_bot.target_ip_address == IPv4Address("192.168.1.99") - assert dos_bot.target_port == Port["POSTGRES_SERVER"] + assert dos_bot.target_port == PORT_LOOKUP["POSTGRES_SERVER"] assert dos_bot.payload == "HACC" assert not dos_bot.repeat assert dos_bot.port_scan_p_of_success == 0.875 diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index 857edd26..bc168c3c 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -9,7 +9,7 @@ from primaite.simulator.network.hardware.base import UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection @@ -20,7 +20,7 @@ def game_and_agent_fixture(game_and_agent): game, agent = game_and_agent router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=4) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=4) return (game, agent) diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index 28f9ac5a..2bf0486c 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -4,7 +4,7 @@ import pytest from primaite.game.agent.observations.acl_observation import ACLObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer @@ -33,7 +33,7 @@ def test_acl_observations(simulation): server.software_manager.install(NTPServer) # add router acl rule - router.acl.add_rule(action=ACLAction.PERMIT, dst_port=Port["NTP"], src_port=Port["NTP"], position=1) + router.acl.add_rule(action=ACLAction.PERMIT, dst_port=PORT_LOOKUP["NTP"], src_port=PORT_LOOKUP["NTP"], position=1) acl_obs = ACLObservation( where=["network", "nodes", router.hostname, "acl", "acl"], diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 21fe4bed..af8c4669 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -5,8 +5,8 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP def check_default_rules(acl_obs): @@ -62,13 +62,13 @@ def test_firewall_observation(): # add a rule to the internal inbound and check that the observation is correct firewall.internal_inbound_acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], src_ip_address="10.0.0.1", src_wildcard_mask="0.0.0.1", dst_ip_address="10.0.0.2", dst_wildcard_mask="0.0.0.1", - src_port=Port["HTTP"], - dst_port=Port["HTTP"], + src_port=PORT_LOOKUP["HTTP"], + dst_port=PORT_LOOKUP["HTTP"], position=5, ) diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index c28e1bb8..cdd428b0 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -8,9 +8,9 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation +from primaite.utils.validators import PROTOCOL_LOOKUP def test_router_observation(): @@ -39,13 +39,13 @@ def test_router_observation(): # Add an ACL rule to the router router.acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], src_ip_address="10.0.0.1", src_wildcard_mask="0.0.0.1", dst_ip_address="10.0.0.2", dst_wildcard_mask="0.0.0.1", - src_port=Port["HTTP"], - dst_port=Port["HTTP"], + src_port=PORT_LOOKUP["HTTP"], + dst_port=PORT_LOOKUP["HTTP"], position=5, ) # Observe the state using the RouterObservation instance diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index 6ca4bc9e..70637b0d 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -3,7 +3,7 @@ import pytest from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from tests import TEST_ASSETS_ROOT DATA_MANIPULATION_CONFIG = TEST_ASSETS_ROOT / "configs" / "data_manipulation.yaml" @@ -15,7 +15,7 @@ def env_with_ssh() -> PrimaiteGymEnv: env = PrimaiteGymEnv(DATA_MANIPULATION_CONFIG) env.agent.flatten_obs = False router: Router = env.game.simulation.network.get_node_by_hostname("router_1") - router.acl.add_rule(ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=3) + router.acl.add_rule(ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=3) return env diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index c3e86263..2675b615 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -21,11 +21,11 @@ from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT FIREWALL_ACTIONS_NETWORK = TEST_ASSETS_ROOT / "configs/firewall_actions_network.yaml" @@ -608,9 +608,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.internal_outbound_acl.acl[1].action.name == "DENY" assert firewall.internal_outbound_acl.acl[1].src_ip_address == IPv4Address("192.168.0.10") assert firewall.internal_outbound_acl.acl[1].dst_ip_address is None - assert firewall.internal_outbound_acl.acl[1].dst_port == Port["DNS"] - assert firewall.internal_outbound_acl.acl[1].src_port == Port["ARP"] - assert firewall.internal_outbound_acl.acl[1].protocol == IPProtocol["ICMP"] + assert firewall.internal_outbound_acl.acl[1].dst_port == PORT_LOOKUP["DNS"] + assert firewall.internal_outbound_acl.acl[1].src_port == PORT_LOOKUP["ARP"] + assert firewall.internal_outbound_acl.acl[1].protocol == PROTOCOL_LOOKUP["ICMP"] env.step(4) # Remove ACL rule from Internal Outbound assert firewall.internal_outbound_acl.num_rules == 2 @@ -620,9 +620,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.dmz_inbound_acl.acl[1].action.name == "DENY" assert firewall.dmz_inbound_acl.acl[1].src_ip_address == IPv4Address("192.168.10.10") assert firewall.dmz_inbound_acl.acl[1].dst_ip_address == IPv4Address("192.168.0.10") - assert firewall.dmz_inbound_acl.acl[1].dst_port == Port["HTTP"] - assert firewall.dmz_inbound_acl.acl[1].src_port == Port["HTTP"] - assert firewall.dmz_inbound_acl.acl[1].protocol == IPProtocol["UDP"] + assert firewall.dmz_inbound_acl.acl[1].dst_port == PORT_LOOKUP["HTTP"] + assert firewall.dmz_inbound_acl.acl[1].src_port == PORT_LOOKUP["HTTP"] + assert firewall.dmz_inbound_acl.acl[1].protocol == PROTOCOL_LOOKUP["UDP"] env.step(6) # Remove ACL rule from DMZ Inbound assert firewall.dmz_inbound_acl.num_rules == 2 @@ -632,9 +632,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.dmz_outbound_acl.acl[2].action.name == "DENY" assert firewall.dmz_outbound_acl.acl[2].src_ip_address == IPv4Address("192.168.10.10") assert firewall.dmz_outbound_acl.acl[2].dst_ip_address == IPv4Address("192.168.0.10") - assert firewall.dmz_outbound_acl.acl[2].dst_port == Port["HTTP"] - assert firewall.dmz_outbound_acl.acl[2].src_port == Port["HTTP"] - assert firewall.dmz_outbound_acl.acl[2].protocol == IPProtocol["TCP"] + assert firewall.dmz_outbound_acl.acl[2].dst_port == PORT_LOOKUP["HTTP"] + assert firewall.dmz_outbound_acl.acl[2].src_port == PORT_LOOKUP["HTTP"] + assert firewall.dmz_outbound_acl.acl[2].protocol == PROTOCOL_LOOKUP["TCP"] env.step(8) # Remove ACL rule from DMZ Outbound assert firewall.dmz_outbound_acl.num_rules == 2 @@ -644,9 +644,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.external_inbound_acl.acl[10].action.name == "DENY" assert firewall.external_inbound_acl.acl[10].src_ip_address == IPv4Address("192.168.20.10") assert firewall.external_inbound_acl.acl[10].dst_ip_address == IPv4Address("192.168.10.10") - assert firewall.external_inbound_acl.acl[10].dst_port == Port["POSTGRES_SERVER"] - assert firewall.external_inbound_acl.acl[10].src_port == Port["POSTGRES_SERVER"] - assert firewall.external_inbound_acl.acl[10].protocol == IPProtocol["ICMP"] + assert firewall.external_inbound_acl.acl[10].dst_port == PORT_LOOKUP["POSTGRES_SERVER"] + assert firewall.external_inbound_acl.acl[10].src_port == PORT_LOOKUP["POSTGRES_SERVER"] + assert firewall.external_inbound_acl.acl[10].protocol == PROTOCOL_LOOKUP["ICMP"] env.step(10) # Remove ACL rule from External Inbound assert firewall.external_inbound_acl.num_rules == 1 diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 570c4ad6..0afe666c 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -9,11 +9,11 @@ from primaite.interface.request import RequestResponse from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT from tests.conftest import ControlledAgent @@ -42,7 +42,12 @@ def test_WebpageUnavailablePenalty(game_and_agent): # Block the web traffic, check that failing to fetch the webpage yields a reward of -0.7 router: Router = game.simulation.network.get_node_by_hostname("router") - router.acl.add_rule(action=ACLAction.DENY, protocol=IPProtocol["TCP"], src_port=Port["HTTP"], dst_port=Port["HTTP"]) + router.acl.add_rule( + action=ACLAction.DENY, + protocol=PROTOCOL_LOOKUP["TCP"], + src_port=PORT_LOOKUP["HTTP"], + dst_port=PORT_LOOKUP["HTTP"], + ) agent.store_action(("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0})) game.step() assert agent.reward_function.current_reward == -0.7 @@ -66,7 +71,7 @@ def test_uc2_rewards(game_and_agent): router: Router = game.simulation.network.get_node_by_hostname("router") router.acl.add_rule( - ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=2 + ACLAction.PERMIT, src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"], position=2 ) comp = GreenAdminDatabaseUnreachablePenalty("client_1") diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index da0af89d..b5b2acbc 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -8,10 +8,10 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.service import Service +from primaite.utils.validators import PROTOCOL_LOOKUP class BroadcastTestService(Service): @@ -20,8 +20,8 @@ class BroadcastTestService(Service): def __init__(self, **kwargs): # Set default service properties for broadcasting kwargs["name"] = "BroadcastService" - kwargs["port"] = Port["HTTP"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: @@ -33,12 +33,14 @@ class BroadcastTestService(Service): super().send( payload="unicast", dest_ip_address=ip_address, - dest_port=Port["HTTP"], + dest_port=PORT_LOOKUP["HTTP"], ) def broadcast(self, ip_network: IPv4Network): # Send a broadcast payload to an entire IP network - super().send(payload="broadcast", dest_ip_address=ip_network, dest_port=Port["HTTP"], ip_protocol=self.protocol) + super().send( + payload="broadcast", dest_ip_address=ip_network, dest_port=PORT_LOOKUP["HTTP"], ip_protocol=self.protocol + ) class BroadcastTestClient(Application, identifier="BroadcastTestClient"): @@ -49,8 +51,8 @@ class BroadcastTestClient(Application, identifier="BroadcastTestClient"): def __init__(self, **kwargs): # Set default client properties kwargs["name"] = "BroadcastTestClient" - kwargs["port"] = Port["HTTP"] - kwargs["protocol"] = IPProtocol["TCP"] + kwargs["port"] = PORT_LOOKUP["HTTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) def describe_state(self) -> Dict: diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 44b660cf..58763c3e 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -7,10 +7,10 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -53,31 +53,31 @@ def dmz_external_internal_network() -> Network: ) # Allow ICMP - firewall_node.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) - firewall_node.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) - firewall_node.external_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) - firewall_node.external_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) - firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) - firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + firewall_node.internal_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + firewall_node.internal_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + firewall_node.external_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + firewall_node.external_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + firewall_node.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + firewall_node.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Allow ARP firewall_node.internal_inbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) firewall_node.internal_outbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) firewall_node.external_inbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) firewall_node.external_outbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) firewall_node.dmz_inbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) firewall_node.dmz_outbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) # external node @@ -267,10 +267,10 @@ def test_service_allowed_with_rule(dmz_external_internal_network): assert not internal_ntp_client.time firewall.internal_outbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"], position=1 ) firewall.internal_inbound_acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1 + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"], position=1 ) internal_ntp_client.request_time() @@ -279,8 +279,12 @@ def test_service_allowed_with_rule(dmz_external_internal_network): assert not dmz_ntp_client.time - firewall.dmz_outbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) - firewall.dmz_inbound_acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=1) + firewall.dmz_outbound_acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"], position=1 + ) + firewall.dmz_inbound_acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"], position=1 + ) dmz_ntp_client.request_time() diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 641342e2..dde66a43 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -6,10 +6,10 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -73,8 +73,10 @@ def multi_hop_network() -> Network: router_1.enable_port(2) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Configure PC B pc_b = Computer( @@ -197,8 +199,12 @@ def test_routing_services(multi_hop_network): router_1: Router = multi_hop_network.get_node_by_hostname("router_1") # noqa router_2: Router = multi_hop_network.get_node_by_hostname("router_2") # noqa - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=21) - router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["NTP"], dst_port=Port["NTP"], position=21) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"], position=21 + ) + router_2.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"], position=21 + ) assert ntp_client.time is None ntp_client.request_time() diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index 2f1be930..520ec21a 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -7,8 +7,8 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT @@ -37,8 +37,10 @@ def wireless_wan_network(): network.connect(pc_a.network_interface[1], router_1.network_interface[2]) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Configure PC B pc_b = Computer( diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index d819b511..b1979154 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -13,8 +13,7 @@ from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import AccessControlList, ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon @@ -25,6 +24,7 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer 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.web_server.web_server import WebServer +from primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT @@ -227,7 +227,7 @@ def test_c2_suite_acl_block(basic_network): assert c2_beacon.c2_connection_active == True # Now we add a HTTP blocking acl (Thus preventing a keep alive) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=0) c2_beacon.apply_timestep(2) c2_beacon.apply_timestep(3) @@ -322,8 +322,8 @@ def test_c2_suite_acl_bypass(basic_network): ################ Confirm Default Setup ######################### # Permitting all HTTP & FTP traffic - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=1) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=0) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["FTP"], dst_port=PORT_LOOKUP["FTP"], position=1) c2_beacon.apply_timestep(0) assert c2_beacon.keep_alive_inactivity == 1 @@ -337,7 +337,7 @@ def test_c2_suite_acl_bypass(basic_network): ################ Denying HTTP Traffic ######################### # Now we add a HTTP blocking acl (Thus preventing a keep alive) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=0) blocking_acl: AccessControlList = router.acl.acl[0] # Asserts to show the C2 Suite is unable to maintain connection: @@ -359,8 +359,8 @@ def test_c2_suite_acl_bypass(basic_network): c2_beacon.configure( c2_server_ip_address="192.168.0.2", keep_alive_frequency=2, - masquerade_port=Port["FTP"], - masquerade_protocol=IPProtocol["TCP"], + masquerade_port=PORT_LOOKUP["FTP"], + masquerade_protocol=PROTOCOL_LOOKUP["TCP"], ) c2_beacon.establish() @@ -407,8 +407,8 @@ def test_c2_suite_acl_bypass(basic_network): ################ Denying FTP Traffic & Enable HTTP ######################### # Blocking FTP and re-permitting HTTP: - router.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) - router.acl.add_rule(action=ACLAction.DENY, src_port=Port["FTP"], dst_port=Port["FTP"], position=1) + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=0) + router.acl.add_rule(action=ACLAction.DENY, src_port=PORT_LOOKUP["FTP"], dst_port=PORT_LOOKUP["FTP"], position=1) blocking_acl: AccessControlList = router.acl.acl[1] # Asserts to show the C2 Suite is unable to maintain connection: @@ -430,8 +430,8 @@ def test_c2_suite_acl_bypass(basic_network): c2_beacon.configure( c2_server_ip_address="192.168.0.2", keep_alive_frequency=2, - masquerade_port=Port["HTTP"], - masquerade_protocol=IPProtocol["TCP"], + masquerade_port=PORT_LOOKUP["HTTP"], + masquerade_protocol=PROTOCOL_LOOKUP["TCP"], ) c2_beacon.establish() diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 9c0760b7..54c372e4 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -9,7 +9,7 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( @@ -52,7 +52,10 @@ def data_manipulation_db_server_green_client(example_network) -> Network: router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 + action=ACLAction.PERMIT, + src_port=PORT_LOOKUP["POSTGRES_SERVER"], + dst_port=PORT_LOOKUP["POSTGRES_SERVER"], + position=0, ) client_1: Computer = network.get_node_by_hostname("client_1") diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index 709a417f..ad0a519b 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -8,7 +8,7 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot @@ -26,7 +26,7 @@ def dos_bot_and_db_server(client_server) -> Tuple[DoSBot, Computer, DatabaseServ dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") dos_bot.configure( target_ip_address=IPv4Address(server.network_interface[1].ip_address), - target_port=Port["POSTGRES_SERVER"], + target_port=PORT_LOOKUP["POSTGRES_SERVER"], ) # Install DB Server service on server @@ -43,7 +43,10 @@ def dos_bot_db_server_green_client(example_network) -> Network: router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 + action=ACLAction.PERMIT, + src_port=PORT_LOOKUP["POSTGRES_SERVER"], + dst_port=PORT_LOOKUP["POSTGRES_SERVER"], + position=0, ) client_1: Computer = network.get_node_by_hostname("client_1") @@ -56,7 +59,7 @@ def dos_bot_db_server_green_client(example_network) -> Network: dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot") dos_bot.configure( target_ip_address=IPv4Address(server.network_interface[1].ip_address), - target_port=Port["POSTGRES_SERVER"], + target_port=PORT_LOOKUP["POSTGRES_SERVER"], ) # install db server service on server diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index b34e9b30..09cbcf85 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -9,7 +9,7 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.database.database_service import DatabaseService @@ -47,7 +47,10 @@ def ransomware_script_db_server_green_client(example_network) -> Network: router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 + action=ACLAction.PERMIT, + src_port=PORT_LOOKUP["POSTGRES_SERVER"], + dst_port=PORT_LOOKUP["POSTGRES_SERVER"], + position=0, ) client_1: Computer = network.get_node_by_hostname("client_1") diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index 9d92b660..c1c4df82 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -5,9 +5,9 @@ from ipaddress import IPv4Address, IPv4Network import yaml from primaite.game.game import PrimaiteGame -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.nmap import NMAP +from primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT @@ -74,11 +74,11 @@ def test_port_scan_one_node_one_port(example_network): actual_result = client_1_nmap.port_scan( target_ip_address=client_2.network_interface[1].ip_address, - target_port=Port["DNS"], - target_protocol=IPProtocol["TCP"], + target_port=PORT_LOOKUP["DNS"], + target_protocol=PROTOCOL_LOOKUP["TCP"], ) - expected_result = {IPv4Address("192.168.10.22"): {IPProtocol["TCP"]: [Port["DNS"]]}} + expected_result = {IPv4Address("192.168.10.22"): {PROTOCOL_LOOKUP["TCP"]: [PORT_LOOKUP["DNS"]]}} assert actual_result == expected_result @@ -103,14 +103,20 @@ def test_port_scan_full_subnet_all_ports_and_protocols(example_network): actual_result = client_1_nmap.port_scan( target_ip_address=IPv4Network("192.168.10.0/24"), - target_port=[Port["ARP"], Port["HTTP"], Port["FTP"], Port["DNS"], Port["NTP"]], + target_port=[ + PORT_LOOKUP["ARP"], + PORT_LOOKUP["HTTP"], + PORT_LOOKUP["FTP"], + PORT_LOOKUP["DNS"], + PORT_LOOKUP["NTP"], + ], ) expected_result = { - IPv4Address("192.168.10.1"): {IPProtocol["UDP"]: [Port["ARP"]]}, + IPv4Address("192.168.10.1"): {PROTOCOL_LOOKUP["UDP"]: [PORT_LOOKUP["ARP"]]}, IPv4Address("192.168.10.22"): { - IPProtocol["TCP"]: [Port["HTTP"], Port["FTP"], Port["DNS"]], - IPProtocol["UDP"]: [Port["ARP"], Port["NTP"]], + PROTOCOL_LOOKUP["TCP"]: [PORT_LOOKUP["HTTP"], PORT_LOOKUP["FTP"], PORT_LOOKUP["DNS"]], + PROTOCOL_LOOKUP["UDP"]: [PORT_LOOKUP["ARP"], PORT_LOOKUP["NTP"]], }, } @@ -124,10 +130,12 @@ def test_network_service_recon_all_ports_and_protocols(example_network): client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa actual_result = client_1_nmap.network_service_recon( - target_ip_address=IPv4Network("192.168.10.0/24"), target_port=Port["HTTP"], target_protocol=IPProtocol["TCP"] + target_ip_address=IPv4Network("192.168.10.0/24"), + target_port=PORT_LOOKUP["HTTP"], + target_protocol=PROTOCOL_LOOKUP["TCP"], ) - expected_result = {IPv4Address("192.168.10.22"): {IPProtocol["TCP"]: [Port["HTTP"]]}} + expected_result = {IPv4Address("192.168.10.22"): {PROTOCOL_LOOKUP["TCP"]: [PORT_LOOKUP["HTTP"]]}} assert sort_dict(actual_result) == sort_dict(expected_result) 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 5226ab4a..4108041d 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -6,19 +6,19 @@ 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.network.transmission.transport_layer import PORT_LOOKUP 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 primaite.utils.validators import PROTOCOL_LOOKUP from tests import TEST_ASSETS_ROOT class _DatabaseListener(Service): name: str = "DatabaseListener" - protocol: str = IPProtocol["TCP"] - port: int = Port["NONE"] - listen_on_ports: Set[int] = {Port["POSTGRES_SERVER"]} + protocol: str = PROTOCOL_LOOKUP["TCP"] + port: int = PORT_LOOKUP["NONE"] + listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} payloads_received: List[Any] = Field(default_factory=list) def receive(self, payload: Any, session_id: str, **kwargs) -> bool: @@ -51,8 +51,8 @@ def test_http_listener(client_server): computer.session_manager.receive_payload_from_software_manager( payload="masquerade as Database traffic", dst_ip_address=server.network_interface[1].ip_address, - dst_port=Port["POSTGRES_SERVER"], - ip_protocol=IPProtocol["TCP"], + dst_port=PORT_LOOKUP["POSTGRES_SERVER"], + ip_protocol=PROTOCOL_LOOKUP["TCP"], ) assert len(server_db_listener.payloads_received) == 1 @@ -76,9 +76,9 @@ def test_set_listen_on_ports_from_config(): 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() + assert PORT_LOOKUP["SMB"] in client.software_manager.get_open_ports() + assert PORT_LOOKUP["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"]}) + assert not web_browser.listen_on_ports.difference({PORT_LOOKUP["SMB"], PORT_LOOKUP["IPP"]}) diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 6c37360f..854ef41b 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -9,7 +9,7 @@ from primaite.simulator.network.hardware.base import Link from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService @@ -24,17 +24,22 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer, # add rules to network router router_1: Router = example_network.get_node_by_hostname("router_1") router_1.acl.add_rule( - action=ACLAction.PERMIT, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"], position=0 + action=ACLAction.PERMIT, + src_port=PORT_LOOKUP["POSTGRES_SERVER"], + dst_port=PORT_LOOKUP["POSTGRES_SERVER"], + position=0, ) # Allow DNS requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["DNS"], dst_port=Port["DNS"], position=1) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["DNS"], dst_port=PORT_LOOKUP["DNS"], position=1) # Allow FTP requests - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["FTP"], dst_port=Port["FTP"], position=2) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["FTP"], dst_port=PORT_LOOKUP["FTP"], position=2) # Open port 80 for web server - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=3 + ) # Create Computer computer: Computer = example_network.get_node_by_hostname("client_1") @@ -148,7 +153,9 @@ class TestWebBrowserHistory: assert web_browser.history[-1].response_code == 200 router = network.get_node_by_hostname("router_1") - router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) + router.acl.add_rule( + action=ACLAction.DENY, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=0 + ) assert not web_browser.get_webpage() assert len(web_browser.history) == 3 # with current NIC behaviour, even if you block communication, you won't get SERVER_UNREACHABLE because @@ -166,7 +173,9 @@ class TestWebBrowserHistory: web_browser.get_webpage() router = network.get_node_by_hostname("router_1") - router.acl.add_rule(action=ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=0) + router.acl.add_rule( + action=ACLAction.DENY, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=0 + ) web_browser.get_webpage() state = computer.describe_state() diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index ff73e621..7813628c 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -12,7 +12,7 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from tests.conftest import DummyApplication, DummyService @@ -171,7 +171,7 @@ class TestDataManipulationGreenRequests: assert client_1_browser_execute.status == "success" assert client_2_browser_execute.status == "success" - router.acl.add_rule(ACLAction.DENY, src_port=Port["HTTP"], dst_port=Port["HTTP"], position=3) + router.acl.add_rule(ACLAction.DENY, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=3) client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) assert client_1_browser_execute.status == "failure" @@ -182,7 +182,9 @@ class TestDataManipulationGreenRequests: assert client_1_db_client_execute.status == "success" assert client_2_db_client_execute.status == "success" - router.acl.add_rule(ACLAction.DENY, src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"]) + router.acl.add_rule( + ACLAction.DENY, src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"] + ) client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) assert client_1_db_client_execute.status == "failure" diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 4c471faa..ba7628c2 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -7,8 +7,9 @@ from primaite.simulator.network.hardware.base import generate_mac_address from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame -from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader, UDPHeader +from primaite.simulator.network.transmission.network_layer import IPPacket +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPHeader, UDPHeader +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -28,16 +29,16 @@ def router_with_acl_rules(): # Add rules here as needed acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], src_ip_address="192.168.1.1", - src_port=Port["HTTPS"], + src_port=PORT_LOOKUP["HTTPS"], dst_ip_address="192.168.1.2", - dst_port=Port["HTTP"], + dst_port=PORT_LOOKUP["HTTP"], position=1, ) acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], src_ip_address="192.168.1.3", src_port=8080, dst_ip_address="192.168.1.4", @@ -65,7 +66,7 @@ def router_with_wildcard_acl(): # Rule to permit traffic from a specific source IP and port to a specific destination IP and port acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], src_ip_address="192.168.1.1", src_port=8080, dst_ip_address="10.1.1.2", @@ -75,7 +76,7 @@ def router_with_wildcard_acl(): # Rule to deny traffic from an IP range to a specific destination IP and port acl.add_rule( action=ACLAction.DENY, - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], src_ip_address="192.168.1.0", src_wildcard_mask="0.0.0.255", dst_ip_address="10.1.1.3", @@ -109,11 +110,11 @@ def test_add_rule(router_with_acl_rules): acl = router_with_acl_rules.acl assert acl.acl[1].action == ACLAction.PERMIT - assert acl.acl[1].protocol == IPProtocol["TCP"] + assert acl.acl[1].protocol == PROTOCOL_LOOKUP["TCP"] assert acl.acl[1].src_ip_address == IPv4Address("192.168.1.1") - assert acl.acl[1].src_port == Port["HTTPS"] + assert acl.acl[1].src_port == PORT_LOOKUP["HTTPS"] assert acl.acl[1].dst_ip_address == IPv4Address("192.168.1.2") - assert acl.acl[1].dst_port == Port["HTTP"] + assert acl.acl[1].dst_port == PORT_LOOKUP["HTTP"] def test_remove_rule(router_with_acl_rules): @@ -136,8 +137,8 @@ def test_traffic_permitted_by_specific_rule(router_with_acl_rules): acl = router_with_acl_rules.acl permitted_frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="192.168.1.2", protocol=IPProtocol["TCP"]), - tcp=TCPHeader(src_port=Port["HTTPS"], dst_port=Port["HTTP"]), + ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="192.168.1.2", protocol=PROTOCOL_LOOKUP["TCP"]), + tcp=TCPHeader(src_port=PORT_LOOKUP["HTTPS"], dst_port=PORT_LOOKUP["HTTP"]), ) is_permitted, _ = acl.is_permitted(permitted_frame) assert is_permitted @@ -153,7 +154,7 @@ def test_traffic_denied_by_specific_rule(router_with_acl_rules): acl = router_with_acl_rules.acl not_permitted_frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.3", dst_ip_address="192.168.1.4", protocol=IPProtocol["TCP"]), + ip=IPPacket(src_ip_address="192.168.1.3", dst_ip_address="192.168.1.4", protocol=PROTOCOL_LOOKUP["TCP"]), tcp=TCPHeader(src_port=8080, dst_port=80), ) is_permitted, _ = acl.is_permitted(not_permitted_frame) @@ -173,8 +174,8 @@ def test_default_rule(router_with_acl_rules): acl = router_with_acl_rules.acl not_permitted_frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.5", dst_ip_address="192.168.1.12", protocol=IPProtocol["UDP"]), - udp=UDPHeader(src_port=Port["HTTPS"], dst_port=Port["HTTP"]), + ip=IPPacket(src_ip_address="192.168.1.5", dst_ip_address="192.168.1.12", protocol=PROTOCOL_LOOKUP["UDP"]), + udp=UDPHeader(src_port=PORT_LOOKUP["HTTPS"], dst_port=PORT_LOOKUP["HTTP"]), ) is_permitted, rule = acl.is_permitted(not_permitted_frame) assert not is_permitted @@ -189,7 +190,7 @@ def test_direct_ip_match_with_acl(router_with_wildcard_acl): acl = router_with_wildcard_acl.acl frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="10.1.1.2", protocol=IPProtocol["TCP"]), + ip=IPPacket(src_ip_address="192.168.1.1", dst_ip_address="10.1.1.2", protocol=PROTOCOL_LOOKUP["TCP"]), tcp=TCPHeader(src_port=8080, dst_port=80), ) assert acl.is_permitted(frame)[0], "Direct IP match should be permitted." @@ -204,7 +205,7 @@ def test_ip_range_match_denied_with_acl(router_with_wildcard_acl): acl = router_with_wildcard_acl.acl frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.100", dst_ip_address="10.1.1.3", protocol=IPProtocol["TCP"]), + ip=IPPacket(src_ip_address="192.168.1.100", dst_ip_address="10.1.1.3", protocol=PROTOCOL_LOOKUP["TCP"]), tcp=TCPHeader(src_port=8080, dst_port=443), ) assert not acl.is_permitted(frame)[0], "IP range match with wildcard mask should be denied." @@ -219,7 +220,7 @@ def test_traffic_permitted_to_destination_range_with_acl(router_with_wildcard_ac acl = router_with_wildcard_acl.acl frame = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=IPProtocol["UDP"]), + ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=PROTOCOL_LOOKUP["UDP"]), udp=UDPHeader(src_port=1433, dst_port=1433), ) assert acl.is_permitted(frame)[0], "Traffic to destination IP range should be permitted." @@ -253,23 +254,23 @@ def test_ip_traffic_from_specific_subnet(): permitted_frame_1 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=IPProtocol["TCP"]), - tcp=TCPHeader(src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"]), + ip=IPPacket(src_ip_address="192.168.1.50", dst_ip_address="10.2.200.200", protocol=PROTOCOL_LOOKUP["TCP"]), + tcp=TCPHeader(src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"]), ) assert acl.is_permitted(permitted_frame_1)[0] permitted_frame_2 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.10", dst_ip_address="85.199.214.101", protocol=IPProtocol["UDP"]), - udp=UDPHeader(src_port=Port["NTP"], dst_port=Port["NTP"]), + ip=IPPacket(src_ip_address="192.168.1.10", dst_ip_address="85.199.214.101", protocol=PROTOCOL_LOOKUP["UDP"]), + udp=UDPHeader(src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"]), ) assert acl.is_permitted(permitted_frame_2)[0] permitted_frame_3 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.1.200", dst_ip_address="192.168.1.1", protocol=IPProtocol["ICMP"]), + ip=IPPacket(src_ip_address="192.168.1.200", dst_ip_address="192.168.1.1", protocol=PROTOCOL_LOOKUP["ICMP"]), icmp=ICMPPacket(identifier=1), ) @@ -277,16 +278,16 @@ def test_ip_traffic_from_specific_subnet(): not_permitted_frame_1 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.0.50", dst_ip_address="10.2.200.200", protocol=IPProtocol["TCP"]), - tcp=TCPHeader(src_port=Port["POSTGRES_SERVER"], dst_port=Port["POSTGRES_SERVER"]), + ip=IPPacket(src_ip_address="192.168.0.50", dst_ip_address="10.2.200.200", protocol=PROTOCOL_LOOKUP["TCP"]), + tcp=TCPHeader(src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"]), ) assert not acl.is_permitted(not_permitted_frame_1)[0] not_permitted_frame_2 = Frame( ethernet=EthernetHeader(src_mac_addr=generate_mac_address(), dst_mac_addr=generate_mac_address()), - ip=IPPacket(src_ip_address="192.168.2.10", dst_ip_address="85.199.214.101", protocol=IPProtocol["UDP"]), - udp=UDPHeader(src_port=Port["NTP"], dst_port=Port["NTP"]), + ip=IPPacket(src_ip_address="192.168.2.10", dst_ip_address="85.199.214.101", protocol=PROTOCOL_LOOKUP["UDP"]), + udp=UDPHeader(src_port=PORT_LOOKUP["NTP"], dst_port=PORT_LOOKUP["NTP"]), ) assert not acl.is_permitted(not_permitted_frame_2)[0] diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index 3551ce38..0e1844c4 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -2,8 +2,8 @@ from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validators import PROTOCOL_LOOKUP def test_wireless_router_from_config(): @@ -67,12 +67,12 @@ def test_wireless_router_from_config(): r0 = rt.acl.acl[0] assert r0.action == ACLAction.PERMIT - assert r0.src_port == r0.dst_port == Port["POSTGRES_SERVER"] + assert r0.src_port == r0.dst_port == PORT_LOOKUP["POSTGRES_SERVER"] assert r0.src_ip_address == r0.dst_ip_address == r0.dst_wildcard_mask == r0.src_wildcard_mask == r0.protocol == None r1 = rt.acl.acl[1] assert r1.action == ACLAction.PERMIT - assert r1.protocol == IPProtocol["ICMP"] + assert r1.protocol == PROTOCOL_LOOKUP["ICMP"] assert ( r1.src_ip_address == r1.dst_ip_address diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py index 9fd39dfc..9e9a1f72 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py @@ -3,9 +3,10 @@ import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame -from primaite.simulator.network.transmission.network_layer import IPPacket, IPProtocol, Precedence +from primaite.simulator.network.transmission.network_layer import IPPacket, Precedence from primaite.simulator.network.transmission.primaite_layer import AgentSource, DataStatus -from primaite.simulator.network.transmission.transport_layer import Port, TCPFlags, TCPHeader, UDPHeader +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPFlags, TCPHeader, UDPHeader +from primaite.utils.validators import PROTOCOL_LOOKUP def test_frame_minimal_instantiation(): @@ -20,7 +21,7 @@ def test_frame_minimal_instantiation(): ) # Check network layer default values - assert frame.ip.protocol == IPProtocol["TCP"] + assert frame.ip.protocol == PROTOCOL_LOOKUP["TCP"] assert frame.ip.ttl == 64 assert frame.ip.precedence == Precedence.ROUTINE @@ -40,7 +41,7 @@ def test_frame_creation_fails_tcp_without_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["TCP"]), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=PROTOCOL_LOOKUP["TCP"]), ) @@ -49,7 +50,7 @@ def test_frame_creation_fails_udp_without_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["UDP"]), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=PROTOCOL_LOOKUP["UDP"]), ) @@ -58,7 +59,7 @@ def test_frame_creation_fails_tcp_with_udp_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["TCP"]), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=PROTOCOL_LOOKUP["TCP"]), udp=UDPHeader(src_port=8080, dst_port=80), ) @@ -68,7 +69,7 @@ def test_frame_creation_fails_udp_with_tcp_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["UDP"]), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=PROTOCOL_LOOKUP["UDP"]), udp=TCPHeader(src_port=8080, dst_port=80), ) @@ -77,7 +78,7 @@ def test_icmp_frame_creation(): """Tests Frame creation for ICMP.""" frame = Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["ICMP"]), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=PROTOCOL_LOOKUP["ICMP"]), icmp=ICMPPacket(), ) assert frame @@ -88,5 +89,5 @@ def test_icmp_frame_creation_fails_without_icmp_header(): with pytest.raises(ValueError): Frame( ethernet=EthernetHeader(src_mac_addr="aa:bb:cc:dd:ee:ff", dst_mac_addr="11:22:33:44:55:66"), - ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=IPProtocol["ICMP"]), + ip=IPPacket(src_ip_address="192.168.0.10", dst_ip_address="192.168.0.20", protocol=PROTOCOL_LOOKUP["ICMP"]), ) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 6e53aebc..fde70616 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -4,11 +4,11 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -129,19 +129,19 @@ def test_c2_handle_switching_port(basic_c2_network): # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. assert c2_beacon.c2_config.keep_alive_frequency is 2 - assert c2_beacon.c2_config.masquerade_port is Port["HTTP"] - assert c2_beacon.c2_config.masquerade_protocol is IPProtocol["TCP"] + assert c2_beacon.c2_config.masquerade_port is PORT_LOOKUP["HTTP"] + assert c2_beacon.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] assert c2_server.c2_config.keep_alive_frequency is 2 - assert c2_server.c2_config.masquerade_port is Port["HTTP"] - assert c2_server.c2_config.masquerade_protocol is IPProtocol["TCP"] + assert c2_server.c2_config.masquerade_port is PORT_LOOKUP["HTTP"] + assert c2_server.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] # Configuring the C2 Beacon. c2_beacon.configure( c2_server_ip_address="192.168.0.1", keep_alive_frequency=2, - masquerade_port=Port["FTP"], - masquerade_protocol=IPProtocol["TCP"], + masquerade_port=PORT_LOOKUP["FTP"], + masquerade_protocol=PROTOCOL_LOOKUP["TCP"], ) # Asserting that the c2 applications have established a c2 connection @@ -150,11 +150,11 @@ def test_c2_handle_switching_port(basic_c2_network): # Assert to confirm that both the C2 server and the C2 beacon # Have reconfigured their C2 settings. - assert c2_beacon.c2_config.masquerade_port is Port["FTP"] - assert c2_beacon.c2_config.masquerade_protocol is IPProtocol["TCP"] + assert c2_beacon.c2_config.masquerade_port is PORT_LOOKUP["FTP"] + assert c2_beacon.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] - assert c2_server.c2_config.masquerade_port is Port["FTP"] - assert c2_server.c2_config.masquerade_protocol is IPProtocol["TCP"] + assert c2_server.c2_config.masquerade_port is PORT_LOOKUP["FTP"] + assert c2_server.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] def test_c2_handle_switching_frequency(basic_c2_network): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index 229f98fe..f4750158 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -3,13 +3,13 @@ import pytest from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.networks import arcd_uc2_network -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( DataManipulationAttackStage, DataManipulationBot, ) +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -27,8 +27,8 @@ def test_create_dm_bot(dm_client): data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software.get("DataManipulationBot") assert data_manipulation_bot.name == "DataManipulationBot" - assert data_manipulation_bot.port == Port["NONE"] - assert data_manipulation_bot.protocol == IPProtocol["NONE"] + assert data_manipulation_bot.port == PORT_LOOKUP["NONE"] + assert data_manipulation_bot.protocol == PROTOCOL_LOOKUP["NONE"] assert data_manipulation_bot.payload == "DELETE" diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index 2acd991a..d0c65266 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -5,7 +5,7 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index c274c18e..f5781485 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -4,10 +4,10 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.http import HttpResponsePacket, HttpStatusCode -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -39,8 +39,8 @@ def test_create_web_client(): # Web Browser should be pre-installed in computer web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") assert web_browser.name is "WebBrowser" - assert web_browser.port is Port["HTTP"] - assert web_browser.protocol is IPProtocol["TCP"] + assert web_browser.port is PORT_LOOKUP["HTTP"] + assert web_browser.protocol is PROTOCOL_LOOKUP["TCP"] def test_receive_invalid_payload(web_browser): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 1a51708d..09099c5c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -6,10 +6,10 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -28,8 +28,8 @@ def test_create_dns_client(dns_client): assert dns_client is not None dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") assert dns_client_service.name is "DNSClient" - assert dns_client_service.port is Port["DNS"] - assert dns_client_service.protocol is IPProtocol["TCP"] + assert dns_client_service.port is PORT_LOOKUP["DNS"] + assert dns_client_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_dns_client_add_domain_to_cache_when_not_running(dns_client): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 8cdb1b84..688bfd7d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -8,10 +8,10 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -32,8 +32,8 @@ def test_create_dns_server(dns_server): assert dns_server is not None dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") assert dns_server_service.name is "DNSServer" - assert dns_server_service.port is Port["DNS"] - assert dns_server_service.protocol is IPProtocol["TCP"] + assert dns_server_service.port is PORT_LOOKUP["DNS"] + assert dns_server_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_dns_server_domain_name_registration(dns_server): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 3c1afb28..b4fe8633 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -8,10 +8,10 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -31,8 +31,8 @@ def test_create_ftp_client(ftp_client): assert ftp_client is not None ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") assert ftp_client_service.name is "FTPClient" - assert ftp_client_service.port is Port["FTP"] - assert ftp_client_service.protocol is IPProtocol["TCP"] + assert ftp_client_service.port is PORT_LOOKUP["FTP"] + assert ftp_client_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_ftp_client_store_file(ftp_client): @@ -61,7 +61,7 @@ def test_ftp_should_not_process_commands_if_service_not_running(ftp_client): """Method _process_ftp_command should return false if service is not running.""" payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, - ftp_command_args=Port["FTP"], + ftp_command_args=PORT_LOOKUP["FTP"], status_code=FTPStatusCode.OK, ) @@ -102,7 +102,7 @@ def test_offline_ftp_client_receives_request(ftp_client): payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, - ftp_command_args=Port["FTP"], + ftp_command_args=PORT_LOOKUP["FTP"], status_code=FTPStatusCode.OK, ) @@ -119,7 +119,7 @@ def test_receive_should_ignore_payload_with_none_status_code(ftp_client): """Receive should ignore payload with no set status code to prevent infinite send/receive loops.""" payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.PORT, - ftp_command_args=Port["FTP"], + ftp_command_args=PORT_LOOKUP["FTP"], status_code=None, ) ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index aa13ec5e..3f10db4d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -6,10 +6,10 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -30,8 +30,8 @@ def test_create_ftp_server(ftp_server): assert ftp_server is not None ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") assert ftp_server_service.name is "FTPServer" - assert ftp_server_service.port is Port["FTP"] - assert ftp_server_service.protocol is IPProtocol["TCP"] + assert ftp_server_service.port is PORT_LOOKUP["FTP"] + assert ftp_server_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_ftp_server_store_file(ftp_server): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 21ed839b..f2895091 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -18,13 +18,13 @@ from primaite.simulator.network.protocols.ssh import ( SSHTransportMessage, SSHUserCredentials, ) -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection, Terminal from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -77,11 +77,15 @@ def wireless_wan_network(): network.connect(pc_a.network_interface[1], router_1.network_interface[2]) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # add ACL rule to allow SSH traffic - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=21) + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=21 + ) # Configure PC B pc_b = Computer( @@ -329,7 +333,9 @@ def test_SSH_across_network(wireless_wan_network): terminal_a: Terminal = pc_a.software_manager.software.get("Terminal") terminal_b: Terminal = pc_b.software_manager.software.get("Terminal") - router_2.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["SSH"], dst_port=Port["SSH"], position=21) + router_2.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=21 + ) assert len(terminal_a._connections) == 0 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index c1df3857..c78a381e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -9,9 +9,9 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.web_server.web_server import WebServer +from primaite.utils.validators import PROTOCOL_LOOKUP @pytest.fixture(scope="function") @@ -33,8 +33,8 @@ def test_create_web_server(web_server): assert web_server is not None web_server_service: WebServer = web_server.software_manager.software.get("WebServer") assert web_server_service.name is "WebServer" - assert web_server_service.port is Port["HTTP"] - assert web_server_service.protocol is IPProtocol["TCP"] + assert web_server_service.port is PORT_LOOKUP["HTTP"] + assert web_server_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_handling_get_request_not_found_path(web_server): diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index b7a663af..1baaf88e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -3,11 +3,11 @@ from typing import Dict import pytest -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.service import Service from primaite.simulator.system.software import IOSoftware, SoftwareHealthState +from primaite.utils.validators import PROTOCOL_LOOKUP class TestSoftware(Service): @@ -19,10 +19,10 @@ class TestSoftware(Service): def software(file_system): return TestSoftware( name="TestSoftware", - port=Port["ARP"], + port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="test_service"), - protocol=IPProtocol["TCP"], + protocol=PROTOCOL_LOOKUP["TCP"], ) diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index 8becc6ae..10ed36e0 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.utils.converters import convert_dict_enum_keys_to_enum_values +from primaite.utils.validators import PROTOCOL_LOOKUP def test_simple_conversion(): @@ -11,7 +11,7 @@ def test_simple_conversion(): The original dictionary contains one level of nested dictionary with enums as keys. The expected output should have string values of enums as keys. """ - original_dict = {IPProtocol["UDP"]: {Port["ARP"]: {"inbound": 0, "outbound": 1016.0}}} + original_dict = {PROTOCOL_LOOKUP["UDP"]: {PORT_LOOKUP["ARP"]: {"inbound": 0, "outbound": 1016.0}}} expected_dict = {"udp": {219: {"inbound": 0, "outbound": 1016.0}}} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict @@ -36,8 +36,8 @@ def test_mixed_keys(): The expected output should have string values of enums and original string keys. """ original_dict = { - IPProtocol["TCP"]: {"port": {"inbound": 0, "outbound": 1016.0}}, - "protocol": {Port["HTTP"]: {"inbound": 10, "outbound": 2020.0}}, + PROTOCOL_LOOKUP["TCP"]: {"port": {"inbound": 0, "outbound": 1016.0}}, + "protocol": {PORT_LOOKUP["HTTP"]: {"inbound": 10, "outbound": 2020.0}}, } expected_dict = { "tcp": {"port": {"inbound": 0, "outbound": 1016.0}}, @@ -66,8 +66,12 @@ def test_nested_dicts(): The expected output should have string values of enums as keys at all levels. """ original_dict = { - IPProtocol["UDP"]: { - Port["ARP"]: {"inbound": 0, "outbound": 1016.0, "details": {IPProtocol["TCP"]: {"latency": "low"}}} + PROTOCOL_LOOKUP["UDP"]: { + PORT_LOOKUP["ARP"]: { + "inbound": 0, + "outbound": 1016.0, + "details": {PROTOCOL_LOOKUP["TCP"]: {"latency": "low"}}, + } } } expected_dict = {"udp": {219: {"inbound": 0, "outbound": 1016.0, "details": {"tcp": {"latency": "low"}}}}} @@ -82,8 +86,11 @@ def test_non_dict_values(): The expected output should preserve these non-dictionary values while converting enum keys to string values. """ original_dict = { - IPProtocol["UDP"]: [Port["ARP"], Port["HTTP"]], - "protocols": (IPProtocol["TCP"], IPProtocol["UDP"]), + PROTOCOL_LOOKUP["UDP"]: [PORT_LOOKUP["ARP"], PORT_LOOKUP["HTTP"]], + "protocols": (PROTOCOL_LOOKUP["TCP"], PROTOCOL_LOOKUP["UDP"]), + } + expected_dict = { + "udp": [PORT_LOOKUP["ARP"], PORT_LOOKUP["HTTP"]], + "protocols": (PROTOCOL_LOOKUP["TCP"], PROTOCOL_LOOKUP["UDP"]), } - expected_dict = {"udp": [Port["ARP"], Port["HTTP"]], "protocols": (IPProtocol["TCP"], IPProtocol["UDP"])} assert convert_dict_enum_keys_to_enum_values(original_dict) == expected_dict From f1b911bc651f152ae0426dac13e26b6002668e46 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 25 Sep 2024 16:28:22 +0100 Subject: [PATCH 005/224] Change port and protocol to annotated validators --- .../agent/observations/host_observations.py | 3 +- .../agent/observations/nic_observations.py | 30 ++------ .../agent/observations/node_observations.py | 29 ++------ src/primaite/game/game.py | 36 ++-------- src/primaite/simulator/network/creation.py | 4 +- .../simulator/network/hardware/base.py | 5 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../hardware/nodes/network/firewall.py | 5 +- .../network/hardware/nodes/network/router.py | 61 +++++++--------- .../hardware/nodes/network/wireless_router.py | 5 +- src/primaite/simulator/network/networks.py | 4 +- .../simulator/network/protocols/masquerade.py | 6 +- .../network/transmission/data_link_layer.py | 5 +- .../network/transmission/network_layer.py | 3 +- .../network/transmission/transport_layer.py | 32 --------- .../system/applications/database_client.py | 5 +- .../simulator/system/applications/nmap.py | 47 +++++++------ .../red_applications/c2/abstract_c2.py | 10 +-- .../red_applications/c2/c2_beacon.py | 4 +- .../red_applications/data_manipulation_bot.py | 4 +- .../applications/red_applications/dos_bot.py | 4 +- .../red_applications/ransomware_script.py | 4 +- .../system/applications/web_browser.py | 6 +- .../simulator/system/core/session_manager.py | 49 +++++++------ .../simulator/system/core/software_manager.py | 22 +++--- .../simulator/system/services/arp/arp.py | 5 +- .../services/database/database_service.py | 4 +- .../system/services/dns/dns_client.py | 6 +- .../system/services/dns/dns_server.py | 4 +- .../system/services/ftp/ftp_client.py | 18 ++--- .../system/services/ftp/ftp_server.py | 6 +- .../system/services/ftp/ftp_service.py | 7 +- .../simulator/system/services/icmp/icmp.py | 4 +- .../system/services/ntp/ntp_client.py | 6 +- .../system/services/ntp/ntp_server.py | 4 +- .../system/services/terminal/terminal.py | 4 +- .../system/services/web_server/web_server.py | 6 +- src/primaite/simulator/system/software.py | 11 +-- src/primaite/utils/validation/__init__.py | 1 + src/primaite/utils/validation/ip_protocol.py | 47 +++++++++++++ .../ipv4_address.py} | 38 +--------- src/primaite/utils/validation/port.py | 70 +++++++++++++++++++ tests/conftest.py | 4 +- .../nodes/network/test_firewall_config.py | 4 +- .../nodes/network/test_router_config.py | 4 +- .../applications/extended_application.py | 4 +- .../extensions/nodes/super_computer.py | 2 +- .../extensions/services/extended_service.py | 4 +- .../actions/test_c2_suite_actions.py | 2 +- .../actions/test_configure_actions.py | 2 +- .../actions/test_terminal_actions.py | 2 +- .../observations/test_acl_observations.py | 2 +- .../observations/test_firewall_observation.py | 4 +- .../observations/test_router_observation.py | 4 +- .../observations/test_user_observations.py | 2 +- .../game_layer/test_actions.py | 4 +- .../game_layer/test_rewards.py | 4 +- .../network/test_broadcast.py | 4 +- .../network/test_firewall.py | 4 +- .../integration_tests/network/test_routing.py | 4 +- .../network/test_wireless_router.py | 4 +- .../test_c2_suite_integration.py | 4 +- .../test_data_manipulation_bot_and_server.py | 2 +- .../test_dos_bot_and_server.py | 2 +- .../test_ransomware_script.py | 2 +- tests/integration_tests/system/test_nmap.py | 4 +- .../system/test_service_listening_on_ports.py | 4 +- .../test_web_client_server_and_database.py | 2 +- .../test_simulation/test_request_response.py | 2 +- .../_network/_hardware/nodes/test_acl.py | 5 +- .../_network/_hardware/nodes/test_router.py | 4 +- .../_transmission/test_data_link_layer.py | 5 +- .../_red_applications/test_c2_suite.py | 4 +- .../test_data_manipulation_bot.py | 4 +- .../_red_applications/test_dos_bot.py | 2 +- .../_system/_applications/test_web_browser.py | 4 +- .../_system/_services/test_dns_client.py | 4 +- .../_system/_services/test_dns_server.py | 4 +- .../_system/_services/test_ftp_client.py | 4 +- .../_system/_services/test_ftp_server.py | 4 +- .../_system/_services/test_terminal.py | 4 +- .../_system/_services/test_web_server.py | 4 +- .../_simulator/_system/test_software.py | 4 +- .../_utils/test_dict_enum_keys_conversion.py | 4 +- 84 files changed, 380 insertions(+), 392 deletions(-) create mode 100644 src/primaite/utils/validation/__init__.py create mode 100644 src/primaite/utils/validation/ip_protocol.py rename src/primaite/utils/{validators.py => validation/ipv4_address.py} (59%) create mode 100644 src/primaite/utils/validation/port.py diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 0984f008..96c5f40d 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -12,7 +12,8 @@ from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.software_observation import ApplicationObservation, ServiceObservation from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -from primaite.utils.validators import IPProtocol, Port +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index c51cb427..d180b641 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -1,16 +1,15 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations -from typing import Dict, Optional +from typing import Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType -from pydantic import field_validator from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.port import Port class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): @@ -23,30 +22,9 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): """Number of the network interface.""" include_nmne: Optional[bool] = None """Whether to include number of malicious network events (NMNE) in the observation.""" - monitored_traffic: Optional[Dict] = None + monitored_traffic: Optional[Dict[IPProtocol, List[Port]]] = None """A dict containing which traffic types are to be included in the observation.""" - @field_validator("monitored_traffic", mode="before") - def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: - """ - Convert monitored_traffic by lookup against Port and Protocol dicts. - - This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. - This method will be removed in PrimAITE >= 4.0 - """ - if val is None: - return val - new_val = {} - for proto, port_list in val.items(): - # convert protocol, for instance ICMP becomes "icmp" - proto = PROTOCOL_LOOKUP[proto] if proto in PROTOCOL_LOOKUP else proto - new_val[proto] = [] - for port in port_list: - # convert ports, for instance "HTTP" becomes 80 - port = PORT_LOOKUP[port] if port in PORT_LOOKUP else port - new_val[proto].append(port) - return new_val - def __init__(self, where: WhereType, include_nmne: bool, monitored_traffic: Optional[Dict] = None) -> None: """ Initialise a network interface observation instance. diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 0bb8ea0f..e11521b6 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -5,15 +5,15 @@ from typing import Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType -from pydantic import field_validator, model_validator +from pydantic import model_validator from primaite import getLogger from primaite.game.agent.observations.firewall_observation import FirewallObservation from primaite.game.agent.observations.host_observations import HostObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.router_observation import RouterObservation -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) @@ -42,7 +42,7 @@ class NodesObservation(AbstractObservation, identifier="NODES"): """Number of network interface cards (NICs).""" include_nmne: Optional[bool] = None """Flag to include nmne.""" - monitored_traffic: Optional[Dict] = None + monitored_traffic: Optional[Dict[IPProtocol, List[Port]]] = None """A dict containing which traffic types are to be included in the observation.""" include_num_access: Optional[bool] = None """Flag to include the number of accesses.""" @@ -63,27 +63,6 @@ class NodesObservation(AbstractObservation, identifier="NODES"): num_rules: Optional[int] = None """Number of rules ACL rules to show.""" - @field_validator("monitored_traffic", mode="before") - def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: - """ - Convert monitored_traffic by lookup against Port and Protocol dicts. - - This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. - This method will be removed in PrimAITE >= 4.0 - """ - if val is None: - return val - new_val = {} - for proto, port_list in val.items(): - # convert protocol, for instance ICMP becomes "icmp" - proto = PROTOCOL_LOOKUP[proto] if proto in PROTOCOL_LOOKUP else proto - new_val[proto] = [] - for port in port_list: - # convert ports, for instance "HTTP" becomes 80 - port = PORT_LOOKUP[port] if port in PORT_LOOKUP else port - new_val[proto].append(port) - return new_val - @model_validator(mode="after") def force_optional_fields(self) -> NodesObservation.ConfigSchema: """Check that options are specified only if they are needed for the nodes that are part of the config.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index a0d2ceb4..6d1c0920 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -4,7 +4,7 @@ from ipaddress import IPv4Address from typing import Dict, List, Optional, Union import numpy as np -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import BaseModel, ConfigDict from primaite import DEFAULT_BANDWIDTH, getLogger from primaite.game.agent.actions import ActionManager @@ -27,7 +27,6 @@ from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from primaite.simulator.network.nmne import NMNEConfig -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient # noqa: F401 @@ -50,7 +49,8 @@ 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 -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -81,39 +81,13 @@ class PrimaiteGameOptions(BaseModel): """Random number seed for RNGs.""" max_episode_length: int = 256 """Maximum number of episodes for the PrimAITE game.""" - ports: List[int] + ports: List[Port] """A whitelist of available ports in the simulation.""" - protocols: List[str] + protocols: List[IPProtocol] """A whitelist of available protocols in the simulation.""" thresholds: Optional[Dict] = {} """A dict containing the thresholds used for determining what is acceptable during observations.""" - @field_validator("ports", mode="before") - def ports_str2int(cls, vals: Union[List[str], List[int]]) -> List[int]: - """ - Convert named port strings to port integer values. Integer ports remain unaffected. - - This is necessary to retain backwards compatibility with configs written for PrimAITE<=3.3. - :warning: This will be deprecated in PrimAITE 4.0 and configs will need to be converted. - """ - for i, port_val in enumerate(vals): - if port_val in PORT_LOOKUP: - vals[i] = PORT_LOOKUP[port_val] - return vals - - @field_validator("protocols", mode="before") - def protocols_str2int(cls, vals: List[str]) -> List[str]: - """ - Convert old-style named protocols to their proper values. - - This is necessary to retain backwards compatibility with configs written for PrimAITE<=3.3. - :warning: This will be deprecated in PrimAITE 4.0 and configs will need to be converted. - """ - for i, proto_val in enumerate(vals): - if proto_val in PROTOCOL_LOOKUP: - vals[i] = PROTOCOL_LOOKUP[proto_val] - return vals - class PrimaiteGame: """ diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 9e2e5502..891c445e 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -6,8 +6,8 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP def num_of_switches_required(num_nodes: int, max_network_interface: int = 24) -> int: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index affaf3cc..778cffa2 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -21,7 +21,6 @@ from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.packet_capture import PacketCapture from primaite.simulator.system.core.session_manager import SessionManager @@ -32,7 +31,9 @@ from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.terminal.terminal import Terminal from primaite.simulator.system.software import IOSoftware, Software from primaite.utils.converters import convert_dict_enum_keys_to_enum_values -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import PORT_LOOKUP IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware) diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 8a420e44..5699721b 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -22,7 +22,7 @@ from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.icmp.icmp import ICMP from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.terminal.terminal import Terminal -from primaite.utils.validators import IPV4Address +from primaite.utils.validation.ipv4_address import IPV4Address _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index eed1132b..47cfae57 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -14,9 +14,10 @@ from primaite.simulator.network.hardware.nodes.network.router import ( RouterInterface, ) from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.sys_log import SysLog -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import PORT_LOOKUP EXTERNAL_PORT_ID: Final[int] = 1 """The Firewall port ID of the external port.""" diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 46efe668..244f40ce 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import field_validator, validate_call +from pydantic import validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -17,14 +17,15 @@ from primaite.simulator.network.hardware.nodes.network.network_node import Netwo from primaite.simulator.network.protocols.arp import ARPPacket from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.nmap import NMAP from primaite.simulator.system.core.session_manager import SessionManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.arp.arp import ARP from primaite.simulator.system.services.icmp.icmp import ICMP from primaite.simulator.system.services.terminal.terminal import Terminal -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import Port, PORT_LOOKUP @validate_call() @@ -120,29 +121,15 @@ class ACLRule(SimComponent): """ action: ACLAction = ACLAction.DENY - protocol: Optional[str] = None + protocol: Optional[IPProtocol] = None src_ip_address: Optional[IPV4Address] = None src_wildcard_mask: Optional[IPV4Address] = None dst_ip_address: Optional[IPV4Address] = None dst_wildcard_mask: Optional[IPV4Address] = None - src_port: Optional[int] = None - dst_port: Optional[int] = None + src_port: Optional[Port] = None + dst_port: Optional[Port] = None match_count: int = 0 - @field_validator("protocol", mode="before") - def protocol_valid(cls, val: Optional[str]) -> Optional[str]: - """Assert that the protocol for the rule is predefined in the IPProtocol lookup.""" - if val is not None: - assert val in PROTOCOL_LOOKUP.values(), f"Cannot create ACL rule with invalid protocol {val}" - return val - - @field_validator("src_port", "dst_port", mode="before") - def ports_valid(cls, val: Optional[int]) -> Optional[int]: - """Assert that the port for the rule is predefined in the Port lookup.""" - if val is not None: - assert val in PORT_LOOKUP.values(), f"Cannot create ACL rule with invalid port {val}" - return val - def __str__(self) -> str: rule_strings = [] for key, value in self.model_dump(exclude={"uuid", "request_manager"}).items(): @@ -390,13 +377,13 @@ class AccessControlList(SimComponent): def add_rule( self, action: ACLAction = ACLAction.DENY, - protocol: Optional[str] = None, + protocol: Optional[IPProtocol] = None, src_ip_address: Optional[IPV4Address] = None, src_wildcard_mask: Optional[IPV4Address] = None, dst_ip_address: Optional[IPV4Address] = None, dst_wildcard_mask: Optional[IPV4Address] = None, - src_port: Optional[int] = None, - dst_port: Optional[int] = None, + src_port: Optional[Port] = None, + dst_port: Optional[Port] = None, position: int = 0, ) -> bool: """ @@ -498,11 +485,11 @@ class AccessControlList(SimComponent): def get_relevant_rules( self, - protocol: str, + protocol: IPProtocol, src_ip_address: Union[str, IPv4Address], - src_port: int, + src_port: Port, dst_ip_address: Union[str, IPv4Address], - dst_port: int, + dst_port: Port, ) -> List[ACLRule]: """ Get the list of relevant rules for a packet with given properties. @@ -1101,17 +1088,17 @@ class RouterSessionManager(SessionManager): def resolve_outbound_transmission_details( self, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[int] = None, - dst_port: Optional[int] = None, - protocol: Optional[str] = None, + src_port: Optional[Port] = None, + dst_port: Optional[Port] = None, + protocol: Optional[IPProtocol] = None, session_id: Optional[str] = None, ) -> Tuple[ Optional[RouterInterface], Optional[str], IPv4Address, - Optional[int], - Optional[int], - Optional[str], + Optional[Port], + Optional[Port], + Optional[IPProtocol], bool, ]: """ @@ -1131,19 +1118,19 @@ class RouterSessionManager(SessionManager): treats the transmission as a broadcast to that network. Optional. :type dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] :param src_port: The source port number for the transmission. Optional. - :type src_port: Optional[int] + :type src_port: Optional[Port] :param dst_port: The destination port number for the transmission. Optional. - :type dst_port: Optional[int] + :type dst_port: Optional[Port] :param protocol: The IP protocol to be used for the transmission. Optional. - :type protocol: Optional[str] + :type protocol: Optional[IPProtocol] :param session_id: The session ID associated with the transmission. If provided, the session details override other parameters. Optional. :type session_id: Optional[str] :return: A tuple containing the resolved outbound network interface, destination MAC address, destination IP address, source port, destination port, protocol, and a boolean indicating whether the transmission is a broadcast. - :rtype: Tuple[Optional[RouterInterface], Optional[str], IPv4Address, Optional[int], Optional[int], - Optional[str], bool] + :rtype: Tuple[Optional[RouterInterface], Optional[str], IPv4Address, Optional[Port], Optional[Port], + Optional[IPProtocol], bool] """ if dst_ip_address and not isinstance(dst_ip_address, (IPv4Address, IPv4Network)): dst_ip_address = IPv4Address(dst_ip_address) diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 3615ef54..27a13154 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -8,8 +8,9 @@ from primaite.simulator.network.airspace import AirSpace, IPWirelessNetworkInter from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router, RouterInterface from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import PORT_LOOKUP class WirelessAccessPoint(IPWirelessNetworkInterface): diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index c3b4a341..2c3c15b4 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -12,14 +12,14 @@ from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.web_server.web_server import WebServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index ef060bc7..5c5f03b2 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -3,14 +3,16 @@ from enum import Enum from typing import Optional from primaite.simulator.network.protocols.packet import DataPacket +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.port import Port class MasqueradePacket(DataPacket): """Represents an generic malicious packet that is masquerading as another protocol.""" - masquerade_protocol: str # The 'Masquerade' protocol that is currently in use + masquerade_protocol: IPProtocol # The 'Masquerade' protocol that is currently in use - masquerade_port: int # The 'Masquerade' port that is currently in use + masquerade_port: Port # The 'Masquerade' port that is currently in use class C2Packet(MasqueradePacket): diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index ca212c58..259d62e3 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -9,9 +9,10 @@ from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.protocols.packet import DataPacket from primaite.simulator.network.transmission.network_layer import IPPacket from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPHeader, UDPHeader +from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader from primaite.simulator.network.utils import convert_bytes_to_megabits -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index 47e8a032..49dcd1f5 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -4,7 +4,8 @@ from enum import Enum from pydantic import BaseModel from primaite import getLogger -from primaite.utils.validators import IPProtocol, IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index fbc4b5ad..10cf802c 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -4,38 +4,6 @@ from typing import List from pydantic import BaseModel -PORT_LOOKUP: dict[str, int] = dict( - UNUSED=-1, - NONE=0, - WOL=9, - FTP_DATA=20, - FTP=21, - SSH=22, - SMTP=25, - DNS=53, - HTTP=80, - POP3=110, - SFTP=115, - NTP=123, - IMAP=143, - SNMP=161, - SNMP_TRAP=162, - ARP=219, - LDAP=389, - HTTPS=443, - SMB=445, - IPP=631, - SQL_SERVER=1433, - MYSQL=3306, - RDP=3389, - RTP=5004, - RTP_ALT=5005, - DNS_ALT=5353, - HTTP_ALT=8080, - HTTPS_ALT=8443, - POSTGRES_SERVER=5432, -) - class UDPHeader(BaseModel): """ diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 4967f519..cd4b2a03 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -11,10 +11,11 @@ from pydantic import BaseModel from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.software_manager import SoftwareManager -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import PORT_LOOKUP class DatabaseClientConnection(BaseModel): diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 34433e65..a04067c4 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -7,9 +7,10 @@ from pydantic import validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, is_valid_protocol, PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import is_valid_port, Port, PORT_LOOKUP class PortScanPayload(SimComponent): @@ -23,8 +24,8 @@ class PortScanPayload(SimComponent): """ ip_address: IPV4Address - port: int - protocol: str + port: Port + protocol: IPProtocol request: bool = True def describe_state(self) -> Dict: @@ -217,14 +218,14 @@ class NMAP(Application, identifier="NMAP"): print(table.get_string(sortby="IP Address")) return active_nodes - def _determine_port_scan_type(self, target_ip_addresses: List[IPV4Address], target_ports: List[int]) -> str: + def _determine_port_scan_type(self, target_ip_addresses: List[IPV4Address], target_ports: List[Port]) -> str: """ Determine the type of port scan based on the number of target IP addresses and ports. :param target_ip_addresses: The list of target IP addresses. :type target_ip_addresses: List[IPV4Address] :param target_ports: The list of target ports. - :type target_ports: List[int] + :type target_ports: List[Port] :return: The type of port scan. :rtype: str @@ -237,8 +238,8 @@ class NMAP(Application, identifier="NMAP"): def _check_port_open_on_ip_address( self, ip_address: IPv4Address, - port: int, - protocol: str, + port: Port, + protocol: IPProtocol, is_re_attempt: bool = False, port_scan_uuid: Optional[str] = None, ) -> bool: @@ -250,7 +251,7 @@ class NMAP(Application, identifier="NMAP"): :param port: The target port. :type port: Port :param protocol: The protocol used for the port scan. - :type protocol: str + :type protocol: IPProtocol :param is_re_attempt: Flag indicating if this is a reattempt. Defaults to False. :type is_re_attempt: bool :param port_scan_uuid: The UUID of the port scan payload. Defaults to None. @@ -319,20 +320,20 @@ class NMAP(Application, identifier="NMAP"): def port_scan( self, target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]], - target_protocol: Optional[Union[str, List[str]]] = None, - target_port: Optional[Union[int, List[int]]] = None, + target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] = None, + target_port: Optional[Union[Port, List[Port]]] = None, show: bool = True, json_serializable: bool = False, - ) -> Dict[IPv4Address, Dict[str, List[int]]]: + ) -> Dict[IPv4Address, Dict[IPProtocol, List[Port]]]: """ Perform a port scan on the target IP address(es). :param target_ip_address: The target IP address(es) or network(s) for the port scan. :type target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]] :param target_protocol: The protocol(s) to use for the port scan. Defaults to None, which includes TCP and UDP. - :type target_protocol: Optional[Union[str, List[str]]] + :type target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] :param target_port: The port(s) to scan. Defaults to None, which includes all valid ports. - :type target_port: Optional[Union[int, List[int]]] + :type target_port: Optional[Union[Port, List[Port]]] :param show: Flag indicating whether to display the scan results. Defaults to True. :type show: bool :param json_serializable: Flag indicating whether the return value should be JSON serializable. Defaults to @@ -340,16 +341,16 @@ class NMAP(Application, identifier="NMAP"): :type json_serializable: bool :return: A dictionary mapping IP addresses to protocols and lists of open ports. - :rtype: Dict[IPv4Address, Dict[str, List[int]]] + :rtype: Dict[IPv4Address, Dict[IPProtocol, List[Port]]] """ ip_addresses = self._explode_ip_address_network_array(target_ip_address) - if isinstance(target_port, int): + if is_valid_port(target_port): target_port = [target_port] elif target_port is None: target_port = [port for port in PORT_LOOKUP if port not in {PORT_LOOKUP["NONE"], PORT_LOOKUP["UNUSED"]}] - if isinstance(target_protocol, str): + if is_valid_protocol(target_protocol): target_protocol = [target_protocol] elif target_protocol is None: target_protocol = [PROTOCOL_LOOKUP["TCP"], PROTOCOL_LOOKUP["UDP"]] @@ -389,12 +390,12 @@ class NMAP(Application, identifier="NMAP"): def network_service_recon( self, target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]], - target_protocol: Optional[Union[str, List[str]]] = None, - target_port: Optional[Union[int, List[int]]] = None, + target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] = None, + target_port: Optional[Union[Port, List[Port]]] = None, show: bool = True, show_online_only: bool = True, json_serializable: bool = False, - ) -> Dict[IPv4Address, Dict[str, List[int]]]: + ) -> Dict[IPv4Address, Dict[IPProtocol, List[Port]]]: """ Perform a network service reconnaissance which includes a ping scan followed by a port scan. @@ -407,9 +408,9 @@ class NMAP(Application, identifier="NMAP"): :param target_ip_address: The target IP address(es) or network(s) for the port scan. :type target_ip_address: Union[IPV4Address, List[IPV4Address], IPv4Network, List[IPv4Network]] :param target_protocol: The protocol(s) to use for the port scan. Defaults to None, which includes TCP and UDP. - :type target_protocol: Optional[Union[str, List[str]]] + :type target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] :param target_port: The port(s) to scan. Defaults to None, which includes all valid ports. - :type target_port: Optional[Union[int, List[int]]] + :type target_port: Optional[Union[Port, List[Port]]] :param show: Flag indicating whether to display the scan results. Defaults to True. :type show: bool :param show_online_only: Flag indicating whether to show only the online hosts. Defaults to True. @@ -419,7 +420,7 @@ class NMAP(Application, identifier="NMAP"): :type json_serializable: bool :return: A dictionary mapping IP addresses to protocols and lists of open ports. - :rtype: Dict[IPv4Address, Dict[str, List[int]]] + :rtype: Dict[IPv4Address, Dict[IPProtocol, List[Port]]] """ ping_scan_results = self.ping_scan( target_ip_address=target_ip_address, show=show, show_online_only=show_online_only, json_serializable=False diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index b0cdefba..aff12748 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -9,14 +9,14 @@ from pydantic import BaseModel, Field, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.protocols.masquerade import C2Packet -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.session_manager import Session 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.service import ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, is_valid_protocol, PROTOCOL_LOOKUP +from primaite.utils.validation.port import is_valid_port, Port, PORT_LOOKUP class C2Command(Enum): @@ -81,10 +81,10 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_frequency: int = Field(default=5, ge=1) """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" - masquerade_protocol: str = Field(default=PROTOCOL_LOOKUP["TCP"]) + masquerade_protocol: IPProtocol = Field(default=PROTOCOL_LOOKUP["TCP"]) """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - masquerade_port: int = Field(default=PORT_LOOKUP["HTTP"]) + masquerade_port: Port = Field(default=PORT_LOOKUP["HTTP"]) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" c2_config: _C2Opts = _C2Opts() @@ -367,7 +367,7 @@ class AbstractC2(Application, identifier="AbstractC2"): :rtype: bool """ # Validating that they are valid Enums. - if not isinstance(payload.masquerade_port, int) or not isinstance(payload.masquerade_protocol, str): + if not is_valid_port(payload.masquerade_port) or not is_valid_protocol(payload.masquerade_protocol): self.sys_log.warning( f"{self.name}: Received invalid Masquerade Values within Keep Alive." f"Port: {payload.masquerade_port} Protocol: {payload.masquerade_protocol}." diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 450c60ad..c0c3d872 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -8,12 +8,12 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts, RansomwareOpts, TerminalOpts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.terminal.terminal import Terminal, TerminalClientConnection -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP class C2Beacon(AbstractC2, identifier="C2Beacon"): diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index c2d19160..9fdbae57 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -7,10 +7,10 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index 7e199b48..fb2c8847 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -7,8 +7,8 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -35,7 +35,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): target_ip_address: Optional[IPv4Address] = None """IP address of the target service.""" - target_port: Optional[int] = None + target_port: Optional[Port] = None """Port of the target service.""" payload: Optional[str] = None diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 56f885f4..93b4c50d 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -6,10 +6,10 @@ from prettytable import MARKDOWN, PrettyTable from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP class RansomwareScript(Application, identifier="RansomwareScript"): diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index faa7b5ec..c57a9bd3 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -15,10 +15,10 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -154,7 +154,7 @@ class WebBrowser(Application, identifier="WebBrowser"): self, payload: HttpRequestPacket, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = PORT_LOOKUP["HTTP"], + dest_port: Optional[Port] = PORT_LOOKUP["HTTP"], session_id: Optional[str] = None, **kwargs, ) -> bool: diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index fcf07d9f..75322e86 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -11,8 +11,9 @@ from primaite.simulator.network.protocols.arp import ARPPacket from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.network_layer import IPPacket -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPHeader, UDPHeader -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP if TYPE_CHECKING: from primaite.simulator.network.hardware.base import NetworkInterface @@ -37,12 +38,12 @@ class Session(SimComponent): protocol: str with_ip_address: IPv4Address - src_port: Optional[int] - dst_port: Optional[int] + src_port: Optional[Port] + dst_port: Optional[Port] connected: bool = False @classmethod - def from_session_key(cls, session_key: Tuple[str, IPv4Address, Optional[int], Optional[int]]) -> Session: + def from_session_key(cls, session_key: Tuple[IPProtocol, IPv4Address, Optional[Port], Optional[Port]]) -> Session: """ Create a Session instance from a session key tuple. @@ -77,7 +78,9 @@ class SessionManager: """ def __init__(self, sys_log: SysLog): - self.sessions_by_key: Dict[Tuple[str, IPv4Address, IPv4Address, Optional[int], Optional[int]], Session] = {} + self.sessions_by_key: Dict[ + Tuple[IPProtocol, IPv4Address, IPv4Address, Optional[Port], Optional[Port]], Session + ] = {} self.sessions_by_uuid: Dict[str, Session] = {} self.sys_log: SysLog = sys_log self.software_manager: SoftwareManager = None # Noqa @@ -102,7 +105,7 @@ class SessionManager: @staticmethod def _get_session_key( frame: Frame, inbound_frame: bool = True - ) -> Tuple[str, IPv4Address, Optional[int], Optional[int]]: + ) -> Tuple[IPProtocol, IPv4Address, Optional[Port], Optional[Port]]: """ Extracts the session key from the given frame. @@ -110,8 +113,8 @@ class SessionManager: - IPProtocol: The transport protocol (e.g. TCP, UDP, ICMP). - IPv4Address: The source IP address. - IPv4Address: The destination IP address. - - Optional[int]: The source port number (if applicable). - - Optional[int]: The destination port number (if applicable). + - Optional[Port]: The source port number (if applicable). + - Optional[Port]: The destination port number (if applicable). :param frame: The network frame from which to extract the session key. :return: A tuple containing the session key. @@ -166,17 +169,17 @@ class SessionManager: def resolve_outbound_transmission_details( self, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[int] = None, - dst_port: Optional[int] = None, - protocol: Optional[str] = None, + src_port: Optional[Port] = None, + dst_port: Optional[Port] = None, + protocol: Optional[IPProtocol] = None, session_id: Optional[str] = None, ) -> Tuple[ Optional["NetworkInterface"], Optional[str], IPv4Address, - Optional[int], - Optional[int], - Optional[str], + Optional[Port], + Optional[Port], + Optional[IPProtocol], bool, ]: """ @@ -195,19 +198,19 @@ class SessionManager: treats the transmission as a broadcast to that network. Optional. :type dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] :param src_port: The source port number for the transmission. Optional. - :type src_port: Optional[int] + :type src_port: Optional[Port] :param dst_port: The destination port number for the transmission. Optional. - :type dst_port: Optional[int] + :type dst_port: Optional[Port] :param protocol: The IP protocol to be used for the transmission. Optional. - :type protocol: Optional[str] + :type protocol: Optional[IPProtocol] :param session_id: The session ID associated with the transmission. If provided, the session details override other parameters. Optional. :type session_id: Optional[str] :return: A tuple containing the resolved outbound network interface, destination MAC address, destination IP address, source port, destination port, protocol, and a boolean indicating whether the transmission is a broadcast. - :rtype: Tuple[Optional["NetworkInterface"], Optional[str], IPv4Address, Optional[int], Optional[int], - Optional[str], bool] + :rtype: Tuple[Optional["NetworkInterface"], Optional[str], IPv4Address, Optional[Port], Optional[Port], + Optional[IPProtocol], bool] """ if dst_ip_address and not isinstance(dst_ip_address, (IPv4Address, IPv4Network)): dst_ip_address = IPv4Address(dst_ip_address) @@ -258,10 +261,10 @@ class SessionManager: self, payload: Any, dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[int] = None, - dst_port: Optional[int] = None, + src_port: Optional[Port] = None, + dst_port: Optional[Port] = None, session_id: Optional[str] = None, - ip_protocol: str = PROTOCOL_LOOKUP["TCP"], + ip_protocol: IPProtocol = PROTOCOL_LOOKUP["TCP"], icmp_packet: Optional[ICMPPacket] = None, ) -> Union[Any, None]: """ diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index abf2ca3a..60621384 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -8,12 +8,12 @@ from prettytable import MARKDOWN, PrettyTable from primaite.simulator.core import RequestType from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import IOSoftware -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP if TYPE_CHECKING: from primaite.simulator.system.core.session_manager import SessionManager @@ -52,7 +52,7 @@ class SoftwareManager: self.session_manager = session_manager self.software: Dict[str, Union[Service, Application]] = {} self._software_class_to_name_map: Dict[Type[IOSoftware], str] = {} - self.port_protocol_mapping: Dict[Tuple[int, str], Union[Service, Application]] = {} + self.port_protocol_mapping: Dict[Tuple[Port, IPProtocol], Union[Service, Application]] = {} self.sys_log: SysLog = sys_log self.file_system: FileSystem = file_system self.dns_server: Optional[IPv4Address] = dns_server @@ -67,7 +67,7 @@ class SoftwareManager: """Provides access to the ICMP service instance, if installed.""" return self.software.get("ICMP") # noqa - def get_open_ports(self) -> List[int]: + def get_open_ports(self) -> List[Port]: """ Get a list of open ports. @@ -81,7 +81,7 @@ class SoftwareManager: open_ports += list(software.listen_on_ports) return open_ports - def check_port_is_open(self, port: int, protocol: str) -> bool: + def check_port_is_open(self, port: Port, protocol: IPProtocol) -> bool: """ Check if a specific port is open and running a service using the specified protocol. @@ -93,7 +93,7 @@ class SoftwareManager: :param port: The port to check. :type port: Port :param protocol: The protocol to check (e.g., TCP, UDP). - :type protocol: str + :type protocol: IPProtocol :return: True if the port is open and a service is running on it using the specified protocol, False otherwise. :rtype: bool """ @@ -189,9 +189,9 @@ class SoftwareManager: self, payload: Any, dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, - src_port: Optional[int] = None, - dest_port: Optional[int] = None, - ip_protocol: str = PROTOCOL_LOOKUP["TCP"], + src_port: Optional[Port] = None, + dest_port: Optional[Port] = None, + ip_protocol: IPProtocol = PROTOCOL_LOOKUP["TCP"], session_id: Optional[str] = None, ) -> bool: """ @@ -219,8 +219,8 @@ class SoftwareManager: def receive_payload_from_session_manager( self, payload: Any, - port: int, - protocol: str, + port: Port, + protocol: IPProtocol, session_id: str, from_network_interface: "NIC", frame: Frame, diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 2641f1c8..816eb99e 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -8,9 +8,10 @@ from prettytable import MARKDOWN, PrettyTable from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service -from primaite.utils.validators import IPV4Address, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import PORT_LOOKUP class ARP(Service): diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index f9a5d087..b7cd8886 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -7,12 +7,12 @@ from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.file_system.folder import Folder -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 316189a7..78642fa6 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -4,10 +4,10 @@ from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.service import Service -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -110,7 +110,7 @@ class DNSClient(Service): payload: DNSPacket, session_id: Optional[str] = None, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = None, + dest_port: Optional[Port] = None, **kwargs, ) -> bool: """ diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index e0786124..5b380320 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -6,9 +6,9 @@ from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 11a926cf..00b70332 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -7,10 +7,10 @@ from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -104,7 +104,7 @@ class FTPClient(FTPServiceABC): def _connect_to_server( self, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = PORT_LOOKUP["FTP"], + dest_port: Optional[Port] = PORT_LOOKUP["FTP"], session_id: Optional[str] = None, is_reattempt: Optional[bool] = False, ) -> bool: @@ -114,7 +114,7 @@ class FTPClient(FTPServiceABC): :param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional. :type: dest_ip_address: Optional[IPv4Address] :param: dest_port: Port of the FTP server the client needs to connect to. Optional. - :type: dest_port: Optional[int] + :type: dest_port: Optional[Port] :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False. :type: is_reattempt: Optional[bool] """ @@ -152,7 +152,7 @@ class FTPClient(FTPServiceABC): return False def _disconnect_from_server( - self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[int] = PORT_LOOKUP["FTP"] + self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = PORT_LOOKUP["FTP"] ) -> bool: """ Connects the client from a given FTP server. @@ -160,7 +160,7 @@ class FTPClient(FTPServiceABC): :param: dest_ip_address: IP address of the FTP server the client needs to disconnect from. Optional. :type: dest_ip_address: Optional[IPv4Address] :param: dest_port: Port of the FTP server the client needs to disconnect from. Optional. - :type: dest_port: Optional[int] + :type: dest_port: Optional[Port] :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. :type: is_reattempt: Optional[bool] """ @@ -179,7 +179,7 @@ class FTPClient(FTPServiceABC): src_file_name: str, dest_folder_name: str, dest_file_name: str, - dest_port: Optional[int] = PORT_LOOKUP["FTP"], + dest_port: Optional[Port] = PORT_LOOKUP["FTP"], session_id: Optional[str] = None, ) -> bool: """ @@ -204,7 +204,7 @@ class FTPClient(FTPServiceABC): :type: dest_file_name: str :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. - :type: dest_port: Optional[int] + :type: dest_port: Optional[Port] :param: session_id: The id of the session :type: session_id: Optional[str] @@ -241,7 +241,7 @@ class FTPClient(FTPServiceABC): src_file_name: str, dest_folder_name: str, dest_file_name: str, - dest_port: Optional[int] = PORT_LOOKUP["FTP"], + dest_port: Optional[Port] = PORT_LOOKUP["FTP"], ) -> bool: """ Request a file from a target IP address. diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 38a253be..671200f5 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -3,9 +3,9 @@ from typing import Any, Optional from primaite import getLogger from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -52,7 +52,7 @@ class FTPServer(FTPServiceABC): # process server specific commands, otherwise call super if payload.ftp_command == FTPCommand.PORT: # check that the port is valid - if isinstance(payload.ftp_command_args, int) and (0 <= payload.ftp_command_args < 65535): + if is_valid_port(payload.ftp_command_args): # return successful connection self.add_connection(connection_id=session_id, session_id=session_id) payload.status_code = FTPStatusCode.OK diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 49678c82..77d82997 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -6,6 +6,7 @@ from typing import Dict, Optional from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.system.services.service import Service +from primaite.utils.validation.port import Port class FTPServiceABC(Service, ABC): @@ -77,7 +78,7 @@ class FTPServiceABC(Service, ABC): dest_folder_name: str, dest_file_name: str, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = None, + dest_port: Optional[Port] = None, session_id: Optional[str] = None, is_response: bool = False, ) -> bool: @@ -97,7 +98,7 @@ class FTPServiceABC(Service, ABC): :type: dest_ip_address: Optional[IPv4Address] :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. - :type: dest_port: Optional[int] + :type: dest_port: Optional[Port] :param: session_id: session ID linked to the FTP Packet. Optional. :type: session_id: Optional[str] @@ -167,7 +168,7 @@ class FTPServiceABC(Service, ABC): payload: FTPPacket, session_id: Optional[str] = None, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = None, + dest_port: Optional[Port] = None, **kwargs, ) -> bool: """ diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 486ba2b0..84ad995d 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -7,9 +7,9 @@ from primaite import getLogger from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 184833e1..ed89971f 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -5,9 +5,9 @@ from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service, ServiceOperatingState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -55,7 +55,7 @@ class NTPClient(Service): payload: NTPPacket, session_id: Optional[str] = None, dest_ip_address: IPv4Address = None, - dest_port: int = PORT_LOOKUP["NTP"], + dest_port: Port = PORT_LOOKUP["NTP"], **kwargs, ) -> bool: """Requests NTP data from NTP server. diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index 4764bffb..b674a296 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -4,9 +4,9 @@ from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import Service -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 2b0bc02b..ae3557f7 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -17,10 +17,10 @@ from primaite.simulator.network.protocols.ssh import ( SSHTransportMessage, SSHUserCredentials, ) -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.service import Service, ServiceOperatingState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP # TODO 2824: Since remote terminal connections and remote user sessions are the same thing, we could refactor diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 2805b1b2..75d9c472 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -10,11 +10,11 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClientConnection from primaite.simulator.system.services.service import Service from primaite.simulator.system.software import SoftwareHealthState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -145,7 +145,7 @@ class WebServer(Service): payload: HttpResponsePacket, session_id: Optional[str] = None, dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[int] = None, + dest_port: Optional[Port] = None, **kwargs, ) -> bool: """ diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index d34678b9..6fb09a16 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -15,7 +15,8 @@ from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.sys_log import SysLog -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port if TYPE_CHECKING: from primaite.simulator.system.core.software_manager import SoftwareManager @@ -250,11 +251,11 @@ class IOSoftware(Software): "Indicates if the software uses TCP protocol for communication. Default is True." udp: bool = True "Indicates if the software uses UDP protocol for communication. Default is True." - port: int + port: Port "The port to which the software is connected." - listen_on_ports: Set[int] = Field(default_factory=set) + listen_on_ports: Set[Port] = Field(default_factory=set) "The set of ports to listen on." - protocol: str + protocol: IPProtocol "The IP Protocol the Software operates on." _connections: Dict[str, Dict] = {} "Active connections." @@ -386,7 +387,7 @@ class IOSoftware(Software): session_id: Optional[str] = None, dest_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None, dest_port: Optional[int] = None, - ip_protocol: str = PROTOCOL_LOOKUP["TCP"], + ip_protocol: IPProtocol = PROTOCOL_LOOKUP["TCP"], **kwargs, ) -> bool: """ diff --git a/src/primaite/utils/validation/__init__.py b/src/primaite/utils/validation/__init__.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/src/primaite/utils/validation/__init__.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/validation/ip_protocol.py b/src/primaite/utils/validation/ip_protocol.py new file mode 100644 index 00000000..4e358305 --- /dev/null +++ b/src/primaite/utils/validation/ip_protocol.py @@ -0,0 +1,47 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# Define a custom IP protocol validator +from typing import Any + +from pydantic import BeforeValidator, TypeAdapter, ValidationError +from typing_extensions import Annotated, Final + +PROTOCOL_LOOKUP: dict[str, str] = dict( + NONE="none", + TCP="tcp", + UDP="udp", + ICMP="icmp", +) +""" +Lookup table used for compatibility with PrimAITE <= 3.3. Configs with the capitalised protocol names are converted +to lowercase at runtime. +""" +VALID_PROTOCOLS = ["none", "tcp", "udp", "icmp"] +"""Supported protocols.""" + + +def protocol_validator(v: Any) -> str: + """ + Validate that IP Protocols are chosen from the list of supported IP Protocols. + + The protocol list is dynamic because plugins are able to extend it, therefore it is necessary to use this custom + validator instead of being able to specify a union of string literals. + """ + if isinstance(v, str) and v in PROTOCOL_LOOKUP: + return PROTOCOL_LOOKUP[v] + if v in VALID_PROTOCOLS: + return v + raise ValueError(f"{v} is not a valid IP Protocol. It must be one of the following: {VALID_PROTOCOLS}") + + +IPProtocol: Final[Annotated] = Annotated[str, BeforeValidator(protocol_validator)] +"""Validates that IP Protocols used in the simulation belong to the list of supported protocols.""" +_IPProtocolTypeAdapter = TypeAdapter(IPProtocol) + + +def is_valid_protocol(v: Any) -> bool: + """Convenience method to return true if the value matches the schema, and false otherwise.""" + try: + _IPProtocolTypeAdapter.validate_python(v) + return True + except ValidationError: + return False diff --git a/src/primaite/utils/validators.py b/src/primaite/utils/validation/ipv4_address.py similarity index 59% rename from src/primaite/utils/validators.py rename to src/primaite/utils/validation/ipv4_address.py index f07b475d..eb0e2574 100644 --- a/src/primaite/utils/validators.py +++ b/src/primaite/utils/validation/ipv4_address.py @@ -1,4 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + + from ipaddress import IPv4Address from typing import Any, Final @@ -37,39 +39,3 @@ will automatically check and convert the input value to an instance of IPv4Addre any Pydantic model uses it. This ensures that any field marked with this type is not just an IPv4Address in form, but also valid according to the rules defined in ipv4_validator. """ - -# Define a custom port validator -Port: Final[Annotated] = Annotated[int, BeforeValidator(lambda n: 0 <= n <= 65535)] -"""Validates that network ports lie in the appropriate range of [0,65535].""" - -# Define a custom IP protocol validator -PROTOCOL_LOOKUP: dict[str, str] = dict( - NONE="none", - TCP="tcp", - UDP="udp", - ICMP="icmp", -) -""" -Lookup table used for compatibility with PrimAITE <= 3.3. Configs with the capitalised protocol names are converted -to lowercase at runtime. -""" -VALID_PROTOCOLS = ["none", "tcp", "udp", "icmp"] -"""Supported protocols.""" - - -def protocol_validator(v: Any) -> str: - """ - Validate that IP Protocols are chosen from the list of supported IP Protocols. - - The protocol list is dynamic because plugins are able to extend it, therefore it is necessary to use this custom - validator instead of being able to specify a union of string literals. - """ - if v in PROTOCOL_LOOKUP: - return PROTOCOL_LOOKUP(v) - if v in VALID_PROTOCOLS: - return v - raise ValueError(f"{v} is not a valid IP Protocol. It must be one of the following: {VALID_PROTOCOLS}") - - -IPProtocol: Final[Annotated] = Annotated[str, BeforeValidator(protocol_validator)] -"""Validates that IP Protocols used in the simulation belong to the list of supported protocols.""" diff --git a/src/primaite/utils/validation/port.py b/src/primaite/utils/validation/port.py new file mode 100644 index 00000000..90c36add --- /dev/null +++ b/src/primaite/utils/validation/port.py @@ -0,0 +1,70 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# Define a custom port validator +from typing import Any + +from pydantic import BeforeValidator, TypeAdapter, ValidationError +from typing_extensions import Annotated, Final + +PORT_LOOKUP: dict[str, int] = dict( + UNUSED=-1, + NONE=0, + WOL=9, + FTP_DATA=20, + FTP=21, + SSH=22, + SMTP=25, + DNS=53, + HTTP=80, + POP3=110, + SFTP=115, + NTP=123, + IMAP=143, + SNMP=161, + SNMP_TRAP=162, + ARP=219, + LDAP=389, + HTTPS=443, + SMB=445, + IPP=631, + SQL_SERVER=1433, + MYSQL=3306, + RDP=3389, + RTP=5004, + RTP_ALT=5005, + DNS_ALT=5353, + HTTP_ALT=8080, + HTTPS_ALT=8443, + POSTGRES_SERVER=5432, +) +""" +Lookup table used for compatibility with PrimAITE <= 3.3. Configs with named ports names are converted +to port integers at runtime. +""" + + +def port_validator(v: Any) -> int: + """ + Validate that Ports are chosen from the list of supported Ports. + + The protocol list is dynamic because plugins are able to extend it, therefore it is necessary to use this custom + validator instead of being able to specify a union of string literals. + """ + if isinstance(v, str) and v in PORT_LOOKUP: + v = PORT_LOOKUP[v] + if isinstance(v, int) and (0 <= v <= 65535): + return v + raise ValueError(f"{v} is not a valid Port. It must be an integer in the range [0,65535] or ") + + +Port: Final[Annotated] = Annotated[int, BeforeValidator(port_validator)] +"""Validates that network ports lie in the appropriate range of [0,65535].""" +_PortTypeAdapter = TypeAdapter(Port) + + +def is_valid_port(v: Any) -> bool: + """Convenience method to return true if the value matches the schema, and false otherwise.""" + try: + _PortTypeAdapter.validate_python(v) + return True + except ValidationError: + return False diff --git a/tests/conftest.py b/tests/conftest.py index 687bec92..64fe0699 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,6 @@ from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.networks import arcd_uc2_network -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.web_browser import WebBrowser @@ -27,7 +26,8 @@ from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT rayinit() diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py index 6d0ef7b0..7f251613 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -9,8 +9,8 @@ from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests.integration_tests.configuration_file_parsing import BASIC_FIREWALL, DMZ_NETWORK, load_config diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index c348ee81..d10c7dbb 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -6,8 +6,8 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 28029b32..70dc7cba 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -15,11 +15,11 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 55bdce09..80f7e3c3 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -3,7 +3,7 @@ from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC from primaite.simulator.system.services.ftp.ftp_client import FTPClient -from primaite.utils.validators import IPV4Address +from primaite.utils.validation.ipv4_address import IPV4Address class SuperComputer(HostNode, identifier="supercomputer"): diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index 70d47aaa..ddaf4a1e 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -7,12 +7,12 @@ from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.file_system.folder import Folder -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 2c750621..187fb1fe 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -11,13 +11,13 @@ from primaite.simulator.network.hardware.base import UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server from primaite.simulator.system.services.database.database_service import DatabaseService 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.service import ServiceOperatingState +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index b56a4b99..508bd5a4 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -11,12 +11,12 @@ from primaite.game.agent.actions import ( ) from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT from tests.conftest import ControlledAgent diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index bc168c3c..a70cea72 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -9,9 +9,9 @@ from primaite.simulator.network.hardware.base import UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index 2bf0486c..e7212f3c 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -4,10 +4,10 @@ import pytest from primaite.game.agent.observations.acl_observation import ACLObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index af8c4669..05cf910c 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -5,8 +5,8 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP def check_default_rules(acl_obs): diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index cdd428b0..4ced02f5 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -8,9 +8,9 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.sim_container import Simulation -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP def test_router_observation(): diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index 70637b0d..e7287eee 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -3,7 +3,7 @@ import pytest from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT DATA_MANIPULATION_CONFIG = TEST_ASSETS_ROOT / "configs" / "data_manipulation.yaml" diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 2675b615..e03a7d26 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -21,11 +21,11 @@ from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.software import SoftwareHealthState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT FIREWALL_ACTIONS_NETWORK = TEST_ASSETS_ROOT / "configs/firewall_actions_network.yaml" diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 0afe666c..0005b508 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -9,11 +9,11 @@ from primaite.interface.request import RequestResponse from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT from tests.conftest import ControlledAgent diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index b5b2acbc..f07f02e7 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -8,10 +8,10 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.service import Service -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP class BroadcastTestService(Service): diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 58763c3e..79452318 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -7,10 +7,10 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index dde66a43..04cdbe78 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -6,10 +6,10 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index 520ec21a..fb0035e9 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -7,8 +7,8 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index b1979154..2cbd4d11 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -13,7 +13,6 @@ from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import AccessControlList, ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon @@ -24,7 +23,8 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer 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.web_server.web_server import WebServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 54c372e4..50b0ceac 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -9,7 +9,6 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( @@ -19,6 +18,7 @@ from primaite.simulator.system.applications.red_applications.data_manipulation_b from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index ad0a519b..1a09e875 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -8,12 +8,12 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index 09cbcf85..a5adbb04 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -9,11 +9,11 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index c1c4df82..c52b5caa 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -5,9 +5,9 @@ from ipaddress import IPv4Address, IPv4Network import yaml from primaite.game.game import PrimaiteGame -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.nmap import NMAP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT 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 4108041d..7a085ee1 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -6,11 +6,11 @@ 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.transport_layer import PORT_LOOKUP 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 primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 854ef41b..f2ac1183 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -9,7 +9,6 @@ from primaite.simulator.network.hardware.base import Link from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService @@ -17,6 +16,7 @@ from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.web_server.web_server import WebServer from primaite.simulator.system.software import SoftwareHealthState +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index 7813628c..a767f365 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -12,7 +12,7 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP from tests.conftest import DummyApplication, DummyService diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index ba7628c2..6eca0c44 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -8,8 +8,9 @@ from primaite.simulator.network.hardware.nodes.network.router import ACLAction, from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.network_layer import IPPacket -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPHeader, UDPHeader -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index 0e1844c4..fe9387de 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -2,8 +2,8 @@ from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP def test_wireless_router_from_config(): diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py index 9e9a1f72..e7e425b1 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py @@ -5,8 +5,9 @@ from primaite.simulator.network.protocols.icmp import ICMPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.network_layer import IPPacket, Precedence from primaite.simulator.network.transmission.primaite_layer import AgentSource, DataStatus -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP, TCPFlags, TCPHeader, UDPHeader -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.simulator.network.transmission.transport_layer import TCPFlags, TCPHeader, UDPHeader +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP def test_frame_minimal_instantiation(): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index fde70616..12dddf67 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -4,11 +4,11 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index f4750158..34a29cd0 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -3,13 +3,13 @@ import pytest from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.networks import arcd_uc2_network -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( DataManipulationAttackStage, DataManipulationBot, ) -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index d0c65266..e9762476 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -5,9 +5,9 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.red_applications.dos_bot import DoSAttackStage, DoSBot +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index f5781485..f1be475a 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -4,10 +4,10 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.http import HttpResponsePacket, HttpStatusCode -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 09099c5c..db7e8d58 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -6,10 +6,10 @@ import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.service import ServiceOperatingState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 688bfd7d..c64602c0 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -8,10 +8,10 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index b4fe8633..95788834 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -8,10 +8,10 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import ServiceOperatingState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 3f10db4d..291cdede 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -6,10 +6,10 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.service import ServiceOperatingState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index f2895091..9b6a4bf3 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -18,13 +18,13 @@ from primaite.simulator.network.protocols.ssh import ( SSHTransportMessage, SSHUserCredentials, ) -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection, Terminal from primaite.simulator.system.services.web_server.web_server import WebServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index c78a381e..54f86ec8 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -9,9 +9,9 @@ from primaite.simulator.network.protocols.http import ( HttpResponsePacket, HttpStatusCode, ) -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.services.web_server.web_server import WebServer -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 1baaf88e..300f8d9d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -3,11 +3,11 @@ from typing import Dict import pytest -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.service import Service from primaite.simulator.system.software import IOSoftware, SoftwareHealthState -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP class TestSoftware(Service): diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index 10ed36e0..1a1848ac 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.simulator.network.transmission.transport_layer import PORT_LOOKUP from primaite.utils.converters import convert_dict_enum_keys_to_enum_values -from primaite.utils.validators import PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP def test_simple_conversion(): From c3eb093144bf96277b2d62f3ddd1797861602234 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 25 Sep 2024 16:50:01 +0100 Subject: [PATCH 006/224] remove temporary notebook --- notebooks/test.ipynb | 157 ------------------------------------------- 1 file changed, 157 deletions(-) delete mode 100644 notebooks/test.ipynb diff --git a/notebooks/test.ipynb b/notebooks/test.ipynb deleted file mode 100644 index 5afe04b0..00000000 --- a/notebooks/test.ipynb +++ /dev/null @@ -1,157 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import yaml\n", - "\n", - "from primaite.game.game import PrimaiteGame\n", - "from primaite.session.environment import PrimaiteGymEnv\n", - "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", - "from primaite.simulator.network.hardware.nodes.host.server import Server\n", - "from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection\n", - "from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot\n", - "from primaite.simulator.system.services.database.database_service import DatabaseService\n", - "from primaite import getLogger, PRIMAITE_PATHS" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "with open(PRIMAITE_PATHS.user_config_path / \"example_config\" / \"data_manipulation.yaml\") as f:\n", - " cfg = yaml.safe_load(f)\n", - "game = PrimaiteGame.from_config(cfg)\n", - "uc2_network = game.simulation.network" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "client_1: Computer = uc2_network.get_node_by_hostname(\"client_1\")\n", - "db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get(\"DataManipulationBot\")\n", - "\n", - "database_server: Server = uc2_network.get_node_by_hostname(\"database_server\")\n", - "db_service: DatabaseService = database_server.software_manager.software.get(\"DatabaseService\")\n", - "\n", - "web_server: Server = uc2_network.get_node_by_hostname(\"web_server\")\n", - "db_client: DatabaseClient = web_server.software_manager.software.get(\"DatabaseClient\")\n", - "db_connection: DatabaseClientConnection = db_client.get_new_connection()\n", - "db_service.backup_database()\n", - "\n", - "# First check that the DB client on the web_server can successfully query the users table on the database\n", - "assert db_connection.query(\"SELECT\")\n", - "\n", - "db_manipulation_bot.data_manipulation_p_of_success = 1.0\n", - "db_manipulation_bot.port_scan_p_of_success = 1.0\n", - "\n", - "# Now we run the DataManipulationBot\n", - "db_manipulation_bot.attack()\n", - "\n", - "# Now check that the DB client on the web_server cannot query the users table on the database\n", - "assert not db_connection.query(\"SELECT\")\n", - "\n", - "# Now restore the database\n", - "db_service.restore_backup()\n", - "\n", - "# Now check that the DB client on the web_server can successfully query the users table on the database\n", - "assert db_connection.query(\"SELECT\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "router = uc2_network.get_node_by_hostname(\"router_1\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----------------------------------------------------------------------------------------------------------------+\n", - "| router_1 Access Control List |\n", - "+-------+--------+----------+--------+--------------+-------------+--------+--------------+-------------+---------+\n", - "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", - "+-------+--------+----------+--------+--------------+-------------+--------+--------------+-------------+---------+\n", - "| 18 | PERMIT | ANY | ANY | ANY | 5432 (5432) | ANY | ANY | 5432 (5432) | 4 |\n", - "| 19 | PERMIT | ANY | ANY | ANY | 53 (53) | ANY | ANY | 53 (53) | 0 |\n", - "| 20 | PERMIT | ANY | ANY | ANY | 21 (21) | ANY | ANY | 21 (21) | 0 |\n", - "| 21 | PERMIT | ANY | ANY | ANY | 80 (80) | ANY | ANY | 80 (80) | 0 |\n", - "| 22 | PERMIT | ANY | ANY | ANY | 219 (219) | ANY | ANY | 219 (219) | 9 |\n", - "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "+-------+--------+----------+--------+--------------+-------------+--------+--------------+-------------+---------+\n" - ] - } - ], - "source": [ - "router.acl.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "AccessControlList.is_permitted() missing 1 required positional argument: 'frame'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[6], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mrouter\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43macl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mis_permitted\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[0;31mTypeError\u001b[0m: AccessControlList.is_permitted() missing 1 required positional argument: 'frame'" - ] - } - ], - "source": [ - "router.acl.is_permitted()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 50e2234a6951d3305d36f4877bcc966159fda53f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 25 Sep 2024 16:51:58 +0100 Subject: [PATCH 007/224] Remove commented out code --- .../agent/observations/host_observations.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 96c5f40d..617e8eee 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -57,27 +57,6 @@ class HostObservation(AbstractObservation, identifier="HOST"): include_users: Optional[bool] = True """If True, report user session information.""" - # @field_validator("monitored_traffic", mode="before") - # def traffic_lookup(cls, val: Optional[Dict]) -> Optional[Dict]: - # """ - # Convert monitored_traffic by lookup against Port and Protocol dicts. - - # This is necessary for retaining compatiblility with configs written for PrimAITE <=3.3. - # This method will be removed in PrimAITE >= 4.0 - # """ - # if val is None: - # return val - # new_val = {} - # for proto, port_list in val.items(): - # # convert protocol, for instance ICMP becomes "icmp" - # proto = PROTOCOL_LOOKUP[proto] if proto in PROTOCOL_LOOKUP else proto - # new_val[proto] = [] - # for port in port_list: - # # convert ports, for instance "HTTP" becomes 80 - # port = PORT_LOOKUP[port] if port in PORT_LOOKUP else port - # new_val[proto].append(port) - # return new_val - def __init__( self, where: WhereType, From f2b6d68b14621d7a2badc44bfaf373eb8b9d466a Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 26 Sep 2024 15:35:50 +0100 Subject: [PATCH 008/224] Fix Port scan --- src/primaite/simulator/system/applications/nmap.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index a04067c4..e2b9117d 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -348,7 +348,7 @@ class NMAP(Application, identifier="NMAP"): if is_valid_port(target_port): target_port = [target_port] elif target_port is None: - target_port = [port for port in PORT_LOOKUP if port not in {PORT_LOOKUP["NONE"], PORT_LOOKUP["UNUSED"]}] + target_port = [PORT_LOOKUP[port] for port in PORT_LOOKUP if port not in {"NONE", "UNUSED"}] if is_valid_protocol(target_protocol): target_protocol = [target_protocol] @@ -358,7 +358,7 @@ class NMAP(Application, identifier="NMAP"): scan_type = self._determine_port_scan_type(list(ip_addresses), target_port) active_ports = {} if show: - table = PrettyTable(["IP Address", "Port", "Name", "Protocol"]) + table = PrettyTable(["IP Address", "Port", "Protocol"]) table.align = "l" table.title = f"{self.software_manager.node.hostname} NMAP Port Scan ({scan_type})" self.sys_log.info(f"{self.name}: Starting port scan") @@ -369,13 +369,12 @@ class NMAP(Application, identifier="NMAP"): for protocol in target_protocol: for port in set(target_port): port_open = self._check_port_open_on_ip_address(ip_address=ip_address, port=port, protocol=protocol) - if port_open: if show: - table.add_row([ip_address, port, port, protocol]) + table.add_row([ip_address, port, protocol]) _ip_address = ip_address if not json_serializable else str(ip_address) - _protocol = protocol if not json_serializable else protocol - _port = port if not json_serializable else port + _protocol = protocol + _port = port if _ip_address not in active_ports: active_ports[_ip_address] = dict() if _protocol not in active_ports[_ip_address]: From 203ec5ec856556fd0d5f81dc782e53ece2385893 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 26 Sep 2024 16:00:59 +0100 Subject: [PATCH 009/224] Add tests for port and protocol validation and update changelog --- CHANGELOG.md | 1 + src/primaite/simulator/network/airspace.py | 6 ++--- .../simulator/network/hardware/base.py | 5 ++-- .../red_applications/c2/abstract_c2.py | 2 +- .../_primaite/_utils/_validation/__init__.py | 1 + .../_utils/_validation/test_ip_protocol.py | 23 +++++++++++++++++ .../_primaite/_utils/_validation/test_port.py | 25 +++++++++++++++++++ 7 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 tests/unit_tests/_primaite/_utils/_validation/__init__.py create mode 100644 tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py create mode 100644 tests/unit_tests/_primaite/_utils/_validation/test_port.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d08974c..9493dec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - File and folder observations can now be configured to always show the true health status, or require scanning like before. - It's now possible to disable stickiness on reward components, meaning their value returns to 0 during timesteps where agent don't issue the corresponding action. Affects `GreenAdminDatabaseUnreachablePenalty`, `WebpageUnavailablePenalty`, `WebServer404Penalty` - Node observations can now be configured to show the number of active local and remote logins. +- Ports, IP Protocols, and airspace frequencies no longer use enums. They defined in dictionary lookups and are handled by custom validation to enable extendability with plugins. ### Fixed - Folder observations showing the true health state without scanning (the old behaviour can be reenabled via config) diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 65dceeb1..03d43130 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -48,11 +48,11 @@ _default_frequency_set: Dict[str, Dict] = { """Frequency configuration that is automatically used for any new airspace.""" -def register_default_frequency(freq_name: str, freq_hz: float, data_rate_bps: float): +def register_default_frequency(freq_name: str, freq_hz: float, data_rate_bps: float) -> None: """Add to the default frequency configuration. This is intended as a plugin hook. If your plugin makes use of bespoke frequencies for wireless communication, you should make a call to this method - whereever you define components that rely on the bespoke frequencies. That way, as soon as your components are + wherever you define components that rely on the bespoke frequencies. That way, as soon as your components are imported, this function automatically updates the default frequency set. This should also be run before instances of AirSpace are created. @@ -93,7 +93,7 @@ class AirSpace(BaseModel): return self.frequencies[freq_name]["data_rate_bps"] / (1024.0 * 1024.0) return 0.0 - def set_frequency_max_capacity_mbps(self, cfg: Dict[int, float]): + def set_frequency_max_capacity_mbps(self, cfg: Dict[int, float]) -> None: """ Sets custom maximum data transmission capacities for multiple frequencies. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 778cffa2..050f4667 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1839,15 +1839,14 @@ class Node(SimComponent): def show_open_ports(self, markdown: bool = False): """Prints a table of the open ports on the Node.""" - table = PrettyTable(["Port", "Name"]) + table = PrettyTable(["Port"]) if markdown: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.hostname} Open Ports" for port in self.software_manager.get_open_ports(): if port > 0: - # TODO: do a reverse lookup for port name, or change this to only show port int - table.add_row([port, port]) + table.add_row([port]) print(table.get_string(sortby="Port")) @property diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index aff12748..f77bc33a 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -366,7 +366,7 @@ class AbstractC2(Application, identifier="AbstractC2"): :return: True on successful configuration, false otherwise. :rtype: bool """ - # Validating that they are valid Enums. + # Validating that they are valid Ports and Protocols. if not is_valid_port(payload.masquerade_port) or not is_valid_protocol(payload.masquerade_protocol): self.sys_log.warning( f"{self.name}: Received invalid Masquerade Values within Keep Alive." diff --git a/tests/unit_tests/_primaite/_utils/_validation/__init__.py b/tests/unit_tests/_primaite/_utils/_validation/__init__.py new file mode 100644 index 00000000..be6c00e7 --- /dev/null +++ b/tests/unit_tests/_primaite/_utils/_validation/__init__.py @@ -0,0 +1 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py new file mode 100644 index 00000000..27829570 --- /dev/null +++ b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py @@ -0,0 +1,23 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.utils.validation.ip_protocol import IPProtocol, is_valid_protocol, PROTOCOL_LOOKUP, protocol_validator + + +def test_port_conversion(): + for proto_name, proto_val in PROTOCOL_LOOKUP.items(): + assert protocol_validator(proto_name) == proto_val + assert is_valid_protocol(proto_name) + + +def test_port_passthrough(): + for proto_val in PROTOCOL_LOOKUP.values(): + assert protocol_validator(proto_val) == proto_val + assert is_valid_protocol(proto_val) + + +def test_invalid_ports(): + for port in (123, "abcdefg", "NONEXISTENT_PROTO"): + with pytest.raises(ValueError): + protocol_validator(port) + assert not is_valid_protocol(port) diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_port.py b/tests/unit_tests/_primaite/_utils/_validation/test_port.py new file mode 100644 index 00000000..6a8a2429 --- /dev/null +++ b/tests/unit_tests/_primaite/_utils/_validation/test_port.py @@ -0,0 +1,25 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.utils.validation.port import is_valid_port, Port, PORT_LOOKUP, port_validator + + +def test_port_conversion(): + valid_port_lookup = {k: v for k, v in PORT_LOOKUP.items() if k != "UNUSED"} + for port_name, port_val in valid_port_lookup.items(): + assert port_validator(port_name) == port_val + assert is_valid_port(port_name) + + +def test_port_passthrough(): + valid_port_lookup = {k: v for k, v in PORT_LOOKUP.items() if k != "UNUSED"} + for port_val in valid_port_lookup.values(): + assert port_validator(port_val) == port_val + assert is_valid_port(port_val) + + +def test_invalid_ports(): + for port in (999999, -20, 3.214, "NONEXISTENT_PORT"): + with pytest.raises(ValueError): + port_validator(port) + assert not is_valid_port(port) From c74d5ac227d8bf947f2545870bc08a362671f52c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 27 Sep 2024 09:28:26 +0100 Subject: [PATCH 010/224] Fix changelog typo and remove repitition in ACL show method --- CHANGELOG.md | 2 +- .../simulator/network/hardware/nodes/network/router.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9493dec4..f51fd648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - File and folder observations can now be configured to always show the true health status, or require scanning like before. - It's now possible to disable stickiness on reward components, meaning their value returns to 0 during timesteps where agent don't issue the corresponding action. Affects `GreenAdminDatabaseUnreachablePenalty`, `WebpageUnavailablePenalty`, `WebServer404Penalty` - Node observations can now be configured to show the number of active local and remote logins. -- Ports, IP Protocols, and airspace frequencies no longer use enums. They defined in dictionary lookups and are handled by custom validation to enable extendability with plugins. +- Ports, IP Protocols, and airspace frequencies no longer use enums. They are defined in dictionary lookups and are handled by custom validation to enable extendability with plugins. ### Fixed - Folder observations showing the true health state without scanning (the old behaviour can be reenabled via config) diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 244f40ce..1080dca8 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -555,10 +555,10 @@ class AccessControlList(SimComponent): rule.protocol if rule.protocol else "ANY", rule.src_ip_address if rule.src_ip_address else "ANY", rule.src_wildcard_mask if rule.src_wildcard_mask else "ANY", - f"{rule.src_port} ({rule.src_port})" if rule.src_port else "ANY", + f"{rule.src_port}" if rule.src_port else "ANY", rule.dst_ip_address if rule.dst_ip_address else "ANY", rule.dst_wildcard_mask if rule.dst_wildcard_mask else "ANY", - f"{rule.dst_port} ({rule.dst_port})" if rule.dst_port else "ANY", + f"{rule.dst_port}" if rule.dst_port else "ANY", rule.match_count, ] ) From 5282cb0294174a903dfc6c4c60199d97a88fe60b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 27 Sep 2024 10:56:48 +0100 Subject: [PATCH 011/224] #2899 - Make software manager always `show()` all software --- src/primaite/simulator/system/core/software_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 60621384..04a3e3fb 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -267,7 +267,7 @@ class SoftwareManager: table.set_style(MARKDOWN) table.align = "l" table.title = f"{self.sys_log.hostname} Software Manager" - for software in self.port_protocol_mapping.values(): + for software in self.software.values(): software_type = "Service" if isinstance(software, Service) else "Application" table.add_row( [ From 221e09ba513f5a871e543e1ef8b4971e55a8bba2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 27 Sep 2024 15:06:19 +0100 Subject: [PATCH 012/224] Turn AirSpaceFrequency to a schema instead of a dict for validation --- src/primaite/simulator/network/airspace.py | 43 ++++++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 03d43130..20af3bfe 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -6,7 +6,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict, List from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel, Field, validate_call +from pydantic import BaseModel, ConfigDict, Field, validate_call from primaite import getLogger from primaite.simulator.network.hardware.base import Layer3Interface, NetworkInterface, WiredNetworkInterface @@ -41,9 +41,24 @@ def format_hertz(hertz: float, format_terahertz: bool = False, decimals: int = 3 return format_str.format(hertz) + " Hz" -_default_frequency_set: Dict[str, Dict] = { - "WIFI_2_4": {"frequency": 2.4e9, "data_rate_bps": 100_000_000.0}, - "WIFI_5": {"frequency": 5e9, "data_rate_bps": 500_000_000.0}, +class AirSpaceFrequency(BaseModel): + """Data transfer object for defining properties of an airspace frequency.""" + + model_config = ConfigDict(extra="forbid") + name: str + """Alias for frequency.""" + frequency_hz: int + """This acts as the primary key. If two names are mapped to the same frequency, they will share a bandwidth.""" + data_rate_bps: float + """How much data can be transmitted on this frequency per second.""" + + +_default_frequency_set: Dict[str, AirSpaceFrequency] = { + freq.name: freq + for freq in ( + AirSpaceFrequency(name="WIFI_2_4", frequency_hz=2.4e9, data_rate_bps=100_000_000.0), + AirSpaceFrequency(name="WIFI_5", frequency_hz=5e9, data_rate_bps=500_000_000.0), + ) } """Frequency configuration that is automatically used for any new airspace.""" @@ -64,7 +79,9 @@ def register_default_frequency(freq_name: str, freq_hz: float, data_rate_bps: fl :param data_rate_bps: The transmission capacity over this frequency, in bits per second. :type data_rate_bps: float """ - _default_frequency_set.update({freq_name: {"frequency": freq_hz, "data_rate_bps": data_rate_bps}}) + _default_frequency_set.update( + {freq_name: AirSpaceFrequency(name=freq_name, frequency_hz=freq_hz, data_rate_bps=data_rate_bps)} + ) class AirSpace(BaseModel): @@ -79,7 +96,7 @@ class AirSpace(BaseModel): wireless_interfaces: Dict[str, WirelessNetworkInterface] = Field(default_factory=lambda: {}) wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field(default_factory=lambda: {}) bandwidth_load: Dict[int, float] = Field(default_factory=lambda: {}) - frequencies: Dict[str, Dict] = Field(default_factory=lambda: copy.deepcopy(_default_frequency_set)) + frequencies: Dict[str, AirSpaceFrequency] = Field(default_factory=lambda: copy.deepcopy(_default_frequency_set)) @validate_call def get_frequency_max_capacity_mbps(self, freq_name: str) -> float: @@ -90,7 +107,7 @@ class AirSpace(BaseModel): :return: The maximum capacity in Mbps for the specified frequency. """ if freq_name in self.frequencies: - return self.frequencies[freq_name]["data_rate_bps"] / (1024.0 * 1024.0) + return self.frequencies[freq_name].data_rate_bps / (1024.0 * 1024.0) return 0.0 def set_frequency_max_capacity_mbps(self, cfg: Dict[int, float]) -> None: @@ -100,7 +117,7 @@ class AirSpace(BaseModel): :param cfg: A dictionary mapping frequencies to their new maximum capacities in Mbps. """ for freq, mbps in cfg.items(): - self.frequencies[freq]["data_rate_bps"] = mbps * 1024 * 1024 + self.frequencies[freq].data_rate_bps = mbps * 1024 * 1024 print(f"Overriding {freq} max capacity as {mbps:.3f} mbps") def register_frequency(self, freq_name: str, freq_hz: float, data_rate_bps: float) -> None: @@ -117,10 +134,12 @@ class AirSpace(BaseModel): if freq_name in self.frequencies: _LOGGER.info( f"Overwriting Air space frequency {freq_name}. " - f"Previous data rate: {self.frequencies[freq_name]['data_rate_bps']}. " + f"Previous data rate: {self.frequencies[freq_name].data_rate_bps}. " f"Current data rate: {data_rate_bps}." ) - self.frequencies.update({freq_name: {"frequency": freq_hz, "data_rate_bps": data_rate_bps}}) + self.frequencies.update( + {freq_name: AirSpaceFrequency(name=freq_name, frequency_hz=freq_hz, data_rate_bps=data_rate_bps)} + ) def show_bandwidth_load(self, markdown: bool = False): """ @@ -145,7 +164,7 @@ class AirSpace(BaseModel): load_percent = 1.0 table.add_row( [ - format_hertz(self.frequencies[frequency]["frequency"]), + format_hertz(self.frequencies[frequency].frequency_hz), f"{load_percent:.0%}", f"{maximum_capacity:.3f}", ] @@ -181,7 +200,7 @@ class AirSpace(BaseModel): interface.mac_address, interface.ip_address if hasattr(interface, "ip_address") else None, interface.subnet_mask if hasattr(interface, "subnet_mask") else None, - format_hertz(self.frequencies[interface.frequency]["frequency"]), + format_hertz(self.frequencies[interface.frequency].frequency_hz), f"{interface.speed:.3f}", status, ] From a838cc6ce160b8d4e06ab5433d8c65eab43aa69a Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 2 Oct 2024 13:56:39 +0100 Subject: [PATCH 013/224] Convert office lan adder to a class and make it extendable --- src/primaite/game/game.py | 6 + src/primaite/simulator/network/airspace.py | 50 +-- src/primaite/simulator/network/creation.py | 315 +++++++++++------- .../simulator/network/hardware/base.py | 19 ++ .../network/hardware/nodes/host/host_node.py | 22 +- .../hardware/nodes/network/network_node.py | 21 +- .../hardware/nodes/network/wireless_router.py | 8 +- 7 files changed, 248 insertions(+), 193 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 6d1c0920..9684e9e8 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -17,6 +17,7 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT +from primaite.simulator.network.creation import NetworkNodeAdder from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC @@ -270,6 +271,7 @@ class PrimaiteGame: nodes_cfg = network_config.get("nodes", []) links_cfg = network_config.get("links", []) + node_sets_cfg = network_config.get("node_sets", []) # Set the NMNE capture config NetworkInterface.nmne_config = NMNEConfig(**network_config.get("nmne_config", {})) @@ -505,6 +507,10 @@ class PrimaiteGame: new_node.start_up_duration = int(node_cfg.get("start_up_duration", 3)) new_node.shut_down_duration = int(node_cfg.get("shut_down_duration", 3)) + # 1.1 Create Node Sets + for node_set_cfg in node_sets_cfg: + NetworkNodeAdder.from_config(node_set_cfg, network=net) + # 2. create links between nodes for link_cfg in links_cfg: node_a = net.get_node_by_hostname(link_cfg["endpoint_a_hostname"]) diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 20af3bfe..2705c108 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -1,9 +1,8 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations -import copy from abc import ABC, abstractmethod -from typing import Any, Dict, List +from typing import Any, ClassVar, Dict, List from prettytable import MARKDOWN, PrettyTable from pydantic import BaseModel, ConfigDict, Field, validate_call @@ -52,36 +51,17 @@ class AirSpaceFrequency(BaseModel): data_rate_bps: float """How much data can be transmitted on this frequency per second.""" + _registry: ClassVar[Dict[str, AirSpaceFrequency]] = {} -_default_frequency_set: Dict[str, AirSpaceFrequency] = { - freq.name: freq - for freq in ( - AirSpaceFrequency(name="WIFI_2_4", frequency_hz=2.4e9, data_rate_bps=100_000_000.0), - AirSpaceFrequency(name="WIFI_5", frequency_hz=5e9, data_rate_bps=500_000_000.0), - ) -} -"""Frequency configuration that is automatically used for any new airspace.""" + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.name in self._registry: + raise RuntimeError(f"Frequency {self.name} is already registered. Cannot register it again.") + self._registry[self.name] = self -def register_default_frequency(freq_name: str, freq_hz: float, data_rate_bps: float) -> None: - """Add to the default frequency configuration. This is intended as a plugin hook. - - If your plugin makes use of bespoke frequencies for wireless communication, you should make a call to this method - wherever you define components that rely on the bespoke frequencies. That way, as soon as your components are - imported, this function automatically updates the default frequency set. - - This should also be run before instances of AirSpace are created. - - :param freq_name: The frequency name. If this clashes with an existing frequency name, it will be overwritten. - :type freq_name: str - :param freq_hz: The frequency itself, measured in Hertz. - :type freq_hz: float - :param data_rate_bps: The transmission capacity over this frequency, in bits per second. - :type data_rate_bps: float - """ - _default_frequency_set.update( - {freq_name: AirSpaceFrequency(name=freq_name, frequency_hz=freq_hz, data_rate_bps=data_rate_bps)} - ) +FREQ_WIFI_2_4 = AirSpaceFrequency(name="WIFI_2_4", frequency_hz=2.4e9, data_rate_bps=100_000_000.0) +FREQ_WIFI_5 = AirSpaceFrequency(name="WIFI_5", frequency_hz=5e9, data_rate_bps=500_000_000.0) class AirSpace(BaseModel): @@ -96,7 +76,7 @@ class AirSpace(BaseModel): wireless_interfaces: Dict[str, WirelessNetworkInterface] = Field(default_factory=lambda: {}) wireless_interfaces_by_frequency: Dict[int, List[WirelessNetworkInterface]] = Field(default_factory=lambda: {}) bandwidth_load: Dict[int, float] = Field(default_factory=lambda: {}) - frequencies: Dict[str, AirSpaceFrequency] = Field(default_factory=lambda: copy.deepcopy(_default_frequency_set)) + frequencies: Dict[str, AirSpaceFrequency] = AirSpaceFrequency._registry @validate_call def get_frequency_max_capacity_mbps(self, freq_name: str) -> float: @@ -228,9 +208,9 @@ class AirSpace(BaseModel): """ if wireless_interface.mac_address not in self.wireless_interfaces: self.wireless_interfaces[wireless_interface.mac_address] = wireless_interface - if wireless_interface.frequency not in self.wireless_interfaces_by_frequency: - self.wireless_interfaces_by_frequency[wireless_interface.frequency] = [] - self.wireless_interfaces_by_frequency[wireless_interface.frequency].append(wireless_interface) + if wireless_interface.frequency.frequency_hz not in self.wireless_interfaces_by_frequency: + self.wireless_interfaces_by_frequency[wireless_interface.frequency.frequency_hz] = [] + self.wireless_interfaces_by_frequency[wireless_interface.frequency.frequency_hz].append(wireless_interface) def remove_wireless_interface(self, wireless_interface: WirelessNetworkInterface): """ @@ -240,7 +220,7 @@ class AirSpace(BaseModel): """ if wireless_interface.mac_address in self.wireless_interfaces: self.wireless_interfaces.pop(wireless_interface.mac_address) - self.wireless_interfaces_by_frequency[wireless_interface.frequency].remove(wireless_interface) + self.wireless_interfaces_by_frequency[wireless_interface.frequency.frequency_hz].remove(wireless_interface) def clear(self): """ @@ -316,7 +296,7 @@ class WirelessNetworkInterface(NetworkInterface, ABC): """ airspace: AirSpace - frequency: str = "WIFI_2_4" + frequency: AirSpaceFrequency = FREQ_WIFI_2_4 def enable(self): """Attempt to enable the network interface.""" diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 891c445e..41fc3f87 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,6 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import ABC, abstractmethod from ipaddress import IPv4Address -from typing import Optional +from typing import Any, ClassVar, Dict, Type + +from pydantic import BaseModel from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -10,6 +13,204 @@ from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP +class NetworkNodeAdder(BaseModel): + """ + Base class for adding a set of related nodes to a network in a standardised way. + + Child classes should define a ConfigSchema nested class that subclasses NetworkNodeAdder.ConfigSchema and a __call__ + method which performs the node addition to the network. + + Here is a template that users can use to define custom node adders: + ``` + class YourNodeAdder(NetworkNodeAdder, identifier="your_name"): + class ConfigSchema(NetworkNodeAdder.ConfigSchema): + property_1 : str + property_2 : int + + @classmetho + def __call__() + ``` + """ + + class ConfigSchema(BaseModel, ABC): + """ + Base schema for node adders. + + Child classes of NetworkNodeAdder must define a schema which inherits from this schema. The identifier is used + by the from_config method to select the correct node adder at runtime. + """ + + identifier: str + """Uniquely identifies the node adder class to use for adding nodes to network.""" + + _registry: ClassVar[Dict[str, Type["NetworkNodeAdder"]]] = {} + + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: + """ + Register a network node adder class. + + :param identifier: Unique name for the node adder to use for matching against primaite config entries. + :type identifier: str + :raises ValueError: When attempting to register a name that is already reserved. + """ + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Duplicate node adder {identifier}") + cls._registry[identifier] = cls + + @classmethod + @abstractmethod + def add_nodes_to_net(cls, config: ConfigSchema, network: Network) -> None: + """ + Add nodes to the network. + + Abstract method that must be overwritten by child classes. Use the config definition to create nodes and add + them to the network that is passed in. + + :param config: Config object that defines how to create and add nodes to the network + :type config: ConfigSchema + :param network: PrimAITE network object to which to add nodes. + :type network: Network + """ + pass + + @classmethod + def from_config(cls, config: Dict, network: Network) -> None: + """ + Accept a config, find the relevant node adder class, and call it to add nodes to the network. + + Child classes do not need to define this method. + + :param config: Configuration object for the child adder class + :type config: Dict + :param network: The Network object to which to add nodes + :type network: Network + """ + if config["type"] not in cls._registry: + raise ValueError(f"Invalid node adder type {config['type']}") + adder_class = cls._registry[config["type"]] + adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config), network=network) + + +class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): + """Creates an office LAN.""" + + class ConfigSchema(NetworkNodeAdder.ConfigSchema): + """Configuration schema for OfficeLANAdder.""" + + lan_name: str + """Name of lan used for generating hostnames for new nodes.""" + subnet_base: int + """Used as the third octet of IP addresses for nodes in the network.""" + pcs_ip_block_start: int + """Starting point for the fourth octet of IP addresses of nodes in the network.""" + num_pcs: int + """The number of hosts to generate.""" + include_router: bool = True + """Whether to include a router in the new office LAN.""" + bandwidth: int = 100 + """Data bandwidth to the LAN measured in Mbps.""" + + @classmethod + def add_nodes_to_net(cls, config: ConfigSchema, network: Network) -> None: + """ + Add an office lan to the network according to the config definition. + + This method creates a number of hosts and enough switches such that all hosts can be connected to a switch. + Optionally, a router is added to connect the switches together. All the nodes and networking devices are added + to the provided network. + + :param config: Configuration object specifying office LAN parameters + :type config: OfficeLANAdder.ConfigSchema + :param network: The PrimAITE network to which to add the office LAN. + :type network: Network + :raises ValueError: upon invalid configuration + """ + # Calculate the required number of switches + num_of_switches = num_of_switches_required(num_nodes=config.num_pcs) + effective_network_interface = 23 # One port less for router connection + if config.pcs_ip_block_start <= num_of_switches: + raise ValueError( + f"pcs_ip_block_start must be greater than the number of required switches {num_of_switches}" + ) + + # Create a core switch if more than one edge switch is needed + if num_of_switches > 1: + core_switch = Switch(hostname=f"switch_core_{config.lan_name}", start_up_duration=0) + core_switch.power_on() + network.add_node(core_switch) + core_switch_port = 1 + + # Initialise the default gateway to None + default_gateway = None + + # Optionally include a router in the LAN + if config.include_router: + default_gateway = IPv4Address(f"192.168.{config.subnet_base}.1") + router = Router(hostname=f"router_{config.lan_name}", start_up_duration=0) + router.power_on() + router.acl.add_rule( + action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 + ) + router.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + network.add_node(router) + router.configure_port(port=1, ip_address=default_gateway, subnet_mask="255.255.255.0") + router.enable_port(1) + + # Initialise the first edge switch and connect to the router or core switch + switch_port = 0 + switch_n = 1 + switch = Switch(hostname=f"switch_edge_{switch_n}_{config.lan_name}", start_up_duration=0) + switch.power_on() + network.add_node(switch) + if num_of_switches > 1: + network.connect( + core_switch.network_interface[core_switch_port], + switch.network_interface[24], + bandwidth=config.bandwidth, + ) + else: + network.connect(router.network_interface[1], switch.network_interface[24], bandwidth=config.bandwidth) + + # Add PCs to the LAN and connect them to switches + for i in range(1, config.num_pcs + 1): + # Add a new edge switch if the current one is full + if switch_port == effective_network_interface: + switch_n += 1 + switch_port = 0 + switch = Switch(hostname=f"switch_edge_{switch_n}_{config.lan_name}", start_up_duration=0) + switch.power_on() + network.add_node(switch) + # Connect the new switch to the router or core switch + if num_of_switches > 1: + core_switch_port += 1 + network.connect( + core_switch.network_interface[core_switch_port], + switch.network_interface[24], + bandwidth=config.bandwidth, + ) + else: + network.connect( + router.network_interface[1], switch.network_interface[24], bandwidth=config.bandwidth + ) + + # Create and add a PC to the network + pc = Computer( + hostname=f"pc_{i}_{config.lan_name}", + ip_address=f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", + subnet_mask="255.255.255.0", + default_gateway=default_gateway, + start_up_duration=0, + ) + pc.power_on() + network.add_node(pc) + + # Connect the PC to the switch + switch_port += 1 + network.connect(switch.network_interface[switch_port], pc.network_interface[1], bandwidth=config.bandwidth) + switch.network_interface[switch_port].enable() + + def num_of_switches_required(num_nodes: int, max_network_interface: int = 24) -> int: """ Calculate the minimum number of network switches required to connect a given number of nodes. @@ -42,115 +243,3 @@ def num_of_switches_required(num_nodes: int, max_network_interface: int = 24) -> # Return the total number of switches required return full_switches + (1 if extra_pcs > 0 else 0) - - -def create_office_lan( - lan_name: str, - subnet_base: int, - pcs_ip_block_start: int, - num_pcs: int, - network: Optional[Network] = None, - include_router: bool = True, - bandwidth: int = 100, -) -> Network: - """ - Creates a 2-Tier or 3-Tier office local area network (LAN). - - The LAN is configured with a specified number of personal computers (PCs), optionally including a router, - and multiple edge switches to connect them. A core switch is added only if more than one edge switch is required. - The network topology involves edge switches connected either directly to the router in a 2-Tier setup or - to a core switch in a 3-Tier setup. If a router is included, it is connected to the core switch (if present) - and configured with basic access control list (ACL) rules. PCs are distributed across the edge switches. - - - :param str lan_name: The name to be assigned to the LAN. - :param int subnet_base: The subnet base number to be used in the IP addresses. - :param int pcs_ip_block_start: The starting block for assigning IP addresses to PCs. - :param int num_pcs: The number of PCs to be added to the LAN. - :param Optional[Network] network: The network to which the LAN components will be added. If None, a new network is - created. - :param bool include_router: Flag to determine if a router should be included in the LAN. Defaults to True. - :return: The network object with the LAN components added. - :raises ValueError: If pcs_ip_block_start is less than or equal to the number of required switches. - """ - # Initialise the network if not provided - if not network: - network = Network() - - # Calculate the required number of switches - num_of_switches = num_of_switches_required(num_nodes=num_pcs) - effective_network_interface = 23 # One port less for router connection - if pcs_ip_block_start <= num_of_switches: - raise ValueError(f"pcs_ip_block_start must be greater than the number of required switches {num_of_switches}") - - # Create a core switch if more than one edge switch is needed - if num_of_switches > 1: - core_switch = Switch(hostname=f"switch_core_{lan_name}", start_up_duration=0) - core_switch.power_on() - network.add_node(core_switch) - core_switch_port = 1 - - # Initialise the default gateway to None - default_gateway = None - - # Optionally include a router in the LAN - if include_router: - default_gateway = IPv4Address(f"192.168.{subnet_base}.1") - router = Router(hostname=f"router_{lan_name}", start_up_duration=0) - router.power_on() - router.acl.add_rule( - action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 - ) - router.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) - network.add_node(router) - router.configure_port(port=1, ip_address=default_gateway, subnet_mask="255.255.255.0") - router.enable_port(1) - - # Initialise the first edge switch and connect to the router or core switch - switch_port = 0 - switch_n = 1 - switch = Switch(hostname=f"switch_edge_{switch_n}_{lan_name}", start_up_duration=0) - switch.power_on() - network.add_node(switch) - if num_of_switches > 1: - network.connect( - core_switch.network_interface[core_switch_port], switch.network_interface[24], bandwidth=bandwidth - ) - else: - network.connect(router.network_interface[1], switch.network_interface[24], bandwidth=bandwidth) - - # Add PCs to the LAN and connect them to switches - for i in range(1, num_pcs + 1): - # Add a new edge switch if the current one is full - if switch_port == effective_network_interface: - switch_n += 1 - switch_port = 0 - switch = Switch(hostname=f"switch_edge_{switch_n}_{lan_name}", start_up_duration=0) - switch.power_on() - network.add_node(switch) - # Connect the new switch to the router or core switch - if num_of_switches > 1: - core_switch_port += 1 - network.connect( - core_switch.network_interface[core_switch_port], switch.network_interface[24], bandwidth=bandwidth - ) - else: - network.connect(router.network_interface[1], switch.network_interface[24], bandwidth=bandwidth) - - # Create and add a PC to the network - pc = Computer( - hostname=f"pc_{i}_{lan_name}", - ip_address=f"192.168.{subnet_base}.{i+pcs_ip_block_start-1}", - subnet_mask="255.255.255.0", - default_gateway=default_gateway, - start_up_duration=0, - ) - pc.power_on() - network.add_node(pc) - - # Connect the PC to the switch - switch_port += 1 - network.connect(switch.network_interface[switch_port], pc.network_interface[1], bandwidth=bandwidth) - switch.network_interface[switch_port].enable() - - return network diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 050f4667..b2bde574 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1539,6 +1539,25 @@ class Node(SimComponent): SYSTEM_SOFTWARE: ClassVar[Dict[str, Type[Software]]] = {} "Base system software that must be preinstalled." + _registry: ClassVar[Dict[str, Type["Node"]]] = {} + """Registry of application types. Automatically populated when subclasses are defined.""" + + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: + """ + Register a node type. + + :param identifier: Uniquely specifies an node class by name. Used for finding items by config. + :type identifier: str + :raises ValueError: When attempting to register an node with a name that is already allocated. + """ + if identifier == "default": + return + identifier = identifier.lower() + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Tried to define new node {identifier}, but this name is already reserved.") + cls._registry[identifier] = cls + def __init__(self, **kwargs): """ Initialize the Node with various components and managers. diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 5699721b..de119562 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -2,7 +2,7 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Optional, Type +from typing import Any, ClassVar, Dict, Optional from primaite import getLogger from primaite.simulator.network.hardware.base import ( @@ -325,30 +325,10 @@ class HostNode(Node): network_interface: Dict[int, NIC] = {} "The NICs on the node by port id." - _registry: ClassVar[Dict[str, Type["HostNode"]]] = {} - """Registry of application types. Automatically populated when subclasses are defined.""" - def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask)) - def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: - """ - Register a hostnode type. - - :param identifier: Uniquely specifies an hostnode class by name. Used for finding items by config. - :type identifier: str - :raises ValueError: When attempting to register an hostnode with a name that is already allocated. - """ - if identifier == "default": - return - # Enforce lowercase registry entries because it makes comparisons everywhere else much easier. - identifier = identifier.lower() - super().__init_subclass__(**kwargs) - if identifier in cls._registry: - raise ValueError(f"Tried to define new hostnode {identifier}, but this name is already reserved.") - cls._registry[identifier] = cls - @property def nmap(self) -> Optional[NMAP]: """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index a0cb63e1..5ff791cc 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod -from typing import Any, ClassVar, Dict, Optional, Type +from typing import Optional from primaite.simulator.network.hardware.base import NetworkInterface, Node from primaite.simulator.network.transmission.data_link_layer import Frame @@ -16,25 +16,6 @@ class NetworkNode(Node): provide functionality for receiving and processing frames received on their network interfaces. """ - _registry: ClassVar[Dict[str, Type["NetworkNode"]]] = {} - """Registry of application types. Automatically populated when subclasses are defined.""" - - def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: - """ - Register a networknode type. - - :param identifier: Uniquely specifies an networknode class by name. Used for finding items by config. - :type identifier: str - :raises ValueError: When attempting to register an networknode with a name that is already allocated. - """ - if identifier == "default": - return - identifier = identifier.lower() - super().__init_subclass__(**kwargs) - if identifier in cls._registry: - raise ValueError(f"Tried to define new networknode {identifier}, but this name is already reserved.") - cls._registry[identifier] = cls - @abstractmethod def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 27a13154..00ac9ca4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Union from pydantic import validate_call -from primaite.simulator.network.airspace import AirSpace, IPWirelessNetworkInterface +from primaite.simulator.network.airspace import AirSpace, AirSpaceFrequency, FREQ_WIFI_2_4, IPWirelessNetworkInterface from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router, RouterInterface from primaite.simulator.network.transmission.data_link_layer import Frame @@ -153,7 +153,7 @@ class WirelessRouter(Router): self, ip_address: IPV4Address, subnet_mask: IPV4Address, - frequency: Optional[str] = "WIFI_2_4", + frequency: Optional[AirSpaceFrequency] = FREQ_WIFI_2_4, ): """ Configures a wireless access point (WAP). @@ -171,7 +171,7 @@ class WirelessRouter(Router): communication. Default is "WIFI_2_4". """ if not frequency: - frequency = "WIFI_2_4" + frequency = FREQ_WIFI_2_4 self.sys_log.info("Configuring wireless access point") self.wireless_access_point.disable() # Temporarily disable the WAP for reconfiguration @@ -264,7 +264,7 @@ class WirelessRouter(Router): if "wireless_access_point" in cfg: ip_address = cfg["wireless_access_point"]["ip_address"] subnet_mask = cfg["wireless_access_point"]["subnet_mask"] - frequency = cfg["wireless_access_point"]["frequency"] + frequency = AirSpaceFrequency._registry[cfg["wireless_access_point"]["frequency"]] router.configure_wireless_access_point(ip_address=ip_address, subnet_mask=subnet_mask, frequency=frequency) if "acl" in cfg: From 5fde945facd30e1161e03593a55744670fb3778e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 2 Oct 2024 14:24:59 +0100 Subject: [PATCH 014/224] rename identifier back to type --- src/primaite/simulator/network/creation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 41fc3f87..2eefa75f 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -40,7 +40,7 @@ class NetworkNodeAdder(BaseModel): by the from_config method to select the correct node adder at runtime. """ - identifier: str + type: str """Uniquely identifies the node adder class to use for adding nodes to network.""" _registry: ClassVar[Dict[str, Type["NetworkNodeAdder"]]] = {} From b5b7fc6a8d0a9f6078b71db7a5d865ce53607e8e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 4 Oct 2024 09:20:07 +0100 Subject: [PATCH 015/224] Give node types their own identifiers and make the network show method use them --- src/primaite/game/game.py | 32 +++++++++---------- src/primaite/simulator/network/container.py | 5 ++- .../simulator/network/hardware/base.py | 4 +++ .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../network/hardware/nodes/host/server.py | 4 +-- .../hardware/nodes/network/firewall.py | 2 +- .../hardware/nodes/network/network_node.py | 2 +- .../network/hardware/nodes/network/router.py | 2 +- .../network/hardware/nodes/network/switch.py | 2 +- .../hardware/nodes/network/wireless_router.py | 2 +- 11 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 9684e9e8..f6b5b62c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -18,7 +18,7 @@ from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.creation import NetworkNodeAdder -from primaite.simulator.network.hardware.base import NetworkInterface, NodeOperatingState, UserManager +from primaite.simulator.network.hardware.base import NetworkInterface, Node, NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC from primaite.simulator.network.hardware.nodes.host.server import Printer, Server @@ -280,21 +280,7 @@ class PrimaiteGame: new_node = None # Handle extended nodes - if n_type.lower() in HostNode._registry: - new_node = HostNode._registry[n_type]( - hostname=node_cfg["hostname"], - ip_address=node_cfg["ip_address"], - subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - default_gateway=node_cfg.get("default_gateway"), - dns_server=node_cfg.get("dns_server", None), - operating_state=NodeOperatingState.ON - if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - elif n_type in NetworkNode._registry: - new_node = NetworkNode._registry[n_type](**node_cfg) - # Default PrimAITE nodes - elif n_type == "computer": + if n_type == "computer": new_node = Computer( hostname=node_cfg["hostname"], ip_address=node_cfg["ip_address"], @@ -339,6 +325,20 @@ class PrimaiteGame: if not (p := node_cfg.get("operating_state")) else NodeOperatingState[p.upper()], ) + elif n_type.lower() in Node._registry: + new_node = HostNode._registry[n_type]( + hostname=node_cfg["hostname"], + ip_address=node_cfg["ip_address"], + subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), + default_gateway=node_cfg.get("default_gateway"), + dns_server=node_cfg.get("dns_server", None), + operating_state=NodeOperatingState.ON + if not (p := node_cfg.get("operating_state")) + else NodeOperatingState[p.upper()], + ) + elif n_type in NetworkNode._registry: + new_node = NetworkNode._registry[n_type](**node_cfg) + # Default PrimAITE nodes else: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 6e019f32..1082e172 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -179,9 +179,8 @@ class Network(SimComponent): table.set_style(MARKDOWN) table.align = "l" table.title = "Nodes" - for node_type, nodes in nodes_type_map.items(): - for node in nodes: - table.add_row([node.hostname, node_type, node.operating_state.name]) + for node in self.nodes.values(): + table.add_row((node.hostname, type(node)._identifier, node.operating_state.name)) print(table) if ip_addresses: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index b2bde574..51e200e7 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1542,6 +1542,9 @@ class Node(SimComponent): _registry: ClassVar[Dict[str, Type["Node"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" + _identifier: ClassVar[str] = "unknown" + """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register a node type. @@ -1557,6 +1560,7 @@ class Node(SimComponent): if identifier in cls._registry: raise ValueError(f"Tried to define new node {identifier}, but this name is already reserved.") cls._registry[identifier] = cls + cls._identifier = identifier def __init__(self, **kwargs): """ diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 68c72554..4253d15c 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -5,7 +5,7 @@ from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.system.services.ftp.ftp_client import FTPClient -class Computer(HostNode): +class Computer(HostNode, identifier="computer"): """ A basic Computer class. diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index de119562..0c309136 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -262,7 +262,7 @@ class NIC(IPWiredNetworkInterface): return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" -class HostNode(Node): +class HostNode(Node, identifier="HostNode"): """ Represents a host node in the network. diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index 379c9927..bf1ef39b 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -2,7 +2,7 @@ from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -class Server(HostNode): +class Server(HostNode, identifier="server"): """ A basic Server class. @@ -31,7 +31,7 @@ class Server(HostNode): """ -class Printer(HostNode): +class Printer(HostNode, identifier="printer"): """Printer? I don't even know her!.""" # TODO: Implement printer-specific behaviour diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 47cfae57..84cf8530 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -27,7 +27,7 @@ DMZ_PORT_ID: Final[int] = 3 """The Firewall port ID of the DMZ port.""" -class Firewall(Router): +class Firewall(Router, identifier="firewall"): """ A Firewall class that extends the functionality of a Router. diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 5ff791cc..a5b8544f 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -7,7 +7,7 @@ from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.services.arp.arp import ARP -class NetworkNode(Node): +class NetworkNode(Node, identifier="NetworkNode"): """ Represents an abstract base class for a network node that can receive and process network frames. diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 1080dca8..e921faff 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1184,7 +1184,7 @@ class RouterSessionManager(SessionManager): return outbound_network_interface, dst_mac_address, dst_ip_address, src_port, dst_port, protocol, is_broadcast -class Router(NetworkNode): +class Router(NetworkNode, identifier="router"): """ Represents a network router, managing routing and forwarding of IP packets across network interfaces. diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 4324ac94..d29152a4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -87,7 +87,7 @@ class SwitchPort(WiredNetworkInterface): return False -class Switch(NetworkNode): +class Switch(NetworkNode, identifier="switch"): """ A class representing a Layer 2 network switch. diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 00ac9ca4..aed314d2 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -91,7 +91,7 @@ class WirelessAccessPoint(IPWirelessNetworkInterface): ) -class WirelessRouter(Router): +class WirelessRouter(Router, identifier="wireless_router"): """ A WirelessRouter class that extends the functionality of a standard Router to include wireless capabilities. From b4cc1b43790abb09eb0995b177c8451800e7c3dd Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 4 Oct 2024 11:07:49 +0100 Subject: [PATCH 016/224] Add tests for office lan creation --- src/primaite/simulator/network/creation.py | 4 +- .../_simulator/_network/test_creation.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/_primaite/_simulator/_network/test_creation.py diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 2eefa75f..80d0c370 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Type +from typing import Any, ClassVar, Dict, Literal, Type from pydantic import BaseModel @@ -98,6 +98,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): class ConfigSchema(NetworkNodeAdder.ConfigSchema): """Configuration schema for OfficeLANAdder.""" + type: Literal["office_lan"] = "office_lan" lan_name: str """Name of lan used for generating hostnames for new nodes.""" subnet_base: int @@ -197,6 +198,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Create and add a PC to the network pc = Computer( hostname=f"pc_{i}_{config.lan_name}", + # TODO: what happens when ip_block_start + num_pcs exceeds 254? ip_address=f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", subnet_mask="255.255.255.0", default_gateway=default_gateway, diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py new file mode 100644 index 00000000..f5b19179 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -0,0 +1,45 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address + +import pytest + +from primaite.simulator.network.container import Network +from primaite.simulator.network.creation import OfficeLANAdder + + +@pytest.mark.parametrize( + ("lan_name", "subnet_base", "pcs_ip_block_start", "num_pcs", "include_router", "bandwidth"), + ( + ("CORP-NETWORK", 3, 10, 6, True, 45), + ("OTHER-NETWORK", 10, 25, 26, True, 100), + ("OTHER-NETWORK", 10, 25, 55, False, 100), + ), +) +def test_office_lan_adder(lan_name, subnet_base, pcs_ip_block_start, num_pcs, include_router, bandwidth): + net = Network() + + office_lan_config = OfficeLANAdder.ConfigSchema( + lan_name=lan_name, + subnet_base=subnet_base, + pcs_ip_block_start=pcs_ip_block_start, + num_pcs=num_pcs, + include_router=include_router, + bandwidth=bandwidth, + ) + OfficeLANAdder.add_nodes_to_net(config=office_lan_config, network=net) + + num_switches = 1 if num_pcs <= 23 else num_pcs // 23 + 2 + num_routers = 1 if include_router else 0 + total_nodes = num_pcs + num_switches + num_routers + + assert all((n.hostname.endswith(lan_name) for n in net.nodes.values())) + assert len(net.computer_nodes) == num_pcs + assert len(net.switch_nodes) == num_switches + assert len(net.router_nodes) == num_routers + assert len(net.nodes) == total_nodes + assert all( + [str(n.network_interface[1].ip_address).startswith(f"192.168.{subnet_base}") for n in net.computer_nodes] + ) + # check that computers occupy address range 192.168.3.10 - 192.168.3.16 + computer_ip_last_octets = {str(n.network_interface[1].ip_address).split(".")[-1] for n in net.computer_nodes} + assert computer_ip_last_octets == {str(i) for i in range(pcs_ip_block_start, pcs_ip_block_start + num_pcs)} From 07c4860059d3a4a16b5f327e47a0caea67165056 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 4 Oct 2024 11:36:48 +0100 Subject: [PATCH 017/224] Add more tests for office lan creation --- .../_simulator/_network/test_creation.py | 68 +++++++++++++------ 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py index f5b19179..2e86ebbc 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -1,33 +1,20 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from ipaddress import IPv4Address - import pytest from primaite.simulator.network.container import Network -from primaite.simulator.network.creation import OfficeLANAdder +from primaite.simulator.network.creation import NetworkNodeAdder, OfficeLANAdder - -@pytest.mark.parametrize( - ("lan_name", "subnet_base", "pcs_ip_block_start", "num_pcs", "include_router", "bandwidth"), - ( - ("CORP-NETWORK", 3, 10, 6, True, 45), - ("OTHER-NETWORK", 10, 25, 26, True, 100), - ("OTHER-NETWORK", 10, 25, 55, False, 100), - ), +param_names = ("lan_name", "subnet_base", "pcs_ip_block_start", "num_pcs", "include_router", "bandwidth") +param_vals = ( + ("CORP-NETWORK", 3, 10, 6, True, 45), + ("OTHER-NETWORK", 10, 25, 26, True, 100), + ("OTHER-NETWORK", 10, 25, 55, False, 100), ) -def test_office_lan_adder(lan_name, subnet_base, pcs_ip_block_start, num_pcs, include_router, bandwidth): - net = Network() +param_dicts = [dict(zip(param_names, vals)) for vals in param_vals] - office_lan_config = OfficeLANAdder.ConfigSchema( - lan_name=lan_name, - subnet_base=subnet_base, - pcs_ip_block_start=pcs_ip_block_start, - num_pcs=num_pcs, - include_router=include_router, - bandwidth=bandwidth, - ) - OfficeLANAdder.add_nodes_to_net(config=office_lan_config, network=net) +def _assert_valid_creation(net: Network, lan_name, subnet_base, pcs_ip_block_start, num_pcs, include_router, bandwidth): + """Assert that the network contains the correct nodes as described by config items""" num_switches = 1 if num_pcs <= 23 else num_pcs // 23 + 2 num_routers = 1 if include_router else 0 total_nodes = num_pcs + num_switches + num_routers @@ -43,3 +30,40 @@ def test_office_lan_adder(lan_name, subnet_base, pcs_ip_block_start, num_pcs, in # check that computers occupy address range 192.168.3.10 - 192.168.3.16 computer_ip_last_octets = {str(n.network_interface[1].ip_address).split(".")[-1] for n in net.computer_nodes} assert computer_ip_last_octets == {str(i) for i in range(pcs_ip_block_start, pcs_ip_block_start + num_pcs)} + + +@pytest.mark.parametrize("kwargs", param_dicts) +def test_office_lan_adder(kwargs): + """Assert that adding an office lan via the python API works correctly.""" + net = Network() + + office_lan_config = OfficeLANAdder.ConfigSchema( + lan_name=kwargs["lan_name"], + subnet_base=kwargs["subnet_base"], + pcs_ip_block_start=kwargs["pcs_ip_block_start"], + num_pcs=kwargs["num_pcs"], + include_router=kwargs["include_router"], + bandwidth=kwargs["bandwidth"], + ) + OfficeLANAdder.add_nodes_to_net(config=office_lan_config, network=net) + + _assert_valid_creation(net=net, **kwargs) + + +@pytest.mark.parametrize("kwargs", param_dicts) +def test_office_lan_from_config(kwargs): + """Assert that the base class can add an office lan given a config dict.""" + net = Network() + + config = dict( + type="office_lan", + lan_name=kwargs["lan_name"], + subnet_base=kwargs["subnet_base"], + pcs_ip_block_start=kwargs["pcs_ip_block_start"], + num_pcs=kwargs["num_pcs"], + include_router=kwargs["include_router"], + bandwidth=kwargs["bandwidth"], + ) + + NetworkNodeAdder.from_config(config=config, network=net) + _assert_valid_creation(net=net, **kwargs) From 39c190e5f4b2a8a10bf23a662d57a565e4fe0919 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 4 Oct 2024 12:46:29 +0100 Subject: [PATCH 018/224] add documentation for node seta and update changelog --- CHANGELOG.md | 6 +- docs/index.rst | 1 + docs/source/node_sets.rst | 115 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 docs/source/node_sets.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index f51fd648..b0ee24be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added reward calculation details to AgentHistoryItem. - Added a new Privilege-Escalation-and Data-Loss-Example.ipynb notebook with a realistic cyber scenario focusing on internal privilege escalation and data loss through the manipulation of SSH access and Access Control Lists (ACLs). +- Added a new extendible `NetworkNodeAdder` class for convenient addition of sets of nodes based on a simplified config. ### Changed - File and folder observations can now be configured to always show the true health status, or require scanning like before. - It's now possible to disable stickiness on reward components, meaning their value returns to 0 during timesteps where agent don't issue the corresponding action. Affects `GreenAdminDatabaseUnreachablePenalty`, `WebpageUnavailablePenalty`, `WebServer404Penalty` - Node observations can now be configured to show the number of active local and remote logins. -- Ports, IP Protocols, and airspace frequencies no longer use enums. They are defined in dictionary lookups and are handled by custom validation to enable extendability with plugins. +- Ports and IP Protocolsno longer use enums. They are defined in dictionary lookups and are handled by custom validation to enable extendability with plugins. +- Changed AirSpaceFrequency to a data transfer object with a registry to allow extendability +- Changed the Office LAN creation convenience function to follow the new `NetworkNodeAdder` pattern. Office LANs can now also be defined in YAML config. ### Fixed - Folder observations showing the true health state without scanning (the old behaviour can be reenabled via config) @@ -32,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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`. +- Made the `show` method of `Network` show all node types, including ones registered at runtime ### Removed - Removed the `install` and `uninstall` methods in the `Node` class. diff --git a/docs/index.rst b/docs/index.rst index ff97f60d..1da15b8c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ What is PrimAITE? source/varying_config_files source/environment source/action_masking + source/node_sets .. toctree:: :caption: Notebooks: diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst new file mode 100644 index 00000000..2a1f74ce --- /dev/null +++ b/docs/source/node_sets.rst @@ -0,0 +1,115 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _network_node_adder: + +Network Node Adder Module +######################### + +This module provides a framework for adding nodes to a network in a standardised way. It defines a base class ``NetworkNodeAdder``, which can be extended to create specific node adders, and utility functions to calculate network infrastructure requirements. + +The module allows you to use the pre-defined node adders, ``OfficeLANAdder``, or create custom ones by extending the base class. + +How It Works +============ + +The main class in the module is ``NetworkNodeAdder``, which defines the interface for adding nodes to a network. Child classes are expected to: + +1. Define a ``ConfigSchema`` nested class to define configuration options. +2. Implement the ``add_nodes_to_net(config, network)`` method, which adds the nodes to the network according to the configuration object. + +The ``NetworkNodeAdder`` base class handles node adders defined in the primAITE config YAML file as well. It does this by keeping a registry of node adder classes, and uses the ``type`` field of the config to select the appropriate class to which to pass the configuration. + +Example Usage +============= + +Via Python API +-------------- + +Adding nodes to a network can be done using the python API by constructing the relevant ``ConfigSchema`` object like this: + +.. code-block:: python + + net = Network() + + office_lan_config = OfficeLANAdder.ConfigSchema( + lan_name="CORP-LAN", + subnet_base=2, + pcs_ip_block_start=10, + num_pcs=8, + include_router=False, + bandwidth=150, + ) + OfficeLANAdder.add_nodes_to_net(config=office_lan_config, network=net) + +In this example, a network with 8 computers connected by a switch will be added to the network object. + + +Via YAML Config +--------------- + +.. code-block:: yaml + simulation: + network: + nodes: + # ... nodes go here + node_sets: + - type: office_lan + lan_name: CORP_LAN + subnet_base: 2 + pcs_ip_block_start: 10 + num_pcs: 8 + include_router: False + bandwidth: 150 + # ... additional node sets can be added below + +``NetworkNodeAdder`` reads the ``type`` property of the config, then constructs and passes the configuration to ``OfficeLANAdder.add_nodes_to_net()``. + +In this example, a network with 8 computers connected by a switch will be added to the network object. Equivalent to the above. + + +Creating Custom Node Adders +=========================== +To create a custom node adder, subclass NetworkNodeAdder and define: + +* A ConfigSchema class that defines the configuration schema for the node adder. +* The add_nodes_to_net method that implements how nodes should be added to the network. + +Example: DataCenterAdder +------------------------ +Here is an example of creating a custom node adder, DataCenterAdder: + +.. code-block:: python + + class DataCenterAdder(NetworkNodeAdder, identifier="data_center"): + class ConfigSchema(NetworkNodeAdder.ConfigSchema): + type: Literal["data_center"] = "data_center" + num_servers: int + data_center_name: str + + @classmethod + def add_nodes_to_net(cls, config: ConfigSchema, network: Network) -> None: + for i in range(config.num_servers): + server = Computer( + hostname=f"server_{i}_{config.data_center_name}", + ip_address=f"192.168.100.{i + 1}", + subnet_mask="255.255.255.0", + default_gateway="192.168.100.1", + start_up_duration=0 + ) + server.power_on() + network.add_node(server) + +**Using the Custom Node Adder:** + +.. code-block:: python + + config = { + "type": "data_center", + "num_servers": 5, + "data_center_name": "dc1" + } + + network = Network() + DataCenterAdder.from_config(config, network) From 56a17c3feaae798fce35fd45581160891ddef4d1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 8 Oct 2024 13:40:40 +0100 Subject: [PATCH 019/224] Update typos and comments according to PR comments --- CHANGELOG.md | 6 +++--- docs/source/node_sets.rst | 2 +- src/primaite/game/game.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ee24be..e54f32e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added reward calculation details to AgentHistoryItem. - Added a new Privilege-Escalation-and Data-Loss-Example.ipynb notebook with a realistic cyber scenario focusing on internal privilege escalation and data loss through the manipulation of SSH access and Access Control Lists (ACLs). -- Added a new extendible `NetworkNodeAdder` class for convenient addition of sets of nodes based on a simplified config. +- Added a new extensible `NetworkNodeAdder` class for convenient addition of sets of nodes based on a simplified config. ### Changed - File and folder observations can now be configured to always show the true health status, or require scanning like before. - It's now possible to disable stickiness on reward components, meaning their value returns to 0 during timesteps where agent don't issue the corresponding action. Affects `GreenAdminDatabaseUnreachablePenalty`, `WebpageUnavailablePenalty`, `WebServer404Penalty` - Node observations can now be configured to show the number of active local and remote logins. -- Ports and IP Protocolsno longer use enums. They are defined in dictionary lookups and are handled by custom validation to enable extendability with plugins. -- Changed AirSpaceFrequency to a data transfer object with a registry to allow extendability +- Ports and IP Protocols no longer use enums. They are defined in dictionary lookups and are handled by custom validation to enable extensibility with plugins. +- Changed AirSpaceFrequency to a data transfer object with a registry to allow extensibility - Changed the Office LAN creation convenience function to follow the new `NetworkNodeAdder` pattern. Office LANs can now also be defined in YAML config. ### Fixed diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst index 2a1f74ce..866f0139 100644 --- a/docs/source/node_sets.rst +++ b/docs/source/node_sets.rst @@ -93,7 +93,7 @@ Here is an example of creating a custom node adder, DataCenterAdder: for i in range(config.num_servers): server = Computer( hostname=f"server_{i}_{config.data_center_name}", - ip_address=f"192.168.100.{i + 1}", + ip_address=f"192.168.100.{i + 8}", subnet_mask="255.255.255.0", default_gateway="192.168.100.1", start_up_duration=0 diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f6b5b62c..691ac2a1 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -279,7 +279,7 @@ class PrimaiteGame: n_type = node_cfg["type"] new_node = None - # Handle extended nodes + # Default PrimAITE nodes if n_type == "computer": new_node = Computer( hostname=node_cfg["hostname"], @@ -325,6 +325,7 @@ class PrimaiteGame: if not (p := node_cfg.get("operating_state")) else NodeOperatingState[p.upper()], ) + # Handle extended nodes elif n_type.lower() in Node._registry: new_node = HostNode._registry[n_type]( hostname=node_cfg["hostname"], @@ -338,7 +339,6 @@ class PrimaiteGame: ) elif n_type in NetworkNode._registry: new_node = NetworkNode._registry[n_type](**node_cfg) - # Default PrimAITE nodes else: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) From a42398ac0929abf3ac0de7aaa9e9da2dd7637fbb Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 9 Oct 2024 14:15:53 +0100 Subject: [PATCH 020/224] Fix typos and improve validation --- src/primaite/simulator/network/creation.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 80d0c370..31499359 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,9 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Literal, Type +from typing import Any, ClassVar, Dict, Literal, Self, Type -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -27,8 +27,12 @@ class NetworkNodeAdder(BaseModel): property_1 : str property_2 : int - @classmetho - def __call__() + @classmethod + def add_nodes_to_net(cls, config: ConfigSchema, network: Network) -> None: + node_1 = Node(property_1, ...) + node_2 = Node(...) + network.connect(node_1.network_interface[1], node_2.network_interface[1]) + ... ``` """ @@ -112,6 +116,16 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): bandwidth: int = 100 """Data bandwidth to the LAN measured in Mbps.""" + @model_validator(mode="after") + def check_ip_range(self) -> Self: + """Make sure the ip addresses of hosts don't exceed the maximum possible ip address.""" + if self.pcs_ip_block_start + self.num_pcs >= 254: + raise ValueError( + f"Cannot create {self.num_pcs} pcs starting at ip block {self.pcs_ip_block_start} " + f"because ip address octets cannot exceed 254." + ) + return self + @classmethod def add_nodes_to_net(cls, config: ConfigSchema, network: Network) -> None: """ From 611b34e29ff5f9ba0a7b2f727530d67a83ce7a1b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 9 Oct 2024 14:16:23 +0100 Subject: [PATCH 021/224] remove outdated comment --- src/primaite/simulator/network/creation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 31499359..8cc9a493 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -212,7 +212,6 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Create and add a PC to the network pc = Computer( hostname=f"pc_{i}_{config.lan_name}", - # TODO: what happens when ip_block_start + num_pcs exceeds 254? ip_address=f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", subnet_mask="255.255.255.0", default_gateway=default_gateway, From 43ec85a669d75c0329dac94f5efa69128458d3f4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 11 Oct 2024 09:52:16 +0100 Subject: [PATCH 022/224] #2755 - refix some air space frequency issues --- src/primaite/game/game.py | 2 +- src/primaite/simulator/network/airspace.py | 28 +++++++++++-------- src/primaite/simulator/network/creation.py | 4 +-- .../extensions/test_extendable_config.py | 8 +++--- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 691ac2a1..c8fbac4e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -329,7 +329,7 @@ class PrimaiteGame: elif n_type.lower() in Node._registry: new_node = HostNode._registry[n_type]( hostname=node_cfg["hostname"], - ip_address=node_cfg["ip_address"], + ip_address=node_cfg.get("ip_address"), subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), default_gateway=node_cfg.get("default_gateway"), dns_server=node_cfg.get("dns_server", None), diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 2705c108..2b8503d6 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -137,14 +137,16 @@ class AirSpace(BaseModel): table.set_style(MARKDOWN) table.align = "l" table.title = "Airspace Frequency Channel Loads" - for frequency, load in self.bandwidth_load.items(): - maximum_capacity = self.get_frequency_max_capacity_mbps(frequency) - load_percent = load / maximum_capacity if maximum_capacity > 0 else 0.0 + for freq_name, freq_obj in self.frequencies.items(): + maximum_capacity = self.get_frequency_max_capacity_mbps(freq_name) + load_percent = ( + self.bandwidth_load.get(freq_obj.frequency_hz, 0.0) / maximum_capacity if maximum_capacity > 0 else 0.0 + ) if load_percent > 1.0: load_percent = 1.0 table.add_row( [ - format_hertz(self.frequencies[frequency].frequency_hz), + format_hertz(self.frequencies[freq_name].frequency_hz), f"{load_percent:.0%}", f"{maximum_capacity:.3f}", ] @@ -180,7 +182,7 @@ class AirSpace(BaseModel): interface.mac_address, interface.ip_address if hasattr(interface, "ip_address") else None, interface.subnet_mask if hasattr(interface, "subnet_mask") else None, - format_hertz(self.frequencies[interface.frequency].frequency_hz), + format_hertz(self.frequencies[interface.frequency.name].frequency_hz), f"{interface.speed:.3f}", status, ] @@ -253,11 +255,11 @@ class AirSpace(BaseModel): relevant frequency and its current bandwidth load. :return: True if the frame can be transmitted within the bandwidth limit, False if it would exceed the limit. """ - if sender_network_interface.frequency not in self.bandwidth_load: - self.bandwidth_load[sender_network_interface.frequency] = 0.0 + if sender_network_interface.frequency.frequency_hz not in self.bandwidth_load: + self.bandwidth_load[sender_network_interface.frequency.frequency_hz] = 0.0 return self.bandwidth_load[ - sender_network_interface.frequency - ] + frame.size_Mbits <= self.get_frequency_max_capacity_mbps(sender_network_interface.frequency) + sender_network_interface.frequency.frequency_hz + ] + frame.size_Mbits <= self.get_frequency_max_capacity_mbps(sender_network_interface.frequency.name) def transmit(self, frame: Frame, sender_network_interface: WirelessNetworkInterface): """ @@ -269,8 +271,10 @@ class AirSpace(BaseModel): :param sender_network_interface: The wireless network interface sending the frame. This interface will be excluded from the list of receivers to prevent it from receiving its own transmission. """ - self.bandwidth_load[sender_network_interface.frequency] += frame.size_Mbits - for wireless_interface in self.wireless_interfaces_by_frequency.get(sender_network_interface.frequency, []): + self.bandwidth_load[sender_network_interface.frequency.frequency_hz] += frame.size_Mbits + for wireless_interface in self.wireless_interfaces_by_frequency.get( + sender_network_interface.frequency.frequency_hz, [] + ): if wireless_interface != sender_network_interface and wireless_interface.enabled: wireless_interface.receive_frame(frame) @@ -428,7 +432,7 @@ class IPWirelessNetworkInterface(WirelessNetworkInterface, Layer3Interface, ABC) # Update the state with information from Layer3Interface state.update(Layer3Interface.describe_state(self)) - state["frequency"] = self.frequency + state["frequency"] = self.frequency.name return state diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 8cc9a493..5d36f58b 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Literal, Self, Type +from typing import Any, ClassVar, Dict, Literal, Type from pydantic import BaseModel, model_validator @@ -117,7 +117,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): """Data bandwidth to the LAN measured in Mbps.""" @model_validator(mode="after") - def check_ip_range(self) -> Self: + def check_ip_range(self) -> "OfficeLANAdder.ConfigSchema": """Make sure the ip addresses of hosts don't exceed the maximum possible ip address.""" if self.pcs_ip_block_start + self.num_pcs >= 254: raise ValueError( diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 8467151b..5addcbd7 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -25,8 +25,8 @@ def test_extended_example_config(): assert len(network.router_nodes) == 1 # 1 router in network assert len(network.switch_nodes) == 1 # 1 switches in network assert len(network.server_nodes) == 5 # 5 servers in network - assert len(network.extended_hostnodes) == 1 # One extended node based on HostNode - assert len(network.extended_networknodes) == 1 # One extended node based on NetworkNode - assert "ExtendedApplication" in network.extended_hostnodes[0].software_manager.software - assert "ExtendedService" in network.extended_hostnodes[0].software_manager.software + extended_host = network.get_node_by_hostname("client_1") + + assert "ExtendedApplication" in extended_host.software_manager.software + assert "ExtendedService" in extended_host.software_manager.software From 6844bd692a603e476c5960a32bf145e6e708a7e2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 11 Oct 2024 13:02:44 +0100 Subject: [PATCH 023/224] bump version to 4.0.0a1 --- src/primaite/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/VERSION b/src/primaite/VERSION index 15a27998..21a8fb4d 100644 --- a/src/primaite/VERSION +++ b/src/primaite/VERSION @@ -1 +1 @@ -3.3.0 +4.0.0a1-dev From 861cfe2c0a2e6955c0f3e1d62f6963c160998d9e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 11 Oct 2024 15:00:26 +0100 Subject: [PATCH 024/224] #2912 - scaffold of action changes --- src/primaite/game/agent/actions.py | 573 +------------------- src/primaite/game/agent/actions/__init__.py | 0 src/primaite/game/agent/actions/manager.py | 487 +++++++++++++++++ src/primaite/game/agent/actions/service.py | 42 ++ 4 files changed, 534 insertions(+), 568 deletions(-) create mode 100644 src/primaite/game/agent/actions/__init__.py create mode 100644 src/primaite/game/agent/actions/manager.py create mode 100644 src/primaite/game/agent/actions/service.py diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 2e6189c0..1df25d27 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -9,91 +9,19 @@ AbstractAction. The ActionManager is responsible for: 3. Converting an action and parameter choice into a request which can be ingested by the PrimAITE simulation. This ensures that requests conform to the simulator's request format. """ -import itertools -from abc import ABC, abstractmethod -from typing import Dict, List, Literal, Optional, Tuple, TYPE_CHECKING, Union +from abc import abstractmethod +from typing import Dict, List, Literal, Optional, TYPE_CHECKING, Union -from gymnasium import spaces from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo from primaite import getLogger +from primaite.game.agent.actions.manager import ActionManager +from primaite.game.agent.actions.manager import AbstractAction +from primaite.game.agent.actions.service import NodeServiceAbstractAction from primaite.interface.request import RequestFormat _LOGGER = getLogger(__name__) -if TYPE_CHECKING: - from primaite.game.game import PrimaiteGame - - -class AbstractAction(ABC): - """Base class for actions.""" - - @abstractmethod - def __init__(self, manager: "ActionManager", **kwargs) -> None: - """ - Init method for action. - - All action init functions should accept **kwargs as a way of ignoring extra arguments. - - Since many parameters are defined for the action space as a whole (such as max files per folder, max services - per node), we need to pass those options to every action that gets created. To prevent verbosity, these - parameters are just broadcasted to all actions and the actions can pay attention to the ones that apply. - """ - self.name: str = "" - """Human-readable action identifier used for printing, logging, and reporting.""" - self.shape: Dict[str, int] = {} - """Dictionary describing the number of options for each parameter of this action. The keys of this dict must - align with the keyword args of the form_request method.""" - self.manager: ActionManager = manager - """Reference to the ActionManager which created this action. This is used to access the game and simulation - objects.""" - - @abstractmethod - def form_request(self) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [] - - -class DoNothingAction(AbstractAction): - """Action which does nothing. This is here to allow agents to be idle if they choose to.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - self.name = "DONOTHING" - self.shape: Dict[str, int] = { - "dummy": 1, - } - # This action does not accept any parameters, therefore it technically has a gymnasium shape of Discrete(1), - # i.e. a choice between one option. To make enumerating this action easier, we are adding a 'dummy' paramter - # with one option. This just aids the Action Manager to enumerate all possibilities. - - def form_request(self, **kwargs) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["do_nothing"] - - -class NodeServiceAbstractAction(AbstractAction): - """ - Base class for service actions. - - Any action which applies to a service and uses node_id and service_id as its only two parameters can inherit from - this base class. - """ - - @abstractmethod - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes, "service_id": num_services} - self.verb: str # define but don't initialise: defends against children classes not defining this - - def form_request(self, node_id: int, service_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - service_name = self.manager.get_service_name_by_idx(node_id, service_id) - if node_name is None or service_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "service", service_name, self.verb] - class NodeServiceScanAction(NodeServiceAbstractAction): """Action which scans a service.""" @@ -1311,494 +1239,3 @@ class RansomwareLaunchC2ServerAction(AbstractAction): # This action currently doesn't require any further configuration options. return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] - -class ActionManager: - """Class which manages the action space for an agent.""" - - act_class_identifiers: Dict[str, type] = { - "DONOTHING": DoNothingAction, - "NODE_SERVICE_SCAN": NodeServiceScanAction, - "NODE_SERVICE_STOP": NodeServiceStopAction, - "NODE_SERVICE_START": NodeServiceStartAction, - "NODE_SERVICE_PAUSE": NodeServicePauseAction, - "NODE_SERVICE_RESUME": NodeServiceResumeAction, - "NODE_SERVICE_RESTART": NodeServiceRestartAction, - "NODE_SERVICE_DISABLE": NodeServiceDisableAction, - "NODE_SERVICE_ENABLE": NodeServiceEnableAction, - "NODE_SERVICE_FIX": NodeServiceFixAction, - "NODE_APPLICATION_EXECUTE": NodeApplicationExecuteAction, - "NODE_APPLICATION_SCAN": NodeApplicationScanAction, - "NODE_APPLICATION_CLOSE": NodeApplicationCloseAction, - "NODE_APPLICATION_FIX": NodeApplicationFixAction, - "NODE_APPLICATION_INSTALL": NodeApplicationInstallAction, - "NODE_APPLICATION_REMOVE": NodeApplicationRemoveAction, - "NODE_FILE_SCAN": NodeFileScanAction, - "NODE_FILE_CREATE": NodeFileCreateAction, - "NODE_FILE_CHECKHASH": NodeFileCheckhashAction, - "NODE_FILE_DELETE": NodeFileDeleteAction, - "NODE_FILE_REPAIR": NodeFileRepairAction, - "NODE_FILE_RESTORE": NodeFileRestoreAction, - "NODE_FILE_CORRUPT": NodeFileCorruptAction, - "NODE_FILE_ACCESS": NodeFileAccessAction, - "NODE_FOLDER_CREATE": NodeFolderCreateAction, - "NODE_FOLDER_SCAN": NodeFolderScanAction, - "NODE_FOLDER_CHECKHASH": NodeFolderCheckhashAction, - "NODE_FOLDER_REPAIR": NodeFolderRepairAction, - "NODE_FOLDER_RESTORE": NodeFolderRestoreAction, - "NODE_OS_SCAN": NodeOSScanAction, - "NODE_SHUTDOWN": NodeShutdownAction, - "NODE_STARTUP": NodeStartupAction, - "NODE_RESET": NodeResetAction, - "ROUTER_ACL_ADDRULE": RouterACLAddRuleAction, - "ROUTER_ACL_REMOVERULE": RouterACLRemoveRuleAction, - "FIREWALL_ACL_ADDRULE": FirewallACLAddRuleAction, - "FIREWALL_ACL_REMOVERULE": FirewallACLRemoveRuleAction, - "HOST_NIC_ENABLE": HostNICEnableAction, - "HOST_NIC_DISABLE": HostNICDisableAction, - "NETWORK_PORT_ENABLE": NetworkPortEnableAction, - "NETWORK_PORT_DISABLE": NetworkPortDisableAction, - "NODE_NMAP_PING_SCAN": NodeNMAPPingScanAction, - "NODE_NMAP_PORT_SCAN": NodeNMAPPortScanAction, - "NODE_NMAP_NETWORK_SERVICE_RECON": NodeNetworkServiceReconAction, - "CONFIGURE_DATABASE_CLIENT": ConfigureDatabaseClientAction, - "CONFIGURE_RANSOMWARE_SCRIPT": ConfigureRansomwareScriptAction, - "CONFIGURE_DOSBOT": ConfigureDoSBotAction, - "CONFIGURE_C2_BEACON": ConfigureC2BeaconAction, - "C2_SERVER_RANSOMWARE_LAUNCH": RansomwareLaunchC2ServerAction, - "C2_SERVER_RANSOMWARE_CONFIGURE": RansomwareConfigureC2ServerAction, - "C2_SERVER_TERMINAL_COMMAND": TerminalC2ServerAction, - "C2_SERVER_DATA_EXFILTRATE": ExfiltrationC2ServerAction, - "NODE_ACCOUNTS_CHANGE_PASSWORD": NodeAccountsChangePasswordAction, - "SSH_TO_REMOTE": NodeSessionsRemoteLoginAction, - "SESSIONS_REMOTE_LOGOFF": NodeSessionsRemoteLogoutAction, - "NODE_SEND_REMOTE_COMMAND": NodeSendRemoteCommandAction, - } - """Dictionary which maps action type strings to the corresponding action class.""" - - def __init__( - self, - actions: List[Dict], # stores list of actions available to agent - nodes: List[Dict], # extra configuration for each node - max_folders_per_node: int = 2, # allows calculating shape - max_files_per_folder: int = 2, # allows calculating shape - max_services_per_node: int = 2, # allows calculating shape - max_applications_per_node: int = 2, # allows calculating shape - max_nics_per_node: int = 8, # allows calculating shape - max_acl_rules: int = 10, # allows calculating shape - protocols: List[str] = ["TCP", "UDP", "ICMP"], # allow mapping index to protocol - ports: List[str] = ["HTTP", "DNS", "ARP", "FTP", "NTP"], # allow mapping index to port - ip_list: List[str] = [], # to allow us to map an index to an ip address. - wildcard_list: List[str] = [], # to allow mapping from wildcard index to - act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions - ) -> None: - """Init method for ActionManager. - - :param game: Reference to the game to which the agent belongs. - :type game: PrimaiteGame - :param actions: List of action specs which should be made available to the agent. The keys of each spec are: - 'type' and 'options' for passing any options to the action class's init method - :type actions: List[dict] - :param nodes: Extra configuration for each node. - :type nodes: List[Dict] - :param max_folders_per_node: Maximum number of folders per node. Used for calculating action shape. - :type max_folders_per_node: int - :param max_files_per_folder: Maximum number of files per folder. Used for calculating action shape. - :type max_files_per_folder: int - :param max_services_per_node: Maximum number of services per node. Used for calculating action shape. - :type max_services_per_node: int - :param max_nics_per_node: Maximum number of NICs per node. Used for calculating action shape. - :type max_nics_per_node: int - :param max_acl_rules: Maximum number of ACL rules per router. Used for calculating action shape. - :type max_acl_rules: int - :param protocols: List of protocols that are available in the simulation. Used for calculating action shape. - :type protocols: List[str] - :param ports: List of ports that are available in the simulation. Used for calculating action shape. - :type ports: List[str] - :param ip_list: List of IP addresses that known to this agent. Used for calculating action shape. - :type ip_list: Optional[List[str]] - :param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions. - :type act_map: Optional[Dict[int, Dict]] - """ - self.node_names: List[str] = [n["node_name"] for n in nodes] - """List of node names in this action space. The list order is the mapping between node index and node name.""" - self.application_names: List[List[str]] = [] - """ - List of applications per node. The list order gives the two-index mapping between (node_id, app_id) to app name. - The first index corresponds to node id, the second index is the app id on that particular node. - For instance, self.application_names[0][2] is the name of the third application on the first node. - """ - self.service_names: List[List[str]] = [] - """ - List of services per node. The list order gives the two-index mapping between (node_id, svc_id) to svc name. - The first index corresponds to node id, the second index is the service id on that particular node. - For instance, self.service_names[0][2] is the name of the third service on the first node. - """ - self.folder_names: List[List[str]] = [] - """ - List of folders per node. The list order gives the two-index mapping between (node_id, folder_id) to folder - name. The first index corresponds to node id, the second index is the folder id on that particular node. - For instance, self.folder_names[0][2] is the name of the third folder on the first node. - """ - self.file_names: List[List[List[str]]] = [] - """ - List of files per folder per node. The list order gives the three-index mapping between - (node_id, folder_id, file_id) to file name. The first index corresponds to node id, the second index is the - folder id on that particular node, and the third index is the file id in that particular folder. - For instance, self.file_names[0][2][1] is the name of the second file in the third folder on the first node. - """ - - # Populate lists of apps, services, files, folders, etc on nodes. - for node in nodes: - app_list = [a["application_name"] for a in node.get("applications", [])] - while len(app_list) < max_applications_per_node: - app_list.append(None) - self.application_names.append(app_list) - - svc_list = [s["service_name"] for s in node.get("services", [])] - while len(svc_list) < max_services_per_node: - svc_list.append(None) - self.service_names.append(svc_list) - - folder_list = [f["folder_name"] for f in node.get("folders", [])] - while len(folder_list) < max_folders_per_node: - folder_list.append(None) - self.folder_names.append(folder_list) - - file_sublist = [] - for folder in node.get("folders", [{"files": []}]): - file_list = [f["file_name"] for f in folder.get("files", [])] - while len(file_list) < max_files_per_folder: - file_list.append(None) - file_sublist.append(file_list) - while len(file_sublist) < max_folders_per_node: - file_sublist.append([None] * max_files_per_folder) - self.file_names.append(file_sublist) - self.protocols: List[str] = protocols - self.ports: List[str] = ports - - self.ip_address_list: List[str] = ip_list - self.wildcard_list: List[str] = wildcard_list - if self.wildcard_list == []: - self.wildcard_list = ["NONE"] - # action_args are settings which are applied to the action space as a whole. - global_action_args = { - "num_nodes": len(self.node_names), - "num_folders": max_folders_per_node, - "num_files": max_files_per_folder, - "num_services": max_services_per_node, - "num_applications": max_applications_per_node, - "num_nics": max_nics_per_node, - "num_acl_rules": max_acl_rules, - "num_protocols": len(self.protocols), - "num_ports": len(self.protocols), - "num_ips": len(self.ip_address_list), - "max_acl_rules": max_acl_rules, - "max_nics_per_node": max_nics_per_node, - } - self.actions: Dict[str, AbstractAction] = {} - for act_spec in actions: - # each action is provided into the action space config like this: - # - type: ACTION_TYPE - # options: - # option_1: value1 - # option_2: value2 - # where `type` decides which AbstractAction subclass should be used - # and `options` is an optional dict of options to pass to the init method of the action class - act_type = act_spec.get("type") - act_options = act_spec.get("options", {}) - self.actions[act_type] = self.act_class_identifiers[act_type](self, **global_action_args, **act_options) - - self.action_map: Dict[int, Tuple[str, Dict]] = {} - """ - Action mapping that converts an integer to a specific action and parameter choice. - - For example : - {0: ("NODE_SERVICE_SCAN", {node_id:0, service_id:2})} - """ - if act_map is None: - # raise RuntimeError("Action map must be specified in the config file.") - pass - else: - self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} - # make sure all numbers between 0 and N are represented as dict keys in action map - assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) - - def _enumerate_actions( - self, - ) -> Dict[int, Tuple[str, Dict]]: - """Generate a list of all the possible actions that could be taken. - - This enumerates all actions all combinations of parameters you could choose for those actions. The output - of this function is intended to populate the self.action_map parameter in the situation where the user provides - a list of action types, but doesn't specify any subset of actions that should be made available to the agent. - - The enumeration relies on the Actions' `shape` attribute. - - :return: An action map maps consecutive integers to a combination of Action type and parameter choices. - An example output could be: - {0: ("DONOTHING", {'dummy': 0}), - 1: ("NODE_OS_SCAN", {'node_id': 0}), - 2: ("NODE_OS_SCAN", {'node_id': 1}), - 3: ("NODE_FOLDER_SCAN", {'node_id:0, folder_id:0}), - ... #etc... - } - :rtype: Dict[int, Tuple[AbstractAction, Dict]] - """ - all_action_possibilities = [] - for act_name, action in self.actions.items(): - param_names = list(action.shape.keys()) - num_possibilities = list(action.shape.values()) - possibilities = [range(n) for n in num_possibilities] - - param_combinations = list(itertools.product(*possibilities)) - all_action_possibilities.extend( - [ - (act_name, {param_names[i]: param_combinations[j][i] for i in range(len(param_names))}) - for j in range(len(param_combinations)) - ] - ) - - return {i: p for i, p in enumerate(all_action_possibilities)} - - def get_action(self, action: int) -> Tuple[str, Dict]: - """Produce action in CAOS format.""" - """the agent chooses an action (as an integer), this is converted into an action in CAOS format""" - """The CAOS format is basically a action identifier, followed by parameters stored in a dictionary""" - act_identifier, act_options = self.action_map[action] - return act_identifier, act_options - - def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: - """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" - act_obj = self.actions[action_identifier] - return act_obj.form_request(**action_options) - - @property - def space(self) -> spaces.Space: - """Return the gymnasium action space for this agent.""" - return spaces.Discrete(len(self.action_map)) - - def get_node_name_by_idx(self, node_idx: int) -> str: - """ - Get the node name corresponding to the given index. - - :param node_idx: The index of the node to retrieve. - :type node_idx: int - :return: The node hostname. - :rtype: str - """ - if not node_idx < len(self.node_names): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx}, but its action space only" - f"has {len(self.node_names)} nodes." - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.node_names[node_idx] - - def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]: - """ - Get the folder name corresponding to the given node and folder indices. - - :param node_idx: The index of the node. - :type node_idx: int - :param folder_idx: The index of the folder on the node. - :type folder_idx: int - :return: The name of the folder. Or None if the node has fewer folders than the given index. - :rtype: Optional[str] - """ - if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this" - f" is out of range for its action space. Folder on each node: {self.folder_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.folder_names[node_idx][folder_idx] - - def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]: - """Get the file name corresponding to the given node, folder, and file indices. - - :param node_idx: The index of the node. - :type node_idx: int - :param folder_idx: The index of the folder on the node. - :type folder_idx: int - :param file_idx: The index of the file in the folder. - :type file_idx: int - :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has - fewer files than the given index. - :rtype: Optional[str] - """ - if ( - node_idx >= len(self.file_names) - or folder_idx >= len(self.file_names[node_idx]) - or file_idx >= len(self.file_names[node_idx][folder_idx]) - ): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}" - f" but this is out of range for its action space. Files on each node: {self.file_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.file_names[node_idx][folder_idx][file_idx] - - def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]: - """Get the service name corresponding to the given node and service indices. - - :param node_idx: The index of the node. - :type node_idx: int - :param service_idx: The index of the service on the node. - :type service_idx: int - :return: The name of the service. Or None if the node has fewer services than the given index. - :rtype: Optional[str] - """ - if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this" - f" is out of range for its action space. Services on each node: {self.service_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.service_names[node_idx][service_idx] - - def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]: - """Get the application name corresponding to the given node and service indices. - - :param node_idx: The index of the node. - :type node_idx: int - :param application_idx: The index of the service on the node. - :type application_idx: int - :return: The name of the service. Or None if the node has fewer services than the given index. - :rtype: Optional[str] - """ - if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but " - f"this is out of range for its action space. Applications on each node: {self.application_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.application_names[node_idx][application_idx] - - def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: - """Get the internet protocol corresponding to the given index. - - :param protocol_idx: The index of the protocol to retrieve. - :type protocol_idx: int - :return: The protocol. - :rtype: str - """ - if protocol_idx >= len(self.protocols): - msg = ( - f"Error: agent attempted to perform an action on protocol {protocol_idx} but this" - f" is out of range for its action space. Protocols: {self.protocols}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.protocols[protocol_idx] - - def get_ip_address_by_idx(self, ip_idx: int) -> str: - """ - Get the IP address corresponding to the given index. - - :param ip_idx: The index of the IP address to retrieve. - :type ip_idx: int - :return: The IP address. - :rtype: str - """ - if ip_idx >= len(self.ip_address_list): - msg = ( - f"Error: agent attempted to perform an action on ip address {ip_idx} but this" - f" is out of range for its action space. IP address list: {self.ip_address_list}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.ip_address_list[ip_idx] - - def get_wildcard_by_idx(self, wildcard_idx: int) -> str: - """ - Get the IP wildcard corresponding to the given index. - - :param ip_idx: The index of the IP wildcard to retrieve. - :type ip_idx: int - :return: The wildcard address. - :rtype: str - """ - if wildcard_idx >= len(self.wildcard_list): - msg = ( - f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this" - f" is out of range for its action space. Wildcard list: {self.wildcard_list}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.wildcard_list[wildcard_idx] - - def get_port_by_idx(self, port_idx: int) -> str: - """ - Get the port corresponding to the given index. - - :param port_idx: The index of the port to retrieve. - :type port_idx: int - :return: The port. - :rtype: str - """ - if port_idx >= len(self.ports): - msg = ( - f"Error: agent attempted to perform an action on port {port_idx} but this" - f" is out of range for its action space. Port list: {self.ip_address_list}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.ports[port_idx] - - def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int: - """ - Get the NIC number corresponding to the given node and NIC indices. - - :param node_idx: The index of the node. - :type node_idx: int - :param nic_idx: The index of the NIC on the node. - :type nic_idx: int - :return: The NIC number. - :rtype: int - """ - return nic_idx + 1 - - @classmethod - def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": - """ - Construct an ActionManager from a config definition. - - The action space config supports the following three sections: - 1. ``action_list`` - ``action_list`` contains a list action components which need to be included in the action space. - Each action component has a ``type`` which maps to a subclass of AbstractAction, and additional options - which will be passed to the action class's __init__ method during initialisation. - 2. ``action_map`` - Since the agent uses a discrete action space which acts as a flattened version of the component-based - action space, action_map provides a mapping between an integer (chosen by the agent) and a meaningful - action and values of parameters. For example action 0 can correspond to do nothing, action 1 can - correspond to "NODE_SERVICE_SCAN" with ``node_id=1`` and ``service_id=1``, action 2 can be " - 3. ``options`` - ``options`` contains a dictionary of options which are passed to the ActionManager's __init__ method. - These options are used to calculate the shape of the action space, and to provide additional information - to the ActionManager which is required to convert the agent's action choice into a CAOS request. - - :param game: The Primaite Game to which the agent belongs. - :type game: PrimaiteGame - :param cfg: The action space config. - :type cfg: Dict - :return: The constructed ActionManager. - :rtype: ActionManager - """ - if "ip_list" not in cfg["options"]: - cfg["options"]["ip_list"] = [] - - obj = cls( - actions=cfg["action_list"], - **cfg["options"], - protocols=game.options.protocols, - ports=game.options.ports, - act_map=cfg.get("action_map"), - ) - - return obj diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py new file mode 100644 index 00000000..34c7c4d6 --- /dev/null +++ b/src/primaite/game/agent/actions/manager.py @@ -0,0 +1,487 @@ +"""yaml example + +agents: + - name: agent_1 + action_space: + actions: + - do_nothing + - node_service_start + - node_service_stop + action_map: +""" + +from abc import ABC, abstractmethod + +from pydantic import BaseModel, ConfigDict +from primaite.game.game import PrimaiteGame +from primaite.interface.request import RequestFormat +from __future__ import annotations +from gymnasium import spaces + + +import itertools +from typing import Any, ClassVar, Dict, List, Literal, Tuple, Type + +class AbstractAction(BaseModel): + """Base class for actions.""" + # notes: + # we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. + # all the init methods in the old actions are just used for holding a verb and shape, which are not really used. + # the config schema should be used to the actual parameters for formatting the action itself. + # (therefore there's no need for creating action instances, just the action class contains logic for converting + # CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be + # instantiated.) + class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options` + model_config = ConfigDict(extra="forbid") + type: str + + _registry: ClassVar[Dict[str,Type[AbstractAction]]] = {} + + def __init_subclass__(cls, identifier:str, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Cannot create new action under reserved name {identifier}") + cls._registry[identifier] = cls + + @classmethod + def form_request(self, config:ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [] + +class DoNothingAction(AbstractAction): + class ConfigSchema(AbstractAction.ConfigSchema): + type: Literal["do_nothing"] = "do_nothing" + + def form_request(self, options:ConfigSchema) -> RequestFormat: + return ["do_nothing"] + +class ActionManager: + """Class which manages the action space for an agent.""" + + def __init__( + self, + actions: List[Dict], # stores list of actions available to agent + # nodes: List[Dict], # extra configuration for each node + # max_folders_per_node: int = 2, # allows calculating shape + # max_files_per_folder: int = 2, # allows calculating shape + # max_services_per_node: int = 2, # allows calculating shape + # max_applications_per_node: int = 2, # allows calculating shape + # max_nics_per_node: int = 8, # allows calculating shape + # max_acl_rules: int = 10, # allows calculating shape + # protocols: List[str] = ["TCP", "UDP", "ICMP"], # allow mapping index to protocol + # ports: List[str] = ["HTTP", "DNS", "ARP", "FTP", "NTP"], # allow mapping index to port + # ip_list: List[str] = [], # to allow us to map an index to an ip address. + # wildcard_list: List[str] = [], # to allow mapping from wildcard index to + act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions + ) -> None: + """Init method for ActionManager. + + :param game: Reference to the game to which the agent belongs. + :type game: PrimaiteGame + :param actions: List of action specs which should be made available to the agent. The keys of each spec are: + 'type' and 'options' for passing any options to the action class's init method + :type actions: List[dict] + :param nodes: Extra configuration for each node. + :type nodes: List[Dict] + :param max_folders_per_node: Maximum number of folders per node. Used for calculating action shape. + :type max_folders_per_node: int + :param max_files_per_folder: Maximum number of files per folder. Used for calculating action shape. + :type max_files_per_folder: int + :param max_services_per_node: Maximum number of services per node. Used for calculating action shape. + :type max_services_per_node: int + :param max_nics_per_node: Maximum number of NICs per node. Used for calculating action shape. + :type max_nics_per_node: int + :param max_acl_rules: Maximum number of ACL rules per router. Used for calculating action shape. + :type max_acl_rules: int + :param protocols: List of protocols that are available in the simulation. Used for calculating action shape. + :type protocols: List[str] + :param ports: List of ports that are available in the simulation. Used for calculating action shape. + :type ports: List[str] + :param ip_list: List of IP addresses that known to this agent. Used for calculating action shape. + :type ip_list: Optional[List[str]] + :param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions. + :type act_map: Optional[Dict[int, Dict]] + """ + self.node_names: List[str] = [n["node_name"] for n in nodes] + """List of node names in this action space. The list order is the mapping between node index and node name.""" + self.application_names: List[List[str]] = [] + """ + List of applications per node. The list order gives the two-index mapping between (node_id, app_id) to app name. + The first index corresponds to node id, the second index is the app id on that particular node. + For instance, self.application_names[0][2] is the name of the third application on the first node. + """ + self.service_names: List[List[str]] = [] + """ + List of services per node. The list order gives the two-index mapping between (node_id, svc_id) to svc name. + The first index corresponds to node id, the second index is the service id on that particular node. + For instance, self.service_names[0][2] is the name of the third service on the first node. + """ + self.folder_names: List[List[str]] = [] + """ + List of folders per node. The list order gives the two-index mapping between (node_id, folder_id) to folder + name. The first index corresponds to node id, the second index is the folder id on that particular node. + For instance, self.folder_names[0][2] is the name of the third folder on the first node. + """ + self.file_names: List[List[List[str]]] = [] + """ + List of files per folder per node. The list order gives the three-index mapping between + (node_id, folder_id, file_id) to file name. The first index corresponds to node id, the second index is the + folder id on that particular node, and the third index is the file id in that particular folder. + For instance, self.file_names[0][2][1] is the name of the second file in the third folder on the first node. + """ + + # Populate lists of apps, services, files, folders, etc on nodes. + for node in nodes: + app_list = [a["application_name"] for a in node.get("applications", [])] + while len(app_list) < max_applications_per_node: + app_list.append(None) + self.application_names.append(app_list) + + svc_list = [s["service_name"] for s in node.get("services", [])] + while len(svc_list) < max_services_per_node: + svc_list.append(None) + self.service_names.append(svc_list) + + folder_list = [f["folder_name"] for f in node.get("folders", [])] + while len(folder_list) < max_folders_per_node: + folder_list.append(None) + self.folder_names.append(folder_list) + + file_sublist = [] + for folder in node.get("folders", [{"files": []}]): + file_list = [f["file_name"] for f in folder.get("files", [])] + while len(file_list) < max_files_per_folder: + file_list.append(None) + file_sublist.append(file_list) + while len(file_sublist) < max_folders_per_node: + file_sublist.append([None] * max_files_per_folder) + self.file_names.append(file_sublist) + self.protocols: List[str] = protocols + self.ports: List[str] = ports + + self.ip_address_list: List[str] = ip_list + self.wildcard_list: List[str] = wildcard_list + if self.wildcard_list == []: + self.wildcard_list = ["NONE"] + # action_args are settings which are applied to the action space as a whole. + global_action_args = { + "num_nodes": len(self.node_names), + "num_folders": max_folders_per_node, + "num_files": max_files_per_folder, + "num_services": max_services_per_node, + "num_applications": max_applications_per_node, + "num_nics": max_nics_per_node, + "num_acl_rules": max_acl_rules, + "num_protocols": len(self.protocols), + "num_ports": len(self.protocols), + "num_ips": len(self.ip_address_list), + "max_acl_rules": max_acl_rules, + "max_nics_per_node": max_nics_per_node, + } + self.actions: Dict[str, AbstractAction] = {} + for act_spec in actions: + # each action is provided into the action space config like this: + # - type: ACTION_TYPE + # options: + # option_1: value1 + # option_2: value2 + # where `type` decides which AbstractAction subclass should be used + # and `options` is an optional dict of options to pass to the init method of the action class + act_type = act_spec.get("type") + act_options = act_spec.get("options", {}) + self.actions[act_type] = self.act_class_identifiers[act_type](self, **global_action_args, **act_options) + + self.action_map: Dict[int, Tuple[str, Dict]] = {} + """ + Action mapping that converts an integer to a specific action and parameter choice. + + For example : + {0: ("NODE_SERVICE_SCAN", {node_id:0, service_id:2})} + """ + if act_map is None: + # raise RuntimeError("Action map must be specified in the config file.") + pass + else: + self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} + # make sure all numbers between 0 and N are represented as dict keys in action map + assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) + + def _enumerate_actions( + self, + ) -> Dict[int, Tuple[str, Dict]]: + """Generate a list of all the possible actions that could be taken. + + This enumerates all actions all combinations of parameters you could choose for those actions. The output + of this function is intended to populate the self.action_map parameter in the situation where the user provides + a list of action types, but doesn't specify any subset of actions that should be made available to the agent. + + The enumeration relies on the Actions' `shape` attribute. + + :return: An action map maps consecutive integers to a combination of Action type and parameter choices. + An example output could be: + {0: ("DONOTHING", {'dummy': 0}), + 1: ("NODE_OS_SCAN", {'node_id': 0}), + 2: ("NODE_OS_SCAN", {'node_id': 1}), + 3: ("NODE_FOLDER_SCAN", {'node_id:0, folder_id:0}), + ... #etc... + } + :rtype: Dict[int, Tuple[AbstractAction, Dict]] + """ + all_action_possibilities = [] + for act_name, action in self.actions.items(): + param_names = list(action.shape.keys()) + num_possibilities = list(action.shape.values()) + possibilities = [range(n) for n in num_possibilities] + + param_combinations = list(itertools.product(*possibilities)) + all_action_possibilities.extend( + [ + (act_name, {param_names[i]: param_combinations[j][i] for i in range(len(param_names))}) + for j in range(len(param_combinations)) + ] + ) + + return {i: p for i, p in enumerate(all_action_possibilities)} + + def get_action(self, action: int) -> Tuple[str, Dict]: + """Produce action in CAOS format.""" + """the agent chooses an action (as an integer), this is converted into an action in CAOS format""" + """The CAOS format is basically a action identifier, followed by parameters stored in a dictionary""" + act_identifier, act_options = self.action_map[action] + return act_identifier, act_options + + def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: + """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" + act_obj = self.actions[action_identifier] + return act_obj.form_request(**action_options) + + @property + def space(self) -> spaces.Space: + """Return the gymnasium action space for this agent.""" + return spaces.Discrete(len(self.action_map)) + + def get_node_name_by_idx(self, node_idx: int) -> str: + """ + Get the node name corresponding to the given index. + + :param node_idx: The index of the node to retrieve. + :type node_idx: int + :return: The node hostname. + :rtype: str + """ + if not node_idx < len(self.node_names): + msg = ( + f"Error: agent attempted to perform an action on node {node_idx}, but its action space only" + f"has {len(self.node_names)} nodes." + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.node_names[node_idx] + + def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]: + """ + Get the folder name corresponding to the given node and folder indices. + + :param node_idx: The index of the node. + :type node_idx: int + :param folder_idx: The index of the folder on the node. + :type folder_idx: int + :return: The name of the folder. Or None if the node has fewer folders than the given index. + :rtype: Optional[str] + """ + if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]): + msg = ( + f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this" + f" is out of range for its action space. Folder on each node: {self.folder_names}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.folder_names[node_idx][folder_idx] + + def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]: + """Get the file name corresponding to the given node, folder, and file indices. + + :param node_idx: The index of the node. + :type node_idx: int + :param folder_idx: The index of the folder on the node. + :type folder_idx: int + :param file_idx: The index of the file in the folder. + :type file_idx: int + :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has + fewer files than the given index. + :rtype: Optional[str] + """ + if ( + node_idx >= len(self.file_names) + or folder_idx >= len(self.file_names[node_idx]) + or file_idx >= len(self.file_names[node_idx][folder_idx]) + ): + msg = ( + f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}" + f" but this is out of range for its action space. Files on each node: {self.file_names}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.file_names[node_idx][folder_idx][file_idx] + + def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]: + """Get the service name corresponding to the given node and service indices. + + :param node_idx: The index of the node. + :type node_idx: int + :param service_idx: The index of the service on the node. + :type service_idx: int + :return: The name of the service. Or None if the node has fewer services than the given index. + :rtype: Optional[str] + """ + if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]): + msg = ( + f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this" + f" is out of range for its action space. Services on each node: {self.service_names}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.service_names[node_idx][service_idx] + + def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]: + """Get the application name corresponding to the given node and service indices. + + :param node_idx: The index of the node. + :type node_idx: int + :param application_idx: The index of the service on the node. + :type application_idx: int + :return: The name of the service. Or None if the node has fewer services than the given index. + :rtype: Optional[str] + """ + if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]): + msg = ( + f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but " + f"this is out of range for its action space. Applications on each node: {self.application_names}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.application_names[node_idx][application_idx] + + def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: + """Get the internet protocol corresponding to the given index. + + :param protocol_idx: The index of the protocol to retrieve. + :type protocol_idx: int + :return: The protocol. + :rtype: str + """ + if protocol_idx >= len(self.protocols): + msg = ( + f"Error: agent attempted to perform an action on protocol {protocol_idx} but this" + f" is out of range for its action space. Protocols: {self.protocols}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.protocols[protocol_idx] + + def get_ip_address_by_idx(self, ip_idx: int) -> str: + """ + Get the IP address corresponding to the given index. + + :param ip_idx: The index of the IP address to retrieve. + :type ip_idx: int + :return: The IP address. + :rtype: str + """ + if ip_idx >= len(self.ip_address_list): + msg = ( + f"Error: agent attempted to perform an action on ip address {ip_idx} but this" + f" is out of range for its action space. IP address list: {self.ip_address_list}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.ip_address_list[ip_idx] + + def get_wildcard_by_idx(self, wildcard_idx: int) -> str: + """ + Get the IP wildcard corresponding to the given index. + + :param ip_idx: The index of the IP wildcard to retrieve. + :type ip_idx: int + :return: The wildcard address. + :rtype: str + """ + if wildcard_idx >= len(self.wildcard_list): + msg = ( + f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this" + f" is out of range for its action space. Wildcard list: {self.wildcard_list}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.wildcard_list[wildcard_idx] + + def get_port_by_idx(self, port_idx: int) -> str: + """ + Get the port corresponding to the given index. + + :param port_idx: The index of the port to retrieve. + :type port_idx: int + :return: The port. + :rtype: str + """ + if port_idx >= len(self.ports): + msg = ( + f"Error: agent attempted to perform an action on port {port_idx} but this" + f" is out of range for its action space. Port list: {self.ip_address_list}" + ) + _LOGGER.error(msg) + raise RuntimeError(msg) + return self.ports[port_idx] + + def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int: + """ + Get the NIC number corresponding to the given node and NIC indices. + + :param node_idx: The index of the node. + :type node_idx: int + :param nic_idx: The index of the NIC on the node. + :type nic_idx: int + :return: The NIC number. + :rtype: int + """ + return nic_idx + 1 + + @classmethod + def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": + """ + Construct an ActionManager from a config definition. + + The action space config supports the following three sections: + 1. ``action_list`` + ``action_list`` contains a list action components which need to be included in the action space. + Each action component has a ``type`` which maps to a subclass of AbstractAction, and additional options + which will be passed to the action class's __init__ method during initialisation. + 2. ``action_map`` + Since the agent uses a discrete action space which acts as a flattened version of the component-based + action space, action_map provides a mapping between an integer (chosen by the agent) and a meaningful + action and values of parameters. For example action 0 can correspond to do nothing, action 1 can + correspond to "NODE_SERVICE_SCAN" with ``node_id=1`` and ``service_id=1``, action 2 can be " + 3. ``options`` + ``options`` contains a dictionary of options which are passed to the ActionManager's __init__ method. + These options are used to calculate the shape of the action space, and to provide additional information + to the ActionManager which is required to convert the agent's action choice into a CAOS request. + + :param game: The Primaite Game to which the agent belongs. + :type game: PrimaiteGame + :param cfg: The action space config. + :type cfg: Dict + :return: The constructed ActionManager. + :rtype: ActionManager + """ + if "ip_list" not in cfg["options"]: + cfg["options"]["ip_list"] = [] + + obj = cls( + actions=cfg["action_list"], + **cfg["options"], + protocols=game.options.protocols, + ports=game.options.ports, + act_map=cfg.get("action_map"), + ) + + return obj diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py new file mode 100644 index 00000000..79d70212 --- /dev/null +++ b/src/primaite/game/agent/actions/service.py @@ -0,0 +1,42 @@ +from typing import ClassVar +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + +class NodeServiceAbstractAction(AbstractAction): + class ConfigSchema(AbstractAction.ConfigSchema): + node_name: str + service_name: str + + verb: ClassVar[str] + + @classmethod + def form_request(cls, config:ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return ["network", "node", config.node_name, "service", config.service_name, cls.verb] + +class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_scan"): + verb: str = "scan" + +class NodeServiceStopAction(NodeServiceAbstractAction, identifier=...): + verb: str = "stop" + +class NodeServiceStartAction(NodeServiceAbstractAction): + verb: str = "start" + +class NodeServicePauseAction(NodeServiceAbstractAction): + verb: str = "pause" + +class NodeServiceResumeAction(NodeServiceAbstractAction): + verb: str = "resume" + +class NodeServiceRestartAction(NodeServiceAbstractAction): + verb: str = "restart" + +class NodeServiceDisableAction(NodeServiceAbstractAction): + verb: str = "disable" + +class NodeServiceEnableAction(NodeServiceAbstractAction): + verb: str = "enable" + +class NodeServiceFixAction(NodeServiceAbstractAction): + verb: str = "fix" From cd30e2d084dde84d8b63a0b6a51e10b0f529180c Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 17 Oct 2024 12:22:30 +0100 Subject: [PATCH 025/224] #2912 - Mid-day commit. Actions moving across from actions.py to game.agent.actions --- src/primaite/game/agent/actions.py | 6 +- src/primaite/game/agent/actions/__init__.py | 27 ++ src/primaite/game/agent/actions/acl.py | 170 +++++++ .../game/agent/actions/application.py | 64 +++ src/primaite/game/agent/actions/file.py | 79 +++ src/primaite/game/agent/actions/folder.py | 65 +++ src/primaite/game/agent/actions/manager.py | 449 +++++++++--------- src/primaite/game/agent/actions/node.py | 52 ++ src/primaite/game/agent/actions/service.py | 75 ++- 9 files changed, 743 insertions(+), 244 deletions(-) create mode 100644 src/primaite/game/agent/actions/acl.py create mode 100644 src/primaite/game/agent/actions/application.py create mode 100644 src/primaite/game/agent/actions/file.py create mode 100644 src/primaite/game/agent/actions/folder.py create mode 100644 src/primaite/game/agent/actions/node.py diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 1df25d27..68e42fb1 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -10,13 +10,12 @@ AbstractAction. The ActionManager is responsible for: ensures that requests conform to the simulator's request format. """ from abc import abstractmethod -from typing import Dict, List, Literal, Optional, TYPE_CHECKING, Union +from typing import Dict, List, Literal, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo from primaite import getLogger -from primaite.game.agent.actions.manager import ActionManager -from primaite.game.agent.actions.manager import AbstractAction +from primaite.game.agent.actions.manager import AbstractAction, ActionManager from primaite.game.agent.actions.service import NodeServiceAbstractAction from primaite.interface.request import RequestFormat @@ -1238,4 +1237,3 @@ class RansomwareLaunchC2ServerAction(AbstractAction): return ["do_nothing"] # This action currently doesn't require any further configuration options. return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] - diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index e69de29b..24a3ad67 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -0,0 +1,27 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from primaite.game.agent.actions.manager import ActionManager +from primaite.game.agent.actions.service import ( + NodeServiceDisableAction, + NodeServiceEnableAction, + NodeServiceFixAction, + NodeServicePauseAction, + NodeServiceRestartAction, + NodeServiceResumeAction, + NodeServiceScanAction, + NodeServiceStartAction, + NodeServiceStopAction, +) + +__all__ = ( + "NodeServiceDisableAction", + "NodeServiceEnableAction", + "NodeServiceFixAction", + "NodeServicePauseAction", + "NodeServiceRestartAction", + "NodeServiceResumeAction", + "NodeServiceScanAction", + "NodeServiceStartAction", + "NodeServiceStopAction", + "ActionManager", +) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py new file mode 100644 index 00000000..22e0a465 --- /dev/null +++ b/src/primaite/game/agent/actions/acl.py @@ -0,0 +1,170 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import Dict, List, Literal + +from pydantic import BaseModel, Field, field_validator, ValidationInfo + +from primaite.game.agent.actions.manager import AbstractAction +from primaite.game.game import _LOGGER + + +class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): + """Action which adds a rule to a router's ACL.""" + + class ACLRuleOptions(BaseModel): + """Validator for ACL_ADD_RULE options.""" + + target_router: str + """On which router to add the rule, must be specified.""" + position: int + """At what position to add the rule, must be specified.""" + permission: Literal[1, 2] + """Whether to allow or deny traffic, must be specified. 1 = PERMIT, 2 = DENY.""" + source_ip_id: int = Field(default=1, ge=1) + """Rule source IP address. By default, all ip addresses.""" + source_wildcard_id: int = Field(default=0, ge=0) + """Rule source IP wildcard. By default, use the wildcard at index 0 from action manager.""" + source_port_id: int = Field(default=1, ge=1) + """Rule source port. By default, all source ports.""" + dest_ip_id: int = Field(default=1, ge=1) + """Rule destination IP address. By default, all ip addresses.""" + dest_wildcard_id: int = Field(default=0, ge=0) + """Rule destination IP wildcard. By default, use the wildcard at index 0 from action manager.""" + dest_port_id: int = Field(default=1, ge=1) + """Rule destination port. By default, all destination ports.""" + protocol_id: int = Field(default=1, ge=1) + """Rule protocol. By default, all protocols.""" + + @field_validator( + "source_ip_id", + "source_port_id", + "source_wildcard_id", + "dest_ip_id", + "dest_port_id", + "dest_wildcard_id", + "protocol_id", + mode="before", + ) + @classmethod + def not_none(cls, v: str, info: ValidationInfo) -> int: + """If None is passed, use the default value instead.""" + if v is None: + return cls.model_fields[info.field_name].default + return v + + def __init__( + self, + manager: "ActionManager", + max_acl_rules: int, + num_ips: int, + num_ports: int, + num_protocols: int, + **kwargs, + ) -> None: + """Init method for RouterACLAddRuleAction. + + :param manager: Reference to the ActionManager which created this action. + :type manager: ActionManager + :param max_acl_rules: Maximum number of ACL rules that can be added to the router. + :type max_acl_rules: int + :param num_ips: Number of IP addresses in the simulation. + :type num_ips: int + :param num_ports: Number of ports in the simulation. + :type num_ports: int + :param num_protocols: Number of protocols in the simulation. + :type num_protocols: int + """ + super().__init__(manager=manager) + num_permissions = 3 + self.shape: Dict[str, int] = { + "position": max_acl_rules, + "permission": num_permissions, + "source_ip_id": num_ips, + "dest_ip_id": num_ips, + "source_port_id": num_ports, + "dest_port_id": num_ports, + "protocol_id": num_protocols, + } + + def form_request( + self, + target_router: str, + position: int, + permission: int, + source_ip_id: int, + source_wildcard_id: int, + dest_ip_id: int, + dest_wildcard_id: int, + source_port_id: int, + dest_port_id: int, + protocol_id: int, + ) -> List[str]: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + # Validate incoming data. + parsed_options = RouterACLAddRuleAction.ACLRuleOptions( + target_router=target_router, + position=position, + permission=permission, + source_ip_id=source_ip_id, + source_wildcard_id=source_wildcard_id, + dest_ip_id=dest_ip_id, + dest_wildcard_id=dest_wildcard_id, + source_port_id=source_port_id, + dest_port_id=dest_port_id, + protocol_id=protocol_id, + ) + if parsed_options.permission == 1: + permission_str = "PERMIT" + elif parsed_options.permission == 2: + permission_str = "DENY" + else: + _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") + + if parsed_options.protocol_id == 1: + protocol = "ALL" + else: + protocol = self.manager.get_internet_protocol_by_idx(parsed_options.protocol_id - 2) + # subtract 2 to account for UNUSED=0 and ALL=1. + + if parsed_options.source_ip_id == 1: + src_ip = "ALL" + else: + src_ip = self.manager.get_ip_address_by_idx(parsed_options.source_ip_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + + src_wildcard = self.manager.get_wildcard_by_idx(parsed_options.source_wildcard_id) + + if parsed_options.source_port_id == 1: + src_port = "ALL" + else: + src_port = self.manager.get_port_by_idx(parsed_options.source_port_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + + if parsed_options.dest_ip_id == 1: + dst_ip = "ALL" + else: + dst_ip = self.manager.get_ip_address_by_idx(parsed_options.dest_ip_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + dst_wildcard = self.manager.get_wildcard_by_idx(parsed_options.dest_wildcard_id) + + if parsed_options.dest_port_id == 1: + dst_port = "ALL" + else: + dst_port = self.manager.get_port_by_idx(parsed_options.dest_port_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + + return [ + "network", + "node", + target_router, + "acl", + "add_rule", + permission_str, + protocol, + str(src_ip), + src_wildcard, + src_port, + str(dst_ip), + dst_wildcard, + dst_port, + position, + ] diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py new file mode 100644 index 00000000..4b82ffd3 --- /dev/null +++ b/src/primaite/game/agent/actions/application.py @@ -0,0 +1,64 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import abstractmethod +from typing import ClassVar, Dict + +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + + +class NodeApplicationAbstractAction(AbstractAction): + """ + Base class for application actions. + + Any action which applies to an application and uses node_id and application_id as its only two parameters can + inherit from this base class. + """ + + class ConfigSchema(AbstractAction.ConfigSchema): + node_name: str + application_name: str + + verb: ClassVar[str] + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.application_name is None: + return ["do_nothing"] + return ["network", "node", config.node_name, "application", config.application_name, cls.verb] + + +class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="node_application_execute"): + """Action which executes an application.""" + + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + verb: str = "execute" + + +class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_application_scan"): + """Action which scans an application.""" + + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + verb: str = "scan" + + +class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node_application_close"): + """Action which closes an application.""" + + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + verb: str = "close" + + +class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_application_fix"): + """Action which fixes an application.""" + + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + verb: str = "fix" + + +class NodeApplicationInstallAction(AbstractAction): + """Action which installs an application.""" + + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + verb: str = "install" diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py new file mode 100644 index 00000000..d21daa9b --- /dev/null +++ b/src/primaite/game/agent/actions/file.py @@ -0,0 +1,79 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import ClassVar + +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + + +class NodeFileAbstractAction(AbstractAction): + """Abstract base class for file actions. + + Any action which applies to a file and uses node_name, folder_name, and file_name as its only three parameters can inherit + from this base class. + """ + + class ConfigSchema(AbstractAction.ConfigSchema): + node_name: str + folder_name: str + file_name: str + + verb: ClassVar[str] + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.folder_name is None or config.file_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "file_system", + "folder", + config.folder_name, + "file", + config.file_name, + cls.verb, + ] + + +class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create"): + """Action which creates a new file in a given folder.""" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + verb: str = "create" + + +class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): + """Action which scans a file.""" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + verb: str = "scan" + + +class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete"): + """Action which deletes a file.""" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + verb: str = "delete" + + +class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restore"): + """Action which restores a file.""" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + verb: str = "restore" + + +class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrupt"): + """Action which corrupts a file.""" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + verb: str = "corrupt" + + +class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access"): + """Action which increases a file's access count.""" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + verb: str = "access" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py new file mode 100644 index 00000000..278f5658 --- /dev/null +++ b/src/primaite/game/agent/actions/folder.py @@ -0,0 +1,65 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import abstractmethod +from typing import ClassVar, Dict + +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + + +class NodeFolderAbstractAction(AbstractAction): + """ + Base class for folder actions. + + Any action which applies to a folder and uses node_id and folder_id as its only two parameters can inherit from + this base class. + """ + + class ConfigSchema(AbstractAction.ConfigSchema): + node_name: str + folder_name: str + + verb: ClassVar[str] + + @classmethod + def form_request(cls, node_id: int, folder_id: int) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = cls.manager.get_node_name_by_idx(node_id) + folder_name = cls.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) + if node_name is None or folder_name is None: + return ["do_nothing"] + return ["network", "node", node_name, "file_system", "folder", folder_name, cls.verb] + + +class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_scan"): + """Action which scans a folder.""" + + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + verb: str = "scan" + + +class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folder_checkhash"): + """Action which checks the hash of a folder.""" + + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + verb: str = "checkhash" + + +class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_repair"): + """Action which repairs a folder.""" + + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + verb: str = "repair" + + +class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_restore"): + """Action which restores a folder.""" + + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + verb: str = "restore" + + +class NodeFolderCreateAction(AbstractAction, identifier="node_folder_create"): + """Action which creates a new folder.""" + + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + verb: str = "create" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 34c7c4d6..99ce091e 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -1,3 +1,4 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """yaml example agents: @@ -10,20 +11,22 @@ agents: action_map: """ -from abc import ABC, abstractmethod - -from pydantic import BaseModel, ConfigDict -from primaite.game.game import PrimaiteGame -from primaite.interface.request import RequestFormat from __future__ import annotations -from gymnasium import spaces - import itertools -from typing import Any, ClassVar, Dict, List, Literal, Tuple, Type +from abc import ABC, abstractmethod +from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type + +from gymnasium import spaces +from pydantic import BaseModel, ConfigDict + +from primaite.game.game import _LOGGER, PrimaiteGame +from primaite.interface.request import RequestFormat + class AbstractAction(BaseModel): """Base class for actions.""" + # notes: # we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. # all the init methods in the old actions are just used for holding a verb and shape, which are not really used. @@ -31,30 +34,32 @@ class AbstractAction(BaseModel): # (therefore there's no need for creating action instances, just the action class contains logic for converting # CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be # instantiated.) - class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options` + class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options` model_config = ConfigDict(extra="forbid") type: str - _registry: ClassVar[Dict[str,Type[AbstractAction]]] = {} + _registry: ClassVar[Dict[str, Type[AbstractAction]]] = {} - def __init_subclass__(cls, identifier:str, **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) if identifier in cls._registry: raise ValueError(f"Cannot create new action under reserved name {identifier}") cls._registry[identifier] = cls @classmethod - def form_request(self, config:ConfigSchema) -> RequestFormat: + def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [] + class DoNothingAction(AbstractAction): class ConfigSchema(AbstractAction.ConfigSchema): type: Literal["do_nothing"] = "do_nothing" - def form_request(self, options:ConfigSchema) -> RequestFormat: + def form_request(self, options: ConfigSchema) -> RequestFormat: return ["do_nothing"] + class ActionManager: """Class which manages the action space for an agent.""" @@ -131,53 +136,53 @@ class ActionManager: """ # Populate lists of apps, services, files, folders, etc on nodes. - for node in nodes: - app_list = [a["application_name"] for a in node.get("applications", [])] - while len(app_list) < max_applications_per_node: - app_list.append(None) - self.application_names.append(app_list) + # for node in nodes: + # app_list = [a["application_name"] for a in node.get("applications", [])] + # while len(app_list) < max_applications_per_node: + # app_list.append(None) + # self.application_names.append(app_list) - svc_list = [s["service_name"] for s in node.get("services", [])] - while len(svc_list) < max_services_per_node: - svc_list.append(None) - self.service_names.append(svc_list) + # svc_list = [s["service_name"] for s in node.get("services", [])] + # while len(svc_list) < max_services_per_node: + # svc_list.append(None) + # self.service_names.append(svc_list) - folder_list = [f["folder_name"] for f in node.get("folders", [])] - while len(folder_list) < max_folders_per_node: - folder_list.append(None) - self.folder_names.append(folder_list) + # folder_list = [f["folder_name"] for f in node.get("folders", [])] + # while len(folder_list) < max_folders_per_node: + # folder_list.append(None) + # self.folder_names.append(folder_list) - file_sublist = [] - for folder in node.get("folders", [{"files": []}]): - file_list = [f["file_name"] for f in folder.get("files", [])] - while len(file_list) < max_files_per_folder: - file_list.append(None) - file_sublist.append(file_list) - while len(file_sublist) < max_folders_per_node: - file_sublist.append([None] * max_files_per_folder) - self.file_names.append(file_sublist) - self.protocols: List[str] = protocols - self.ports: List[str] = ports + # file_sublist = [] + # for folder in node.get("folders", [{"files": []}]): + # file_list = [f["file_name"] for f in folder.get("files", [])] + # while len(file_list) < max_files_per_folder: + # file_list.append(None) + # file_sublist.append(file_list) + # while len(file_sublist) < max_folders_per_node: + # file_sublist.append([None] * max_files_per_folder) + # self.file_names.append(file_sublist) + # self.protocols: List[str] = protocols + # self.ports: List[str] = ports - self.ip_address_list: List[str] = ip_list - self.wildcard_list: List[str] = wildcard_list - if self.wildcard_list == []: - self.wildcard_list = ["NONE"] - # action_args are settings which are applied to the action space as a whole. - global_action_args = { - "num_nodes": len(self.node_names), - "num_folders": max_folders_per_node, - "num_files": max_files_per_folder, - "num_services": max_services_per_node, - "num_applications": max_applications_per_node, - "num_nics": max_nics_per_node, - "num_acl_rules": max_acl_rules, - "num_protocols": len(self.protocols), - "num_ports": len(self.protocols), - "num_ips": len(self.ip_address_list), - "max_acl_rules": max_acl_rules, - "max_nics_per_node": max_nics_per_node, - } + # self.ip_address_list: List[str] = ip_list + # self.wildcard_list: List[str] = wildcard_list + # if self.wildcard_list == []: + # self.wildcard_list = ["NONE"] + # # action_args are settings which are applied to the action space as a whole. + # global_action_args = { + # "num_nodes": len(self.node_names), + # "num_folders": max_folders_per_node, + # "num_files": max_files_per_folder, + # "num_services": max_services_per_node, + # "num_applications": max_applications_per_node, + # "num_nics": max_nics_per_node, + # "num_acl_rules": max_acl_rules, + # "num_protocols": len(self.protocols), + # "num_ports": len(self.protocols), + # "num_ips": len(self.ip_address_list), + # "max_acl_rules": max_acl_rules, + # "max_nics_per_node": max_nics_per_node, + # } self.actions: Dict[str, AbstractAction] = {} for act_spec in actions: # each action is provided into the action space config like this: @@ -260,191 +265,191 @@ class ActionManager: """Return the gymnasium action space for this agent.""" return spaces.Discrete(len(self.action_map)) - def get_node_name_by_idx(self, node_idx: int) -> str: - """ - Get the node name corresponding to the given index. + # def get_node_name_by_idx(self, node_idx: int) -> str: + # """ + # Get the node name corresponding to the given index. - :param node_idx: The index of the node to retrieve. - :type node_idx: int - :return: The node hostname. - :rtype: str - """ - if not node_idx < len(self.node_names): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx}, but its action space only" - f"has {len(self.node_names)} nodes." - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.node_names[node_idx] + # :param node_idx: The index of the node to retrieve. + # :type node_idx: int + # :return: The node hostname. + # :rtype: str + # """ + # if not node_idx < len(self.node_names): + # msg = ( + # f"Error: agent attempted to perform an action on node {node_idx}, but its action space only" + # f"has {len(self.node_names)} nodes." + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.node_names[node_idx] - def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]: - """ - Get the folder name corresponding to the given node and folder indices. + # def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]: + # """ + # Get the folder name corresponding to the given node and folder indices. - :param node_idx: The index of the node. - :type node_idx: int - :param folder_idx: The index of the folder on the node. - :type folder_idx: int - :return: The name of the folder. Or None if the node has fewer folders than the given index. - :rtype: Optional[str] - """ - if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this" - f" is out of range for its action space. Folder on each node: {self.folder_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.folder_names[node_idx][folder_idx] + # :param node_idx: The index of the node. + # :type node_idx: int + # :param folder_idx: The index of the folder on the node. + # :type folder_idx: int + # :return: The name of the folder. Or None if the node has fewer folders than the given index. + # :rtype: Optional[str] + # """ + # if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]): + # msg = ( + # f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this" + # f" is out of range for its action space. Folder on each node: {self.folder_names}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.folder_names[node_idx][folder_idx] - def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]: - """Get the file name corresponding to the given node, folder, and file indices. + # def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]: + # """Get the file name corresponding to the given node, folder, and file indices. - :param node_idx: The index of the node. - :type node_idx: int - :param folder_idx: The index of the folder on the node. - :type folder_idx: int - :param file_idx: The index of the file in the folder. - :type file_idx: int - :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has - fewer files than the given index. - :rtype: Optional[str] - """ - if ( - node_idx >= len(self.file_names) - or folder_idx >= len(self.file_names[node_idx]) - or file_idx >= len(self.file_names[node_idx][folder_idx]) - ): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}" - f" but this is out of range for its action space. Files on each node: {self.file_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.file_names[node_idx][folder_idx][file_idx] + # :param node_idx: The index of the node. + # :type node_idx: int + # :param folder_idx: The index of the folder on the node. + # :type folder_idx: int + # :param file_idx: The index of the file in the folder. + # :type file_idx: int + # :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has + # fewer files than the given index. + # :rtype: Optional[str] + # """ + # if ( + # node_idx >= len(self.file_names) + # or folder_idx >= len(self.file_names[node_idx]) + # or file_idx >= len(self.file_names[node_idx][folder_idx]) + # ): + # msg = ( + # f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}" + # f" but this is out of range for its action space. Files on each node: {self.file_names}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.file_names[node_idx][folder_idx][file_idx] - def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]: - """Get the service name corresponding to the given node and service indices. + # def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]: + # """Get the service name corresponding to the given node and service indices. - :param node_idx: The index of the node. - :type node_idx: int - :param service_idx: The index of the service on the node. - :type service_idx: int - :return: The name of the service. Or None if the node has fewer services than the given index. - :rtype: Optional[str] - """ - if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this" - f" is out of range for its action space. Services on each node: {self.service_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.service_names[node_idx][service_idx] + # :param node_idx: The index of the node. + # :type node_idx: int + # :param service_idx: The index of the service on the node. + # :type service_idx: int + # :return: The name of the service. Or None if the node has fewer services than the given index. + # :rtype: Optional[str] + # """ + # if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]): + # msg = ( + # f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this" + # f" is out of range for its action space. Services on each node: {self.service_names}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.service_names[node_idx][service_idx] - def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]: - """Get the application name corresponding to the given node and service indices. + # def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]: + # """Get the application name corresponding to the given node and service indices. - :param node_idx: The index of the node. - :type node_idx: int - :param application_idx: The index of the service on the node. - :type application_idx: int - :return: The name of the service. Or None if the node has fewer services than the given index. - :rtype: Optional[str] - """ - if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]): - msg = ( - f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but " - f"this is out of range for its action space. Applications on each node: {self.application_names}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.application_names[node_idx][application_idx] + # :param node_idx: The index of the node. + # :type node_idx: int + # :param application_idx: The index of the service on the node. + # :type application_idx: int + # :return: The name of the service. Or None if the node has fewer services than the given index. + # :rtype: Optional[str] + # """ + # if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]): + # msg = ( + # f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but " + # f"this is out of range for its action space. Applications on each node: {self.application_names}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.application_names[node_idx][application_idx] - def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: - """Get the internet protocol corresponding to the given index. + # def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: + # """Get the internet protocol corresponding to the given index. - :param protocol_idx: The index of the protocol to retrieve. - :type protocol_idx: int - :return: The protocol. - :rtype: str - """ - if protocol_idx >= len(self.protocols): - msg = ( - f"Error: agent attempted to perform an action on protocol {protocol_idx} but this" - f" is out of range for its action space. Protocols: {self.protocols}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.protocols[protocol_idx] + # :param protocol_idx: The index of the protocol to retrieve. + # :type protocol_idx: int + # :return: The protocol. + # :rtype: str + # """ + # if protocol_idx >= len(self.protocols): + # msg = ( + # f"Error: agent attempted to perform an action on protocol {protocol_idx} but this" + # f" is out of range for its action space. Protocols: {self.protocols}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.protocols[protocol_idx] - def get_ip_address_by_idx(self, ip_idx: int) -> str: - """ - Get the IP address corresponding to the given index. + # def get_ip_address_by_idx(self, ip_idx: int) -> str: + # """ + # Get the IP address corresponding to the given index. - :param ip_idx: The index of the IP address to retrieve. - :type ip_idx: int - :return: The IP address. - :rtype: str - """ - if ip_idx >= len(self.ip_address_list): - msg = ( - f"Error: agent attempted to perform an action on ip address {ip_idx} but this" - f" is out of range for its action space. IP address list: {self.ip_address_list}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.ip_address_list[ip_idx] + # :param ip_idx: The index of the IP address to retrieve. + # :type ip_idx: int + # :return: The IP address. + # :rtype: str + # """ + # if ip_idx >= len(self.ip_address_list): + # msg = ( + # f"Error: agent attempted to perform an action on ip address {ip_idx} but this" + # f" is out of range for its action space. IP address list: {self.ip_address_list}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.ip_address_list[ip_idx] - def get_wildcard_by_idx(self, wildcard_idx: int) -> str: - """ - Get the IP wildcard corresponding to the given index. + # def get_wildcard_by_idx(self, wildcard_idx: int) -> str: + # """ + # Get the IP wildcard corresponding to the given index. - :param ip_idx: The index of the IP wildcard to retrieve. - :type ip_idx: int - :return: The wildcard address. - :rtype: str - """ - if wildcard_idx >= len(self.wildcard_list): - msg = ( - f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this" - f" is out of range for its action space. Wildcard list: {self.wildcard_list}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.wildcard_list[wildcard_idx] + # :param ip_idx: The index of the IP wildcard to retrieve. + # :type ip_idx: int + # :return: The wildcard address. + # :rtype: str + # """ + # if wildcard_idx >= len(self.wildcard_list): + # msg = ( + # f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this" + # f" is out of range for its action space. Wildcard list: {self.wildcard_list}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.wildcard_list[wildcard_idx] - def get_port_by_idx(self, port_idx: int) -> str: - """ - Get the port corresponding to the given index. + # def get_port_by_idx(self, port_idx: int) -> str: + # """ + # Get the port corresponding to the given index. - :param port_idx: The index of the port to retrieve. - :type port_idx: int - :return: The port. - :rtype: str - """ - if port_idx >= len(self.ports): - msg = ( - f"Error: agent attempted to perform an action on port {port_idx} but this" - f" is out of range for its action space. Port list: {self.ip_address_list}" - ) - _LOGGER.error(msg) - raise RuntimeError(msg) - return self.ports[port_idx] + # :param port_idx: The index of the port to retrieve. + # :type port_idx: int + # :return: The port. + # :rtype: str + # """ + # if port_idx >= len(self.ports): + # msg = ( + # f"Error: agent attempted to perform an action on port {port_idx} but this" + # f" is out of range for its action space. Port list: {self.ip_address_list}" + # ) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + # return self.ports[port_idx] - def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int: - """ - Get the NIC number corresponding to the given node and NIC indices. + # def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int: + # """ + # Get the NIC number corresponding to the given node and NIC indices. - :param node_idx: The index of the node. - :type node_idx: int - :param nic_idx: The index of the NIC on the node. - :type nic_idx: int - :return: The NIC number. - :rtype: int - """ - return nic_idx + 1 + # :param node_idx: The index of the node. + # :type node_idx: int + # :param nic_idx: The index of the NIC on the node. + # :type nic_idx: int + # :return: The NIC number. + # :rtype: int + # """ + # return nic_idx + 1 @classmethod def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py new file mode 100644 index 00000000..cbf035a0 --- /dev/null +++ b/src/primaite/game/agent/actions/node.py @@ -0,0 +1,52 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import abstractmethod +from typing import ClassVar, Dict + +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + + +class NodeAbstractAction(AbstractAction): + """ + Abstract base class for node actions. + + Any action which applies to a node and uses node_name as its only parameter can inherit from this base class. + """ + + class ConfigSchema(AbstractAction.ConfigSchema): + node_name: str + + verb: ClassVar[str] + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return ["network", "node", config.node_name, cls.verb] + + +class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): + """Action which scans a node's OS.""" + + class ConfigSchema(NodeAbstractAction.ConfigSchema): + verb: str = "scan" + + +class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): + """Action which shuts down a node.""" + + class ConfigSchema(NodeAbstractAction.ConfigSchema): + verb: str = "shutdown" + + +class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): + """Action which starts up a node.""" + + class ConfigSchema(NodeAbstractAction.ConfigSchema): + verb: str = "startup" + + +class NodeResetAction(NodeAbstractAction, identifier="node_reset"): + """Action which resets a node.""" + + class ConfigSchema(NodeAbstractAction.ConfigSchema): + verb: str = "reset" diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index 79d70212..97b37bde 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -1,7 +1,10 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar + from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat + class NodeServiceAbstractAction(AbstractAction): class ConfigSchema(AbstractAction.ConfigSchema): node_name: str @@ -10,33 +13,69 @@ class NodeServiceAbstractAction(AbstractAction): verb: ClassVar[str] @classmethod - def form_request(cls, config:ConfigSchema) -> RequestFormat: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return ["network", "node", config.node_name, "service", config.service_name, cls.verb] + class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_scan"): - verb: str = "scan" + """Action which scans a service.""" -class NodeServiceStopAction(NodeServiceAbstractAction, identifier=...): - verb: str = "stop" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "scan" -class NodeServiceStartAction(NodeServiceAbstractAction): - verb: str = "start" -class NodeServicePauseAction(NodeServiceAbstractAction): - verb: str = "pause" +class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_stop"): + """Action which stops a service.""" -class NodeServiceResumeAction(NodeServiceAbstractAction): - verb: str = "resume" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "stop" -class NodeServiceRestartAction(NodeServiceAbstractAction): - verb: str = "restart" -class NodeServiceDisableAction(NodeServiceAbstractAction): - verb: str = "disable" +class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service_start"): + """Action which starts a service.""" -class NodeServiceEnableAction(NodeServiceAbstractAction): - verb: str = "enable" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "start" -class NodeServiceFixAction(NodeServiceAbstractAction): - verb: str = "fix" + +class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service_pause"): + """Action which pauses a service.""" + + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "pause" + + +class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_service_resume"): + """Action which resumes a service.""" + + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "resume" + + +class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_service_restart"): + """Action which restarts a service.""" + + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "restart" + + +class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_service_disable"): + """Action which disables a service.""" + + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "disable" + + +class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_service_enable"): + """Action which enables a service.""" + + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "enable" + + +class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_fix"): + """Action which fixes a service.""" + + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + verb: str = "fix" From fe6a8e6e97cb7a3837091950d7038b52fe1ffbff Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 17 Oct 2024 13:24:57 +0100 Subject: [PATCH 026/224] #2913: Initial commit of new AbstractReward class. --- src/primaite/game/agent/rewards.py | 61 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 1de34b40..0db3cc28 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -27,9 +27,10 @@ the structure: service_ref: web_server_database_client ``` """ -from abc import abstractmethod -from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from abc import ABC, abstractmethod +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from pydantic import BaseModel from typing_extensions import Never from primaite import getLogger @@ -42,9 +43,35 @@ _LOGGER = getLogger(__name__) WhereType = Optional[Iterable[Union[str, int]]] -class AbstractReward: +class AbstractReward(BaseModel): """Base class for reward function components.""" + class ConfigSchema(BaseModel, ABC): + """Config schema for AbstractReward.""" + + type: str + + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Duplicate node adder {identifier}") + cls._registry[identifier] = cls + + @classmethod + @abstractmethod + def from_config(cls, config: Dict) -> "AbstractReward": + """Create a reward function component from a config dictionary. + + :param config: dict of options for the reward component's constructor + :type config: dict + :return: The reward component. + :rtype: AbstractReward + """ + if config["type"] not in cls._registry: + raise ValueError(f"Invalid reward type {config['type']}") + + return cls + @abstractmethod def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. @@ -58,21 +85,9 @@ class AbstractReward: """ return 0.0 - @classmethod - @abstractmethod - def from_config(cls, config: dict) -> "AbstractReward": - """Create a reward function component from a config dictionary. - :param config: dict of options for the reward component's constructor - :type config: dict - :return: The reward component. - :rtype: AbstractReward - """ - return cls() - - -class DummyReward(AbstractReward): - """Dummy reward function component which always returns 0.""" +class DummyReward(AbstractReward, identifier="DummyReward"): + """Dummy reward function component which always returns 0.0""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. @@ -98,7 +113,7 @@ class DummyReward(AbstractReward): return cls() -class DatabaseFileIntegrity(AbstractReward): +class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" def __init__(self, node_hostname: str, folder_name: str, file_name: str) -> None: @@ -168,7 +183,7 @@ class DatabaseFileIntegrity(AbstractReward): return cls(node_hostname=node_hostname, folder_name=folder_name, file_name=file_name) -class WebServer404Penalty(AbstractReward): +class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): """Reward function component which penalises the agent when the web server returns a 404 error.""" def __init__(self, node_hostname: str, service_name: str, sticky: bool = True) -> None: @@ -241,7 +256,7 @@ class WebServer404Penalty(AbstractReward): return cls(node_hostname=node_hostname, service_name=service_name, sticky=sticky) -class WebpageUnavailablePenalty(AbstractReward): +class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePenalty"): """Penalises the agent when the web browser fails to fetch a webpage.""" def __init__(self, node_hostname: str, sticky: bool = True) -> None: @@ -325,7 +340,7 @@ class WebpageUnavailablePenalty(AbstractReward): return cls(node_hostname=node_hostname, sticky=sticky) -class GreenAdminDatabaseUnreachablePenalty(AbstractReward): +class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdminDatabaseUnreachablePenalty"): """Penalises the agent when the green db clients fail to connect to the database.""" def __init__(self, node_hostname: str, sticky: bool = True) -> None: @@ -393,7 +408,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): return cls(node_hostname=node_hostname, sticky=sticky) -class SharedReward(AbstractReward): +class SharedReward(AbstractReward, identifier="SharedReward"): """Adds another agent's reward to the overall reward.""" def __init__(self, agent_name: Optional[str] = None) -> None: @@ -446,7 +461,7 @@ class SharedReward(AbstractReward): return cls(agent_name=agent_name) -class ActionPenalty(AbstractReward): +class ActionPenalty(AbstractReward, identifier="ActionPenalty"): """Apply a negative reward when taking any action except DONOTHING.""" def __init__(self, action_penalty: float, do_nothing_penalty: float) -> None: From 419a86114d0b6eca4d2da343c427ddddf1b297cf Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 17 Oct 2024 16:35:13 +0100 Subject: [PATCH 027/224] #2913: Now with ConfigSchemas. --- src/primaite/game/agent/rewards.py | 39 +++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 0db3cc28..4198af27 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -87,7 +87,7 @@ class AbstractReward(BaseModel): class DummyReward(AbstractReward, identifier="DummyReward"): - """Dummy reward function component which always returns 0.0""" + """Dummy reward function component which always returns 0.0.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. @@ -116,6 +116,13 @@ class DummyReward(AbstractReward, identifier="DummyReward"): class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" + class ConfigSchema(AbstractReward.ConfigSchema): + """ConfigSchema for DatabaseFileIntegrity.""" + + node_hostname: str + folder_name: str + file_name: str + def __init__(self, node_hostname: str, folder_name: str, file_name: str) -> None: """Initialise the reward component. @@ -186,6 +193,13 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): """Reward function component which penalises the agent when the web server returns a 404 error.""" + class ConfigSchema(AbstractReward.ConfigSchema): + """ConfigSchema for WebServer404Penalty.""" + + node_hostname: str + service_name: str + sticky: bool = True + def __init__(self, node_hostname: str, service_name: str, sticky: bool = True) -> None: """Initialise the reward component. @@ -259,6 +273,12 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePenalty"): """Penalises the agent when the web browser fails to fetch a webpage.""" + class ConfigSchema(AbstractReward.ConfigSchema): + """ConfigSchema for WebpageUnavailablePenalty.""" + + node_hostname: str + sticky: bool = True + def __init__(self, node_hostname: str, sticky: bool = True) -> None: """ Initialise the reward component. @@ -343,6 +363,12 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdminDatabaseUnreachablePenalty"): """Penalises the agent when the green db clients fail to connect to the database.""" + class ConfigSchema(AbstractReward.ConfigSchema): + """ConfigSchema for GreenAdminDatabaseUnreachablePenalty.""" + + node_hostname: str + sticky: bool = True + def __init__(self, node_hostname: str, sticky: bool = True) -> None: """ Initialise the reward component. @@ -411,6 +437,11 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi class SharedReward(AbstractReward, identifier="SharedReward"): """Adds another agent's reward to the overall reward.""" + class ConfigSchema(AbstractReward.ConfigSchema): + """Config schema for SharedReward.""" + + agent_name: str + def __init__(self, agent_name: Optional[str] = None) -> None: """ Initialise the shared reward. @@ -464,6 +495,12 @@ class SharedReward(AbstractReward, identifier="SharedReward"): class ActionPenalty(AbstractReward, identifier="ActionPenalty"): """Apply a negative reward when taking any action except DONOTHING.""" + class ConfigSchema(AbstractReward.ConfigSchema): + """Config schema for ActionPenalty.""" + + action_penalty: float = -1.0 + do_nothing_penalty: float = 0.0 + def __init__(self, action_penalty: float, do_nothing_penalty: float) -> None: """ Initialise the reward. From a90aec2bcd133ffbc2f9f63d028a54e01f382bae Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 17 Oct 2024 16:59:44 +0100 Subject: [PATCH 028/224] #2912 - End of day commit --- src/primaite/game/agent/actions/acl.py | 171 ++++++++++++++++++ .../game/agent/actions/application.py | 31 +++- src/primaite/game/agent/actions/config.py | 107 +++++++++++ src/primaite/game/agent/actions/file.py | 18 +- src/primaite/game/agent/actions/folder.py | 12 ++ src/primaite/game/agent/actions/host_nic.py | 57 ++++++ src/primaite/game/agent/actions/manager.py | 5 +- src/primaite/game/agent/actions/network.py | 43 +++++ src/primaite/game/agent/actions/node.py | 10 + 9 files changed, 446 insertions(+), 8 deletions(-) create mode 100644 src/primaite/game/agent/actions/config.py create mode 100644 src/primaite/game/agent/actions/host_nic.py create mode 100644 src/primaite/game/agent/actions/network.py diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 22e0a465..6aeafe4d 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field, field_validator, ValidationInfo from primaite.game.agent.actions.manager import AbstractAction from primaite.game.game import _LOGGER +from primaite.interface.request import RequestFormat class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): @@ -168,3 +169,173 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): dst_port, position, ] + + +class RouterACLRemoveRuleAction(AbstractAction): + """Action which removes a rule from a router's ACL.""" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for RouterACLRemoveRuleAction.""" + + target_router: str + position: str + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return ["network", "node", config.target_router, "acl", "remove_rule", config.position] + + +class FirewallACLAddRuleAction(AbstractAction): + """Action which adds a rule to a firewall port's ACL.""" + + def __init__( + self, + manager: "ActionManager", + max_acl_rules: int, + num_ips: int, + num_ports: int, + num_protocols: int, + **kwargs, + ) -> None: + """Init method for FirewallACLAddRuleAction. + + :param manager: Reference to the ActionManager which created this action. + :type manager: ActionManager + :param max_acl_rules: Maximum number of ACL rules that can be added to the router. + :type max_acl_rules: int + :param num_ips: Number of IP addresses in the simulation. + :type num_ips: int + :param num_ports: Number of ports in the simulation. + :type num_ports: int + :param num_protocols: Number of protocols in the simulation. + :type num_protocols: int + """ + super().__init__(manager=manager) + num_permissions = 3 + self.shape: Dict[str, int] = { + "position": max_acl_rules, + "permission": num_permissions, + "source_ip_id": num_ips, + "dest_ip_id": num_ips, + "source_port_id": num_ports, + "dest_port_id": num_ports, + "protocol_id": num_protocols, + } + + def form_request( + self, + target_firewall_nodename: str, + firewall_port_name: str, + firewall_port_direction: str, + position: int, + permission: int, + source_ip_id: int, + source_wildcard_id: int, + dest_ip_id: int, + dest_wildcard_id: int, + source_port_id: int, + dest_port_id: int, + protocol_id: int, + ) -> List[str]: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if permission == 0: + permission_str = "UNUSED" + return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS + elif permission == 1: + permission_str = "PERMIT" + elif permission == 2: + permission_str = "DENY" + else: + _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") + + if protocol_id == 0: + return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS + + if protocol_id == 1: + protocol = "ALL" + else: + protocol = self.manager.get_internet_protocol_by_idx(protocol_id - 2) + # subtract 2 to account for UNUSED=0 and ALL=1. + + if source_ip_id == 0: + return ["do_nothing"] # invalid formulation + elif source_ip_id == 1: + src_ip = "ALL" + else: + src_ip = self.manager.get_ip_address_by_idx(source_ip_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + + if source_port_id == 0: + return ["do_nothing"] # invalid formulation + elif source_port_id == 1: + src_port = "ALL" + else: + src_port = self.manager.get_port_by_idx(source_port_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + + if dest_ip_id == 0: + return ["do_nothing"] # invalid formulation + elif dest_ip_id == 1: + dst_ip = "ALL" + else: + dst_ip = self.manager.get_ip_address_by_idx(dest_ip_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + + if dest_port_id == 0: + return ["do_nothing"] # invalid formulation + elif dest_port_id == 1: + dst_port = "ALL" + else: + dst_port = self.manager.get_port_by_idx(dest_port_id - 2) + # subtract 2 to account for UNUSED=0, and ALL=1 + src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) + dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) + + return [ + "network", + "node", + target_firewall_nodename, + firewall_port_name, + firewall_port_direction, + "acl", + "add_rule", + permission_str, + protocol, + str(src_ip), + src_wildcard, + src_port, + str(dst_ip), + dst_wildcard, + dst_port, + position, + ] + +class FirewallACLRemoveRuleAction(AbstractAction): + """Action which removes a rule from a firewall port's ACL.""" + + def __init__(self, manager: "ActionManager", max_acl_rules: int, **kwargs) -> None: + """Init method for RouterACLRemoveRuleAction. + + :param manager: Reference to the ActionManager which created this action. + :type manager: ActionManager + :param max_acl_rules: Maximum number of ACL rules that can be added to the router. + :type max_acl_rules: int + """ + super().__init__(manager=manager) + self.shape: Dict[str, int] = {"position": max_acl_rules} + + def form_request( + self, target_firewall_nodename: str, firewall_port_name: str, firewall_port_direction: str, position: int + ) -> List[str]: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [ + "network", + "node", + target_firewall_nodename, + firewall_port_name, + firewall_port_direction, + "acl", + "remove_rule", + position, + ] \ No newline at end of file diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 4b82ffd3..39a7b224 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -1,7 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from abc import abstractmethod -from typing import ClassVar, Dict +from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -16,6 +14,8 @@ class NodeApplicationAbstractAction(AbstractAction): """ class ConfigSchema(AbstractAction.ConfigSchema): + """Base Configuration schema for Node Application actions.""" + node_name: str application_name: str @@ -33,6 +33,8 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="no """Action which executes an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + """Configuration schema for NodeApplicationExecuteAction.""" + verb: str = "execute" @@ -40,6 +42,8 @@ class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_ """Action which scans an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + """Configuration schema for NodeApplicationScanAction.""" + verb: str = "scan" @@ -47,6 +51,8 @@ class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node """Action which closes an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + """Configuration schema for NodeApplicationCloseAction.""" + verb: str = "close" @@ -54,11 +60,28 @@ class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_a """Action which fixes an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + """Configuration schema for NodeApplicationFixAction.""" + verb: str = "fix" -class NodeApplicationInstallAction(AbstractAction): +class NodeApplicationInstallAction(NodeApplicationAbstractAction): """Action which installs an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + """Configuration schema for NodeApplicationInstallAction.""" + verb: str = "install" + + # TODO: Either changes to application form_request bits, or add that here. + +class NodeApplicationRemoveAction(NodeApplicationAbstractAction): + """Action which removes/uninstalls an application""" + + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): + """Configuration schema for NodeApplicationRemoveAction.""" + + verb: str = "uninstall" + + # TODO: Either changes to application form_request bits, or add that here. + diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py new file mode 100644 index 00000000..8b3f99f1 --- /dev/null +++ b/src/primaite/game/agent/actions/config.py @@ -0,0 +1,107 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from typing import Dict, Optional +from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + + +class ConfigureRansomwareScriptAction(AbstractAction): + """Action which sets config parameters for a ransomware script on a node.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this option.""" + + model_config = ConfigDict(extra="forbid") + server_ip_address: Optional[str] = None + server_password: Optional[str] = None + payload: Optional[str] = None + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema + return ["network", "node", node_name, "application", "RansomwareScript", "configure", config] + +class ConfigureDoSBotAction(AbstractAction): + """Action which sets config parameters for a DoS bot on a node.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + model_config = ConfigDict(extra="forbid") + target_ip_address: Optional[str] = None + target_port: Optional[str] = None + payload: Optional[str] = None + repeat: Optional[bool] = None + port_scan_p_of_success: Optional[float] = None + dos_intensity: Optional[float] = None + max_sessions: Optional[int] = None + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, config: Dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + self._Opts.model_validate(config) # check that options adhere to schema + return ["network", "node", node_name, "application", "DoSBot", "configure", config] + + +class ConfigureC2BeaconAction(AbstractAction): + """Action which configures a C2 Beacon based on the parameters given.""" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for ConfigureC2BeaconAction.""" + + node_name: str + c2_server_ip_address: str + keep_alive_frequency: int = Field(default=5, ge=1) + masquerade_protocol: str = Field(default="TCP") + masquerade_port: str = Field(default="HTTP") + + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + c2_server_ip_address: str + keep_alive_frequency: int = Field(default=5, ge=1) + masquerade_protocol: str = Field(default="TCP") + masquerade_port: str = Field(default="HTTP") + + @field_validator( + "c2_server_ip_address", + "keep_alive_frequency", + "masquerade_protocol", + "masquerade_port", + mode="before", + ) + @classmethod + def not_none(cls, v: str, info: ValidationInfo) -> int: + """If None is passed, use the default value instead.""" + if v is None: + return cls.model_fields[info.field_name].default + return v + + @classmethod + def form_request(self, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + if config.node_name is None: + return ["do_nothing"] + config = ConfigureC2BeaconAction._Opts( + c2_server_ip_address=config["c2_server_ip_address"], + keep_alive_frequency=config["keep_alive_frequency"], + masquerade_port=config["masquerade_port"], + masquerade_protocol=config["masquerade_protocol"], + ) + + ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema + + return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] \ No newline at end of file diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index d21daa9b..77bd8ef3 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -8,11 +8,13 @@ from primaite.interface.request import RequestFormat class NodeFileAbstractAction(AbstractAction): """Abstract base class for file actions. - Any action which applies to a file and uses node_name, folder_name, and file_name as its only three parameters can inherit - from this base class. + Any action which applies to a file and uses node_name, folder_name, and file_name as its + only three parameters can inherit from this base class. """ class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration Schema for NodeFileAbstractAction.""" + node_name: str folder_name: str file_name: str @@ -41,6 +43,8 @@ class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create" """Action which creates a new file in a given folder.""" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileCreateAction.""" + verb: str = "create" @@ -48,6 +52,8 @@ class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): """Action which scans a file.""" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileScanAction.""" + verb: str = "scan" @@ -55,6 +61,8 @@ class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete" """Action which deletes a file.""" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileDeleteAction.""" + verb: str = "delete" @@ -62,6 +70,8 @@ class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restor """Action which restores a file.""" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileRestoreAction.""" + verb: str = "restore" @@ -69,6 +79,8 @@ class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrup """Action which corrupts a file.""" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileCorruptAction.""" + verb: str = "corrupt" @@ -76,4 +88,6 @@ class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access" """Action which increases a file's access count.""" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileAccessAction.""" + verb: str = "access" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index 278f5658..255731f6 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -15,6 +15,8 @@ class NodeFolderAbstractAction(AbstractAction): """ class ConfigSchema(AbstractAction.ConfigSchema): + """Base configuration schema for NodeFolder actions.""" + node_name: str folder_name: str @@ -34,6 +36,8 @@ class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_sca """Action which scans a folder.""" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + """Configuration schema for NodeFolderScanAction.""" + verb: str = "scan" @@ -41,6 +45,8 @@ class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folde """Action which checks the hash of a folder.""" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + """Configuration schema for NodeFolderCheckhashAction.""" + verb: str = "checkhash" @@ -48,6 +54,8 @@ class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_r """Action which repairs a folder.""" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + """Configuration schema for NodeFolderRepairAction.""" + verb: str = "repair" @@ -55,6 +63,8 @@ class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_ """Action which restores a folder.""" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + """Configuration schema for NodeFolderRestoreAction.""" + verb: str = "restore" @@ -62,4 +72,6 @@ class NodeFolderCreateAction(AbstractAction, identifier="node_folder_create"): """Action which creates a new folder.""" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): + """Configuration schema for NodeFolderCreateAction.""" + verb: str = "create" diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py new file mode 100644 index 00000000..f8be9465 --- /dev/null +++ b/src/primaite/game/agent/actions/host_nic.py @@ -0,0 +1,57 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from typing import Dict, Optional +from pydantic import BaseModel, ConfigDict +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + +class HostNICAbstractAction(AbstractAction): + """ + Abstract base class for NIC actions. + + Any action which applies to a NIC and uses node_id and nic_id as its only two parameters can inherit from this base + class. + """ + + class ConfigSchema(AbstractAction.ConfigSchema): + """Base Configuration schema for HostNIC actions.""" + num_nodes: str + max_nics_per_node: str + node_name: str + nic_num: str + + def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: + """Init method for HostNICAbstractAction. + + :param manager: Reference to the ActionManager which created this action. + :type manager: ActionManager + :param num_nodes: Number of nodes in the simulation. + :type num_nodes: int + :param max_nics_per_node: Maximum number of NICs per node. + :type max_nics_per_node: int + """ + super().__init__(manager=manager) + self.shape: Dict[str, int] = {"node_id": num_nodes, "nic_id": max_nics_per_node} + self.verb: str # define but don't initialise: defends against children classes not defining this + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.nic_num is None: + return ["do_nothing"] + return ["network", "node", config.node_name, "network_interface", config.nic_num, cls.verb] + +class HostNICEnableAction(HostNICAbstractAction): + """Action which enables a NIC.""" + + class ConfigSchema(HostNICAbstractAction.ConfigSchema): + """Configuration schema for HostNICEnableAction.""" + verb: str = "enable" + + +class HostNICDisableAction(HostNICAbstractAction): + """Action which disables a NIC.""" + + class ConfigSchema(HostNICAbstractAction.ConfigSchema): + """Configuration schema for HostNICDisableAction.""" + verb: str = "disable" \ No newline at end of file diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 99ce091e..09e5a851 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -14,13 +14,13 @@ agents: from __future__ import annotations import itertools -from abc import ABC, abstractmethod +from abc import ABC from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type from gymnasium import spaces from pydantic import BaseModel, ConfigDict -from primaite.game.game import _LOGGER, PrimaiteGame +from primaite.game.game import PrimaiteGame from primaite.interface.request import RequestFormat @@ -57,6 +57,7 @@ class DoNothingAction(AbstractAction): type: Literal["do_nothing"] = "do_nothing" def form_request(self, options: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return ["do_nothing"] diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py new file mode 100644 index 00000000..aa6ef4d3 --- /dev/null +++ b/src/primaite/game/agent/actions/network.py @@ -0,0 +1,43 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from typing import Dict, Optional +from pydantic import BaseModel, ConfigDict +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + +class NetworkPortEnableAction(AbstractAction): + """Action which enables are port on a router or a firewall.""" + + def __init__(self, manager: "ActionManager", max_nics_per_node: int, **kwargs) -> None: + """Init method for NetworkPortEnableAction. + + :param max_nics_per_node: Maximum number of NICs per node. + :type max_nics_per_node: int + """ + super().__init__(manager=manager) + self.shape: Dict[str, int] = {"port_id": max_nics_per_node} + + def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if target_nodename is None or port_id is None: + return ["do_nothing"] + return ["network", "node", target_nodename, "network_interface", port_id, "enable"] + + +class NetworkPortDisableAction(AbstractAction): + """Action which disables are port on a router or a firewall.""" + + def __init__(self, manager: "ActionManager", max_nics_per_node: int, **kwargs) -> None: + """Init method for NetworkPortDisableAction. + + :param max_nics_per_node: Maximum number of NICs per node. + :type max_nics_per_node: int + """ + super().__init__(manager=manager) + self.shape: Dict[str, int] = {"port_id": max_nics_per_node} + + def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if target_nodename is None or port_id is None: + return ["do_nothing"] + return ["network", "node", target_nodename, "network_interface", port_id, "disable"] \ No newline at end of file diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index cbf035a0..d431d344 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -14,6 +14,8 @@ class NodeAbstractAction(AbstractAction): """ class ConfigSchema(AbstractAction.ConfigSchema): + """Base Configuration schema for Node actions.""" + node_name: str verb: ClassVar[str] @@ -28,6 +30,8 @@ class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): """Action which scans a node's OS.""" class ConfigSchema(NodeAbstractAction.ConfigSchema): + """Configuration schema for NodeOSScanAction.""" + verb: str = "scan" @@ -35,6 +39,8 @@ class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): """Action which shuts down a node.""" class ConfigSchema(NodeAbstractAction.ConfigSchema): + """Configuration schema for NodeShutdownAction.""" + verb: str = "shutdown" @@ -42,6 +48,8 @@ class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): """Action which starts up a node.""" class ConfigSchema(NodeAbstractAction.ConfigSchema): + """Configuration schema for NodeStartupAction.""" + verb: str = "startup" @@ -49,4 +57,6 @@ class NodeResetAction(NodeAbstractAction, identifier="node_reset"): """Action which resets a node.""" class ConfigSchema(NodeAbstractAction.ConfigSchema): + """Configuration schema for NodeResetAction.""" + verb: str = "reset" From 1b1f3e4f714db8801369fc52985e1c5b4e1159a2 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 18 Oct 2024 12:07:53 +0100 Subject: [PATCH 029/224] #2912 - Updates to remaining action refactoring --- src/primaite/game/agent/actions/__init__.py | 13 +++++++++++ src/primaite/game/agent/actions/acl.py | 14 ++++++----- .../game/agent/actions/application.py | 2 +- src/primaite/game/agent/actions/config.py | 10 ++++---- src/primaite/game/agent/actions/folder.py | 8 +++---- src/primaite/game/agent/actions/host_nic.py | 23 +++++++------------ src/primaite/game/agent/actions/manager.py | 2 ++ src/primaite/game/agent/actions/network.py | 5 +++- 8 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 24a3ad67..9d6c0fcc 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -25,3 +25,16 @@ __all__ = ( "NodeServiceStopAction", "ActionManager", ) + +# __all__ = ( +# "acl", +# "application", +# "config", +# "file", +# "folder", +# "host_nic", +# "manager", +# "network", +# "node", +# "service", +# ) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 6aeafe4d..050e94e8 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -171,7 +171,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): ] -class RouterACLRemoveRuleAction(AbstractAction): +class RouterACLRemoveRuleAction(AbstractAction, identifier="router_acl_remove_rule"): """Action which removes a rule from a router's ACL.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -186,7 +186,7 @@ class RouterACLRemoveRuleAction(AbstractAction): return ["network", "node", config.target_router, "acl", "remove_rule", config.position] -class FirewallACLAddRuleAction(AbstractAction): +class FirewallACLAddRuleAction(AbstractAction, identifier="firewall_acl_add_rule"): """Action which adds a rule to a firewall port's ACL.""" def __init__( @@ -310,8 +310,9 @@ class FirewallACLAddRuleAction(AbstractAction): dst_port, position, ] - -class FirewallACLRemoveRuleAction(AbstractAction): + + +class FirewallACLRemoveRuleAction(AbstractAction, identifier="firewall_acl_remove_rule"): """Action which removes a rule from a firewall port's ACL.""" def __init__(self, manager: "ActionManager", max_acl_rules: int, **kwargs) -> None: @@ -325,8 +326,9 @@ class FirewallACLRemoveRuleAction(AbstractAction): super().__init__(manager=manager) self.shape: Dict[str, int] = {"position": max_acl_rules} + @classmethod def form_request( - self, target_firewall_nodename: str, firewall_port_name: str, firewall_port_direction: str, position: int + cls, target_firewall_nodename: str, firewall_port_name: str, firewall_port_direction: str, position: int ) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ @@ -338,4 +340,4 @@ class FirewallACLRemoveRuleAction(AbstractAction): "acl", "remove_rule", position, - ] \ No newline at end of file + ] diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 39a7b224..110b71da 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -75,6 +75,7 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction): # TODO: Either changes to application form_request bits, or add that here. + class NodeApplicationRemoveAction(NodeApplicationAbstractAction): """Action which removes/uninstalls an application""" @@ -84,4 +85,3 @@ class NodeApplicationRemoveAction(NodeApplicationAbstractAction): verb: str = "uninstall" # TODO: Either changes to application form_request bits, or add that here. - diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 8b3f99f1..c06ce9c8 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -1,7 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict, Optional -from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator + +from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo + from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -28,6 +30,7 @@ class ConfigureRansomwareScriptAction(AbstractAction): ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema return ["network", "node", node_name, "application", "RansomwareScript", "configure", config] + class ConfigureDoSBotAction(AbstractAction): """Action which sets config parameters for a DoS bot on a node.""" @@ -53,7 +56,7 @@ class ConfigureDoSBotAction(AbstractAction): return ["do_nothing"] self._Opts.model_validate(config) # check that options adhere to schema return ["network", "node", node_name, "application", "DoSBot", "configure", config] - + class ConfigureC2BeaconAction(AbstractAction): """Action which configures a C2 Beacon based on the parameters given.""" @@ -67,7 +70,6 @@ class ConfigureC2BeaconAction(AbstractAction): masquerade_protocol: str = Field(default="TCP") masquerade_port: str = Field(default="HTTP") - class _Opts(BaseModel): """Schema for options that can be passed to this action.""" @@ -104,4 +106,4 @@ class ConfigureC2BeaconAction(AbstractAction): ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] \ No newline at end of file + return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index 255731f6..b9e003c7 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -23,13 +23,11 @@ class NodeFolderAbstractAction(AbstractAction): verb: ClassVar[str] @classmethod - def form_request(cls, node_id: int, folder_id: int) -> RequestFormat: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = cls.manager.get_node_name_by_idx(node_id) - folder_name = cls.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) - if node_name is None or folder_name is None: + if config.node_name is None or config.folder_name is None: return ["do_nothing"] - return ["network", "node", node_name, "file_system", "folder", folder_name, cls.verb] + return ["network", "node", config.node_name, "file_system", "folder", config.folder_name, cls.verb] class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_scan"): diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index f8be9465..2909772b 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,10 +1,13 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict, Optional + from pydantic import BaseModel, ConfigDict + from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat + class HostNICAbstractAction(AbstractAction): """ Abstract base class for NIC actions. @@ -15,25 +18,12 @@ class HostNICAbstractAction(AbstractAction): class ConfigSchema(AbstractAction.ConfigSchema): """Base Configuration schema for HostNIC actions.""" + num_nodes: str max_nics_per_node: str node_name: str nic_num: str - def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: - """Init method for HostNICAbstractAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param num_nodes: Number of nodes in the simulation. - :type num_nodes: int - :param max_nics_per_node: Maximum number of NICs per node. - :type max_nics_per_node: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes, "nic_id": max_nics_per_node} - self.verb: str # define but don't initialise: defends against children classes not defining this - @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -41,11 +31,13 @@ class HostNICAbstractAction(AbstractAction): return ["do_nothing"] return ["network", "node", config.node_name, "network_interface", config.nic_num, cls.verb] + class HostNICEnableAction(HostNICAbstractAction): """Action which enables a NIC.""" class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICEnableAction.""" + verb: str = "enable" @@ -54,4 +46,5 @@ class HostNICDisableAction(HostNICAbstractAction): class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICDisableAction.""" - verb: str = "disable" \ No newline at end of file + + verb: str = "disable" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 09e5a851..9621b7f0 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -23,6 +23,8 @@ from pydantic import BaseModel, ConfigDict from primaite.game.game import PrimaiteGame from primaite.interface.request import RequestFormat +# TODO: Make sure that actions are backwards compatible where the old YAML format is used. + class AbstractAction(BaseModel): """Base class for actions.""" diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index aa6ef4d3..e761fb1e 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,10 +1,13 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict, Optional + from pydantic import BaseModel, ConfigDict + from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat + class NetworkPortEnableAction(AbstractAction): """Action which enables are port on a router or a firewall.""" @@ -40,4 +43,4 @@ class NetworkPortDisableAction(AbstractAction): """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if target_nodename is None or port_id is None: return ["do_nothing"] - return ["network", "node", target_nodename, "network_interface", port_id, "disable"] \ No newline at end of file + return ["network", "node", target_nodename, "network_interface", port_id, "disable"] From 83d3120b0456c11ba87d5789f1737deb961725d9 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 18 Oct 2024 14:52:50 +0100 Subject: [PATCH 030/224] #2912 - Additional actions added to config.py, refactor of HostNIC --- src/primaite/game/agent/actions/config.py | 70 ++++++++++++++++++++- src/primaite/game/agent/actions/host_nic.py | 4 +- src/primaite/game/agent/actions/network.py | 56 ++++++++--------- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index c06ce9c8..6096a0b2 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, Optional +from typing import Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo @@ -107,3 +107,71 @@ class ConfigureC2BeaconAction(AbstractAction): ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] + + +class NodeSendRemoteCommandAction(AbstractAction): + """Action which sends a terminal command to a remote node via SSH.""" + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + return [ + "network", + "node", + node_name, + "service", + "Terminal", + "send_remote_command", + remote_ip, + {"command": command}, + ] + + +class TerminalC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + commands: Union[List[RequestFormat], RequestFormat] + ip_address: Optional[str] + username: Optional[str] + password: Optional[str] + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, node_id: int, commands: List, ip_address: Optional[str], account: dict) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + + command_model = { + "commands": commands, + "ip_address": ip_address, + "username": account["username"], + "password": account["password"], + } + + TerminalC2ServerAction._Opts.model_validate(command_model) + return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] + + +class RansomwareLaunchC2ServerAction(AbstractAction): + """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + + class ConfigSchema(AbstractAction): + """Configuration schema for RansomwareLaunchC2ServerAction.""" + node_name: str + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + if config.node_name is None: + return ["do_nothing"] + # This action currently doesn't require any further configuration options. + return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] \ No newline at end of file diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 2909772b..a4dd8d9c 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -32,7 +32,7 @@ class HostNICAbstractAction(AbstractAction): return ["network", "node", config.node_name, "network_interface", config.nic_num, cls.verb] -class HostNICEnableAction(HostNICAbstractAction): +class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): """Action which enables a NIC.""" class ConfigSchema(HostNICAbstractAction.ConfigSchema): @@ -41,7 +41,7 @@ class HostNICEnableAction(HostNICAbstractAction): verb: str = "enable" -class HostNICDisableAction(HostNICAbstractAction): +class HostNICDisableAction(HostNICAbstractAction, identifier="host_nic_disable"): """Action which disables a NIC.""" class ConfigSchema(HostNICAbstractAction.ConfigSchema): diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index e761fb1e..630385bd 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, Optional +from typing import ClassVar, Dict, Optional from pydantic import BaseModel, ConfigDict @@ -8,39 +8,37 @@ from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat -class NetworkPortEnableAction(AbstractAction): +class NetworkPortAbstractAction(AbstractAction): + """Base class for Network port actions""" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Base configuration schema for NetworkPort actions.""" + target_nodename: str + port_id: str + + verb: ClassVar[str] + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.target_nodename is None or config.port_id is None: + return ["do_nothing"] + return ["network", "node", config.target_nodename, "network_interface", config.port_id, cls.verb] + + +class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_port_enable"): """Action which enables are port on a router or a firewall.""" - def __init__(self, manager: "ActionManager", max_nics_per_node: int, **kwargs) -> None: - """Init method for NetworkPortEnableAction. + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for NetworkPortEnableAction.""" - :param max_nics_per_node: Maximum number of NICs per node. - :type max_nics_per_node: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"port_id": max_nics_per_node} + verb: str = "enable" - def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if target_nodename is None or port_id is None: - return ["do_nothing"] - return ["network", "node", target_nodename, "network_interface", port_id, "enable"] - - -class NetworkPortDisableAction(AbstractAction): +class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_port_disable"): """Action which disables are port on a router or a firewall.""" - def __init__(self, manager: "ActionManager", max_nics_per_node: int, **kwargs) -> None: - """Init method for NetworkPortDisableAction. + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for NetworkPortDisableAction""" - :param max_nics_per_node: Maximum number of NICs per node. - :type max_nics_per_node: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"port_id": max_nics_per_node} + verb: str = "disable" - def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if target_nodename is None or port_id is None: - return ["do_nothing"] - return ["network", "node", target_nodename, "network_interface", port_id, "disable"] From a5c7565f0ebb2d121392b5afd79be7df10255e97 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 18 Oct 2024 16:28:15 +0100 Subject: [PATCH 031/224] #2912 - eod commit. Gutted ActionManager and corrected some identifiers. --- src/primaite/game/agent/actions/config.py | 43 ++++- src/primaite/game/agent/actions/host_nic.py | 5 - src/primaite/game/agent/actions/manager.py | 186 -------------------- src/primaite/game/agent/actions/session.py | 64 +++++++ 4 files changed, 106 insertions(+), 192 deletions(-) create mode 100644 src/primaite/game/agent/actions/session.py diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 6096a0b2..d627e4b0 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -174,4 +174,45 @@ class RansomwareLaunchC2ServerAction(AbstractAction): if config.node_name is None: return ["do_nothing"] # This action currently doesn't require any further configuration options. - return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] \ No newline at end of file + return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] + +class ExfiltrationC2ServerAction(AbstractAction): + """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" + + class _Opts(BaseModel): + """Schema for options that can be passed to this action.""" + + username: Optional[str] + password: Optional[str] + target_ip_address: str + target_file_name: str + target_folder_name: str + exfiltration_folder_name: Optional[str] + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request( + self, + node_id: int, + account: dict, + target_ip_address: str, + target_file_name: str, + target_folder_name: str, + exfiltration_folder_name: Optional[str], + ) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + node_name = self.manager.get_node_name_by_idx(node_id) + if node_name is None: + return ["do_nothing"] + + command_model = { + "target_file_name": target_file_name, + "target_folder_name": target_folder_name, + "exfiltration_folder_name": exfiltration_folder_name, + "target_ip_address": target_ip_address, + "username": account["username"], + "password": account["password"], + } + ExfiltrationC2ServerAction._Opts.model_validate(command_model) + return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] \ No newline at end of file diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index a4dd8d9c..2e53cf72 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,9 +1,4 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK - -from typing import Dict, Optional - -from pydantic import BaseModel, ConfigDict - from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 9621b7f0..d6b7d4b6 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -268,192 +268,6 @@ class ActionManager: """Return the gymnasium action space for this agent.""" return spaces.Discrete(len(self.action_map)) - # def get_node_name_by_idx(self, node_idx: int) -> str: - # """ - # Get the node name corresponding to the given index. - - # :param node_idx: The index of the node to retrieve. - # :type node_idx: int - # :return: The node hostname. - # :rtype: str - # """ - # if not node_idx < len(self.node_names): - # msg = ( - # f"Error: agent attempted to perform an action on node {node_idx}, but its action space only" - # f"has {len(self.node_names)} nodes." - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.node_names[node_idx] - - # def get_folder_name_by_idx(self, node_idx: int, folder_idx: int) -> Optional[str]: - # """ - # Get the folder name corresponding to the given node and folder indices. - - # :param node_idx: The index of the node. - # :type node_idx: int - # :param folder_idx: The index of the folder on the node. - # :type folder_idx: int - # :return: The name of the folder. Or None if the node has fewer folders than the given index. - # :rtype: Optional[str] - # """ - # if node_idx >= len(self.folder_names) or folder_idx >= len(self.folder_names[node_idx]): - # msg = ( - # f"Error: agent attempted to perform an action on node {node_idx} and folder {folder_idx}, but this" - # f" is out of range for its action space. Folder on each node: {self.folder_names}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.folder_names[node_idx][folder_idx] - - # def get_file_name_by_idx(self, node_idx: int, folder_idx: int, file_idx: int) -> Optional[str]: - # """Get the file name corresponding to the given node, folder, and file indices. - - # :param node_idx: The index of the node. - # :type node_idx: int - # :param folder_idx: The index of the folder on the node. - # :type folder_idx: int - # :param file_idx: The index of the file in the folder. - # :type file_idx: int - # :return: The name of the file. Or None if the node has fewer folders than the given index, or the folder has - # fewer files than the given index. - # :rtype: Optional[str] - # """ - # if ( - # node_idx >= len(self.file_names) - # or folder_idx >= len(self.file_names[node_idx]) - # or file_idx >= len(self.file_names[node_idx][folder_idx]) - # ): - # msg = ( - # f"Error: agent attempted to perform an action on node {node_idx} folder {folder_idx} file {file_idx}" - # f" but this is out of range for its action space. Files on each node: {self.file_names}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.file_names[node_idx][folder_idx][file_idx] - - # def get_service_name_by_idx(self, node_idx: int, service_idx: int) -> Optional[str]: - # """Get the service name corresponding to the given node and service indices. - - # :param node_idx: The index of the node. - # :type node_idx: int - # :param service_idx: The index of the service on the node. - # :type service_idx: int - # :return: The name of the service. Or None if the node has fewer services than the given index. - # :rtype: Optional[str] - # """ - # if node_idx >= len(self.service_names) or service_idx >= len(self.service_names[node_idx]): - # msg = ( - # f"Error: agent attempted to perform an action on node {node_idx} and service {service_idx}, but this" - # f" is out of range for its action space. Services on each node: {self.service_names}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.service_names[node_idx][service_idx] - - # def get_application_name_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]: - # """Get the application name corresponding to the given node and service indices. - - # :param node_idx: The index of the node. - # :type node_idx: int - # :param application_idx: The index of the service on the node. - # :type application_idx: int - # :return: The name of the service. Or None if the node has fewer services than the given index. - # :rtype: Optional[str] - # """ - # if node_idx >= len(self.application_names) or application_idx >= len(self.application_names[node_idx]): - # msg = ( - # f"Error: agent attempted to perform an action on node {node_idx} and app {application_idx}, but " - # f"this is out of range for its action space. Applications on each node: {self.application_names}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.application_names[node_idx][application_idx] - - # def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: - # """Get the internet protocol corresponding to the given index. - - # :param protocol_idx: The index of the protocol to retrieve. - # :type protocol_idx: int - # :return: The protocol. - # :rtype: str - # """ - # if protocol_idx >= len(self.protocols): - # msg = ( - # f"Error: agent attempted to perform an action on protocol {protocol_idx} but this" - # f" is out of range for its action space. Protocols: {self.protocols}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.protocols[protocol_idx] - - # def get_ip_address_by_idx(self, ip_idx: int) -> str: - # """ - # Get the IP address corresponding to the given index. - - # :param ip_idx: The index of the IP address to retrieve. - # :type ip_idx: int - # :return: The IP address. - # :rtype: str - # """ - # if ip_idx >= len(self.ip_address_list): - # msg = ( - # f"Error: agent attempted to perform an action on ip address {ip_idx} but this" - # f" is out of range for its action space. IP address list: {self.ip_address_list}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.ip_address_list[ip_idx] - - # def get_wildcard_by_idx(self, wildcard_idx: int) -> str: - # """ - # Get the IP wildcard corresponding to the given index. - - # :param ip_idx: The index of the IP wildcard to retrieve. - # :type ip_idx: int - # :return: The wildcard address. - # :rtype: str - # """ - # if wildcard_idx >= len(self.wildcard_list): - # msg = ( - # f"Error: agent attempted to perform an action on ip wildcard {wildcard_idx} but this" - # f" is out of range for its action space. Wildcard list: {self.wildcard_list}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.wildcard_list[wildcard_idx] - - # def get_port_by_idx(self, port_idx: int) -> str: - # """ - # Get the port corresponding to the given index. - - # :param port_idx: The index of the port to retrieve. - # :type port_idx: int - # :return: The port. - # :rtype: str - # """ - # if port_idx >= len(self.ports): - # msg = ( - # f"Error: agent attempted to perform an action on port {port_idx} but this" - # f" is out of range for its action space. Port list: {self.ip_address_list}" - # ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) - # return self.ports[port_idx] - - # def get_nic_num_by_idx(self, node_idx: int, nic_idx: int) -> int: - # """ - # Get the NIC number corresponding to the given node and NIC indices. - - # :param node_idx: The index of the node. - # :type node_idx: int - # :param nic_idx: The index of the NIC on the node. - # :type nic_idx: int - # :return: The NIC number. - # :rtype: int - # """ - # return nic_idx + 1 - @classmethod def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": """ diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py new file mode 100644 index 00000000..9fd20a0c --- /dev/null +++ b/src/primaite/game/agent/actions/session.py @@ -0,0 +1,64 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from abc import abstractmethod +from typing import ClassVar + +from primaite.game.agent.actions.manager import AbstractAction +from primaite.interface.request import RequestFormat + + +class NodeSessionAbstractAction(AbstractAction): + """Base class for NodeSession actions.""" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Base configuration schema for NodeSessionAbstractActions.""" + + node_name: str + remote_ip: str + + @abstractmethod + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Abstract method. Should return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.remote_ip is None: + return ["do_nothing"] + + +class NodeSessionsRemoteLoginAction(AbstractAction, identifier="node_session_remote_login"): + """Action which performs a remote session login.""" + + class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): + """Configuration schema for NodeSessionsRemoteLoginAction.""" + username: str + password: str + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.remote_ip is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "service", + "Terminal", + "ssh_to_remote", + config.username, + config.password, + config.remote_ip, + ] + + +class NodeSessionsRemoteLogoutAction(AbstractAction, identifier="node_session_remote_logout"): + """Action which performs a remote session logout.""" + + class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): + """Configuration schema for NodeSessionsRemoteLogoutAction.""" + pass + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.remote_ip is None: + return ["do_nothing"] + return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip] \ No newline at end of file From bbcbb26f5edd79202bfed16b46a7bc3573b60397 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 21 Oct 2024 14:43:51 +0100 Subject: [PATCH 032/224] #2913: Fix ActionPenalty. --- src/primaite/game/agent/rewards.py | 35 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 4198af27..1158d919 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -28,7 +28,7 @@ the structure: ``` """ from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from typing import Any, ClassVar, Callable, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union from pydantic import BaseModel from typing_extensions import Never @@ -51,6 +51,8 @@ class AbstractReward(BaseModel): type: str + _registry: ClassVar[Dict[str, Type["AbstractReward"]]] = {} + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) if identifier in cls._registry: @@ -58,7 +60,6 @@ class AbstractReward(BaseModel): cls._registry[identifier] = cls @classmethod - @abstractmethod def from_config(cls, config: Dict) -> "AbstractReward": """Create a reward function component from a config dictionary. @@ -69,7 +70,8 @@ class AbstractReward(BaseModel): """ if config["type"] not in cls._registry: raise ValueError(f"Invalid reward type {config['type']}") - + adder_class = cls._registry[config["type"]] + adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config)) return cls @abstractmethod @@ -276,7 +278,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebpageUnavailablePenalty.""" - node_hostname: str + node_hostname: str = "" sticky: bool = True def __init__(self, node_hostname: str, sticky: bool = True) -> None: @@ -494,6 +496,8 @@ class SharedReward(AbstractReward, identifier="SharedReward"): class ActionPenalty(AbstractReward, identifier="ActionPenalty"): """Apply a negative reward when taking any action except DONOTHING.""" + action_penalty: float = -1.0 + do_nothing_penalty: float = 0.0 class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for ActionPenalty.""" @@ -501,19 +505,20 @@ class ActionPenalty(AbstractReward, identifier="ActionPenalty"): action_penalty: float = -1.0 do_nothing_penalty: float = 0.0 - def __init__(self, action_penalty: float, do_nothing_penalty: float) -> None: - """ - Initialise the reward. + # def __init__(self, action_penalty: float, do_nothing_penalty: float) -> None: + # """ + # Initialise the reward. - Reward or penalise agents for doing nothing or taking actions. + # Reward or penalise agents for doing nothing or taking actions. - :param action_penalty: Reward to give agents for taking any action except DONOTHING - :type action_penalty: float - :param do_nothing_penalty: Reward to give agent for taking the DONOTHING action - :type do_nothing_penalty: float - """ - self.action_penalty = action_penalty - self.do_nothing_penalty = do_nothing_penalty + # :param action_penalty: Reward to give agents for taking any action except DONOTHING + # :type action_penalty: float + # :param do_nothing_penalty: Reward to give agent for taking the DONOTHING action + # :type do_nothing_penalty: float + # """ + # super().__init__(action_penalty=action_penalty, do_nothing_penalty=do_nothing_penalty) + # self.action_penalty = action_penalty + # self.do_nothing_penalty = do_nothing_penalty def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the penalty to be applied. From 0cf8e20e6da5d8941dec6080ca687649437106a0 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 21 Oct 2024 17:11:11 +0100 Subject: [PATCH 033/224] #2913: Update reward classes to work with pydantic. --- src/primaite/game/agent/rewards.py | 110 ++++++++++-------- .../game_layer/test_rewards.py | 2 +- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 1158d919..9777441b 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -274,28 +274,34 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePenalty"): """Penalises the agent when the web browser fails to fetch a webpage.""" + node_hostname: str = "" + sticky: bool = True + reward: float = 0.0 + location_in_state: List[str] = [""] + _node: str = node_hostname class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebpageUnavailablePenalty.""" node_hostname: str = "" sticky: bool = True + reward: float = 0.0 - def __init__(self, node_hostname: str, sticky: bool = True) -> None: - """ - Initialise the reward component. + # def __init__(self, node_hostname: str, sticky: bool = True) -> None: + # """ + # Initialise the reward component. - :param node_hostname: Hostname of the node which has the web browser. - :type node_hostname: str - :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate - the reward if there were any responses this timestep. - :type sticky: bool - """ - self._node: str = node_hostname - self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] - self.sticky: bool = sticky - self.reward: float = 0.0 - """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" + # :param node_hostname: Hostname of the node which has the web browser. + # :type node_hostname: str + # :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate + # the reward if there were any responses this timestep. + # :type sticky: bool + # """ + # self._node: str = node_hostname + # self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] + # self.sticky: bool = sticky + # self.reward: float = 0.0 + # """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ @@ -311,6 +317,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe :return: Reward value :rtype: float """ + self.location_in_state: List[str] = ["network", "nodes", self.node_hostname, "applications", "WebBrowser"] web_browser_state = access_from_nested_dict(state, self.location_in_state) if web_browser_state is NOT_PRESENT_IN_STATE: @@ -364,6 +371,10 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdminDatabaseUnreachablePenalty"): """Penalises the agent when the green db clients fail to connect to the database.""" + node_hostname: str = "" + _node: str = node_hostname + sticky: bool = True + reward: float = 0.0 class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for GreenAdminDatabaseUnreachablePenalty.""" @@ -371,21 +382,21 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi node_hostname: str sticky: bool = True - def __init__(self, node_hostname: str, sticky: bool = True) -> None: - """ - Initialise the reward component. + # def __init__(self, node_hostname: str, sticky: bool = True) -> None: + # """ + # Initialise the reward component. - :param node_hostname: Hostname of the node where the database client sits. - :type node_hostname: str - :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate - the reward if there were any responses this timestep. - :type sticky: bool - """ - self._node: str = node_hostname - self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] - self.sticky: bool = sticky - self.reward: float = 0.0 - """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" + # :param node_hostname: Hostname of the node where the database client sits. + # :type node_hostname: str + # :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate + # the reward if there were any responses this timestep. + # :type sticky: bool + # """ + # self._node: str = node_hostname + # self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] + # self.sticky: bool = sticky + # self.reward: float = 0.0 + # """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ @@ -438,37 +449,38 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi class SharedReward(AbstractReward, identifier="SharedReward"): """Adds another agent's reward to the overall reward.""" + agent_name: str class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for SharedReward.""" agent_name: str - def __init__(self, agent_name: Optional[str] = None) -> None: + # def __init__(self, agent_name: Optional[str] = None) -> None: + # """ + # Initialise the shared reward. + + # The agent_name is a placeholder value. It starts off as none, but it must be set before this reward can work + # correctly. + + # :param agent_name: The name whose reward is an input + # :type agent_name: Optional[str] + # """ + # # self.agent_name = agent_name + # """Agent whose reward to track.""" + + def default_callback(agent_name: str) -> Never: """ - Initialise the shared reward. + Default callback to prevent calling this reward until it's properly initialised. - The agent_name is a placeholder value. It starts off as none, but it must be set before this reward can work - correctly. - - :param agent_name: The name whose reward is an input - :type agent_name: Optional[str] + SharedReward should not be used until the game layer replaces self.callback with a reference to the + function that retrieves the desired agent's reward. Therefore, we define this default callback that raises + an error. """ - self.agent_name = agent_name - """Agent whose reward to track.""" + raise RuntimeError("Attempted to calculate SharedReward but it was not initialised properly.") - def default_callback(agent_name: str) -> Never: - """ - Default callback to prevent calling this reward until it's properly initialised. - - SharedReward should not be used until the game layer replaces self.callback with a reference to the - function that retrieves the desired agent's reward. Therefore, we define this default callback that raises - an error. - """ - raise RuntimeError("Attempted to calculate SharedReward but it was not initialised properly.") - - self.callback: Callable[[str], float] = default_callback - """Method that retrieves an agent's current reward given the agent's name.""" + callback: Callable[[str], float] = default_callback + """Method that retrieves an agent's current reward given the agent's name.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Simply access the other agent's reward and return it. diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 0005b508..bf707feb 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -74,7 +74,7 @@ def test_uc2_rewards(game_and_agent): ACLAction.PERMIT, src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"], position=2 ) - comp = GreenAdminDatabaseUnreachablePenalty("client_1") + comp = GreenAdminDatabaseUnreachablePenalty(node_hostname="client_1") request = ["network", "node", "client_1", "application", "DatabaseClient", "execute"] response = game.simulation.apply_request(request) From 11357f87caacfe8531b5b4b0e707456a009e7dfc Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 21 Oct 2024 17:51:55 +0100 Subject: [PATCH 034/224] #2912 - eod commit. Addressing test and lint errors for refactored actions --- src/primaite/game/agent/actions/__init__.py | 57 +++++------- src/primaite/game/agent/actions/acl.py | 93 +++++++------------ .../game/agent/actions/application.py | 19 ++-- src/primaite/game/agent/actions/config.py | 72 ++++++++------ src/primaite/game/agent/actions/file.py | 11 ++- src/primaite/game/agent/actions/folder.py | 15 ++- src/primaite/game/agent/actions/host_nic.py | 4 +- src/primaite/game/agent/actions/manager.py | 12 ++- src/primaite/game/agent/actions/network.py | 17 ++-- src/primaite/game/agent/actions/node.py | 7 +- src/primaite/game/agent/actions/service.py | 32 ++++++- src/primaite/game/agent/actions/session.py | 23 +++-- .../game/agent/observations/__init__.py | 2 +- 13 files changed, 204 insertions(+), 160 deletions(-) diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 9d6c0fcc..428c6c58 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -1,40 +1,31 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.game.agent.actions.manager import ActionManager -from primaite.game.agent.actions.service import ( - NodeServiceDisableAction, - NodeServiceEnableAction, - NodeServiceFixAction, - NodeServicePauseAction, - NodeServiceRestartAction, - NodeServiceResumeAction, - NodeServiceScanAction, - NodeServiceStartAction, - NodeServiceStopAction, +from primaite.game.agent.actions import ( + acl, + application, + config, + file, + folder, + host_nic, + manager, + network, + node, + service, + session, ) +from primaite.game.agent.actions.manager import ActionManager __all__ = ( - "NodeServiceDisableAction", - "NodeServiceEnableAction", - "NodeServiceFixAction", - "NodeServicePauseAction", - "NodeServiceRestartAction", - "NodeServiceResumeAction", - "NodeServiceScanAction", - "NodeServiceStartAction", - "NodeServiceStopAction", + "acl", + "application", + "config", + "file", + "folder", + "host_nic", + "manager", + "network", + "node", + "service", + "session", "ActionManager", ) - -# __all__ = ( -# "acl", -# "application", -# "config", -# "file", -# "folder", -# "host_nic", -# "manager", -# "network", -# "node", -# "service", -# ) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 050e94e8..1048dc1e 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -4,13 +4,28 @@ from typing import Dict, List, Literal from pydantic import BaseModel, Field, field_validator, ValidationInfo from primaite.game.agent.actions.manager import AbstractAction -from primaite.game.game import _LOGGER from primaite.interface.request import RequestFormat +__all__ = ("RouterACLAddRuleAction", "RouterACLRemoveRuleAction", "FirewallACLAddRuleAction", "FirewallACLRemoveRuleAction") class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration Schema for RouterACLAddRuleAction.""" + + target_router: str + position: int + permission: Literal[1, 2] + source_ip_id: int + source_wildcard_id: int + source_port_id: int + dest_ip_id: int + dest_wildcard_id: int + dest_port_id: int + protocol_id: int + + class ACLRuleOptions(BaseModel): """Validator for ACL_ADD_RULE options.""" @@ -52,73 +67,31 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): return cls.model_fields[info.field_name].default return v - def __init__( - self, - manager: "ActionManager", - max_acl_rules: int, - num_ips: int, - num_ports: int, - num_protocols: int, - **kwargs, - ) -> None: - """Init method for RouterACLAddRuleAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - :param num_ips: Number of IP addresses in the simulation. - :type num_ips: int - :param num_ports: Number of ports in the simulation. - :type num_ports: int - :param num_protocols: Number of protocols in the simulation. - :type num_protocols: int - """ - super().__init__(manager=manager) - num_permissions = 3 - self.shape: Dict[str, int] = { - "position": max_acl_rules, - "permission": num_permissions, - "source_ip_id": num_ips, - "dest_ip_id": num_ips, - "source_port_id": num_ports, - "dest_port_id": num_ports, - "protocol_id": num_protocols, - } - + @classmethod def form_request( - self, - target_router: str, - position: int, - permission: int, - source_ip_id: int, - source_wildcard_id: int, - dest_ip_id: int, - dest_wildcard_id: int, - source_port_id: int, - dest_port_id: int, - protocol_id: int, + cls, + config: ConfigSchema ) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" # Validate incoming data. parsed_options = RouterACLAddRuleAction.ACLRuleOptions( - target_router=target_router, - position=position, - permission=permission, - source_ip_id=source_ip_id, - source_wildcard_id=source_wildcard_id, - dest_ip_id=dest_ip_id, - dest_wildcard_id=dest_wildcard_id, - source_port_id=source_port_id, - dest_port_id=dest_port_id, - protocol_id=protocol_id, + target_router=config.target_router, + position=config.position, + permission=config.permission, + source_ip_id=config.source_ip_id, + source_wildcard_id=config.source_wildcard_id, + dest_ip_id=config.dest_ip_id, + dest_wildcard_id=config.dest_wildcard_id, + source_port_id=config.source_port_id, + dest_port_id=config.dest_port_id, + protocol_id=config.protocol_id, ) if parsed_options.permission == 1: permission_str = "PERMIT" elif parsed_options.permission == 2: permission_str = "DENY" - else: - _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") + # else: + # _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") if parsed_options.protocol_id == 1: protocol = "ALL" @@ -246,8 +219,8 @@ class FirewallACLAddRuleAction(AbstractAction, identifier="firewall_acl_add_rule permission_str = "PERMIT" elif permission == 2: permission_str = "DENY" - else: - _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") + # else: + # _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") if protocol_id == 0: return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 110b71da..3a254d57 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -4,8 +4,17 @@ from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ( + "NodeApplicationExecuteAction", + "NodeApplicationScanAction", + "NodeApplicationCloseAction", + "NodeApplicationFixAction", + "NodeApplicationInstallAction", + "NodeApplicationRemoveAction", +) -class NodeApplicationAbstractAction(AbstractAction): + +class NodeApplicationAbstractAction(AbstractAction, identifier="node_application_abstract_action"): """ Base class for application actions. @@ -65,7 +74,7 @@ class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_a verb: str = "fix" -class NodeApplicationInstallAction(NodeApplicationAbstractAction): +class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="node_application_install"): """Action which installs an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): @@ -76,12 +85,10 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction): # TODO: Either changes to application form_request bits, or add that here. -class NodeApplicationRemoveAction(NodeApplicationAbstractAction): - """Action which removes/uninstalls an application""" +class NodeApplicationRemoveAction(NodeApplicationAbstractAction, identifier="node_application_remove"): + """Action which removes/uninstalls an application.""" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationRemoveAction.""" verb: str = "uninstall" - - # TODO: Either changes to application form_request bits, or add that here. diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index d627e4b0..e92d443b 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -7,31 +7,39 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationIn from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ( + "ConfigureRansomwareScriptAction", + "ConfigureDoSBotAction", + "ConfigureC2BeaconAction", + "NodeSendRemoteCommandAction", + "TerminalC2ServerAction", + "RansomwareLaunchC2ServerAction", + "ExfiltrationC2ServerAction", +) -class ConfigureRansomwareScriptAction(AbstractAction): + +class ConfigureRansomwareScriptAction(AbstractAction, identifier="configure_ransomware"): """Action which sets config parameters for a ransomware script on a node.""" - class _Opts(BaseModel): - """Schema for options that can be passed to this option.""" + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for ConfigureRansomwareScriptAction.""" model_config = ConfigDict(extra="forbid") - server_ip_address: Optional[str] = None - server_password: Optional[str] = None - payload: Optional[str] = None + node_name: str + server_ip_address: Optional[str] + server_password: Optional[str] + payload: Optional[str] - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, config: Dict) -> RequestFormat: + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: + if config.node_name is None: return ["do_nothing"] ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "RansomwareScript", "configure", config] + return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", config] -class ConfigureDoSBotAction(AbstractAction): +class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): """Action which sets config parameters for a DoS bot on a node.""" class _Opts(BaseModel): @@ -58,7 +66,7 @@ class ConfigureDoSBotAction(AbstractAction): return ["network", "node", node_name, "application", "DoSBot", "configure", config] -class ConfigureC2BeaconAction(AbstractAction): +class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2"): """Action which configures a C2 Beacon based on the parameters given.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -109,28 +117,32 @@ class ConfigureC2BeaconAction(AbstractAction): return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] -class NodeSendRemoteCommandAction(AbstractAction): +class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_command"): """Action which sends a terminal command to a remote node via SSH.""" - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for NodeSendRemoteCommandAction.""" - def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: + node_name: str + remote_ip: str + command: RequestFormat + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) return [ "network", "node", - node_name, + config.node_name, "service", "Terminal", "send_remote_command", - remote_ip, - {"command": command}, + config.remote_ip, + {"command": config.command}, ] -class TerminalC2ServerAction(AbstractAction): +class TerminalC2ServerAction(AbstractAction, identifier="terminal_c2_server"): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" class _Opts(BaseModel): @@ -161,11 +173,12 @@ class TerminalC2ServerAction(AbstractAction): return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] -class RansomwareLaunchC2ServerAction(AbstractAction): +class RansomwareLaunchC2ServerAction(AbstractAction, identifier="ransomware_launch"): """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" - class ConfigSchema(AbstractAction): + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for RansomwareLaunchC2ServerAction.""" + node_name: str @classmethod @@ -175,8 +188,9 @@ class RansomwareLaunchC2ServerAction(AbstractAction): return ["do_nothing"] # This action currently doesn't require any further configuration options. return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] - -class ExfiltrationC2ServerAction(AbstractAction): + + +class ExfiltrationC2ServerAction(AbstractAction, identifier="exfiltration_c2_server"): """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" class _Opts(BaseModel): @@ -215,4 +229,4 @@ class ExfiltrationC2ServerAction(AbstractAction): "password": account["password"], } ExfiltrationC2ServerAction._Opts.model_validate(command_model) - return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] \ No newline at end of file + return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index 77bd8ef3..6935a11c 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -4,8 +4,17 @@ from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ( + "NodeFileCreateAction", + "NodeFileScanAction", + "NodeFileDeleteAction", + "NodeFileRestoreAction", + "NodeFileCorruptAction", + "NodeFileAccessAction", +) -class NodeFileAbstractAction(AbstractAction): + +class NodeFileAbstractAction(AbstractAction, identifier="node_file_abstract_action"): """Abstract base class for file actions. Any action which applies to a file and uses node_name, folder_name, and file_name as its diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index b9e003c7..74820eb0 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -1,16 +1,23 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from abc import abstractmethod -from typing import ClassVar, Dict +from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ( + "NodeFolderScanAction", + "NodeFolderCheckhashAction", + "NodeFolderRepairAction", + "NodeFolderRestoreAction", + "NodeFolderCreateAction", +) -class NodeFolderAbstractAction(AbstractAction): + +class NodeFolderAbstractAction(AbstractAction, identifier="node_folder_abstract"): """ Base class for folder actions. - Any action which applies to a folder and uses node_id and folder_id as its only two parameters can inherit from + Any action which applies to a folder and uses node_name and folder_name as its only two parameters can inherit from this base class. """ diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 2e53cf72..4f66f9b9 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -2,8 +2,10 @@ from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ("HostNICEnableAction", "HostNICDisableAction") -class HostNICAbstractAction(AbstractAction): + +class HostNICAbstractAction(AbstractAction, identifier="host_nic_abstract"): """ Abstract base class for NIC actions. diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index d6b7d4b6..2f47ea7c 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -"""yaml example +"""yaml example. agents: - name: agent_1 @@ -20,7 +20,7 @@ from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type from gymnasium import spaces from pydantic import BaseModel, ConfigDict -from primaite.game.game import PrimaiteGame +# from primaite.game.game import PrimaiteGame # TODO: Breaks things from primaite.interface.request import RequestFormat # TODO: Make sure that actions are backwards compatible where the old YAML format is used. @@ -37,6 +37,8 @@ class AbstractAction(BaseModel): # CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be # instantiated.) class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options` + """Base configuration schema for Actions.""" + model_config = ConfigDict(extra="forbid") type: str @@ -54,8 +56,12 @@ class AbstractAction(BaseModel): return [] -class DoNothingAction(AbstractAction): +class DoNothingAction(AbstractAction, identifier="do_nothing"): + """Do Nothing Action.""" + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration Schema for DoNothingAction.""" + type: Literal["do_nothing"] = "do_nothing" def form_request(self, options: ConfigSchema) -> RequestFormat: diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index 630385bd..63eff218 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,18 +1,19 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import ClassVar, Dict, Optional - -from pydantic import BaseModel, ConfigDict +from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ("NetworkPortEnableAction", "NetworkPortDisableAction") -class NetworkPortAbstractAction(AbstractAction): - """Base class for Network port actions""" + +class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstract"): + """Base class for Network port actions.""" class ConfigSchema(AbstractAction.ConfigSchema): """Base configuration schema for NetworkPort actions.""" + target_nodename: str port_id: str @@ -21,7 +22,7 @@ class NetworkPortAbstractAction(AbstractAction): @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.target_nodename is None or config.port_id is None: + if config.target_nodename is None or config.port_id is None: return ["do_nothing"] return ["network", "node", config.target_nodename, "network_interface", config.port_id, cls.verb] @@ -34,11 +35,11 @@ class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_por verb: str = "enable" + class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_port_disable"): """Action which disables are port on a router or a firewall.""" class ConfigSchema(AbstractAction.ConfigSchema): - """Configuration schema for NetworkPortDisableAction""" + """Configuration schema for NetworkPortDisableAction.""" verb: str = "disable" - diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index d431d344..011ff4dc 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,12 +1,13 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from abc import abstractmethod -from typing import ClassVar, Dict +from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ("NodeOSScanAction", "NodeShutdownAction", "NodeStartupAction", "NodeResetAction") -class NodeAbstractAction(AbstractAction): + +class NodeAbstractAction(AbstractAction, identifier="node_abstract"): """ Abstract base class for node actions. diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index 97b37bde..cf277b5d 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -4,8 +4,20 @@ from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ( + "NodeServiceScanAction", + "NodeServiceStopAction", + "NodeServiceStartAction", + "NodeServicePauseAction", + "NodeServiceResumeAction", + "NodeServiceRestartAction", + "NodeServiceDisableAction", + "NodeServiceEnableAction", + "NodeServiceFixAction", +) -class NodeServiceAbstractAction(AbstractAction): + +class NodeServiceAbstractAction(AbstractAction, identifier="node_service_abstract"): class ConfigSchema(AbstractAction.ConfigSchema): node_name: str service_name: str @@ -22,6 +34,8 @@ class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_ """Action which scans a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceScanAction""" + verb: str = "scan" @@ -29,6 +43,8 @@ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_ """Action which stops a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceStopAction.""" + verb: str = "stop" @@ -36,6 +52,8 @@ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service """Action which starts a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceStartAction.""" + verb: str = "start" @@ -43,6 +61,8 @@ class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service """Action which pauses a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServicePauseAction.""" + verb: str = "pause" @@ -50,6 +70,8 @@ class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_servic """Action which resumes a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceResumeAction.""" + verb: str = "resume" @@ -57,6 +79,8 @@ class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_servi """Action which restarts a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceRestartAction.""" + verb: str = "restart" @@ -64,6 +88,8 @@ class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_servi """Action which disables a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceDisableAction.""" + verb: str = "disable" @@ -71,6 +97,8 @@ class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_servic """Action which enables a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceEnableAction.""" + verb: str = "enable" @@ -78,4 +106,6 @@ class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_f """Action which fixes a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): + """Configuration Schema for NodeServiceFixAction.""" + verb: str = "fix" diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 9fd20a0c..eb035ff3 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -1,33 +1,35 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod -from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +__all__ = ("NodeSessionsRemoteLoginAction", "NodeSessionsRemoteLogoutAction") -class NodeSessionAbstractAction(AbstractAction): + +class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstract"): """Base class for NodeSession actions.""" class ConfigSchema(AbstractAction.ConfigSchema): """Base configuration schema for NodeSessionAbstractActions.""" - + node_name: str remote_ip: str - @abstractmethod @classmethod + @abstractmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: - """Abstract method. Should return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.node_name is None or config.remote_ip is None: - return ["do_nothing"] + """Abstract method. Should return the action formatted as a request which + can be ingested by the PrimAITE simulation.""" + pass -class NodeSessionsRemoteLoginAction(AbstractAction, identifier="node_session_remote_login"): +class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_session_remote_login"): """Action which performs a remote session login.""" class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLoginAction.""" + username: str password: str @@ -49,11 +51,12 @@ class NodeSessionsRemoteLoginAction(AbstractAction, identifier="node_session_rem ] -class NodeSessionsRemoteLogoutAction(AbstractAction, identifier="node_session_remote_logout"): +class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node_session_remote_logout"): """Action which performs a remote session logout.""" class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLogoutAction.""" + pass @classmethod @@ -61,4 +64,4 @@ class NodeSessionsRemoteLogoutAction(AbstractAction, identifier="node_session_re """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.remote_ip is None: return ["do_nothing"] - return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip] \ No newline at end of file + return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip] diff --git a/src/primaite/game/agent/observations/__init__.py b/src/primaite/game/agent/observations/__init__.py index 6c88f844..c4811c98 100644 --- a/src/primaite/game/agent/observations/__init__.py +++ b/src/primaite/game/agent/observations/__init__.py @@ -17,5 +17,5 @@ from primaite.game.agent.observations.software_observation import ApplicationObs __all__ = [ "ACLObservation", "FileObservation", "FolderObservation", "FirewallObservation", "HostObservation", "LinksObservation", "NICObservation", "PortObservation", "NodesObservation", "NestedObservation", - "ObservationManager", "ApplicationObservation", "ServiceObservation",] + "ObservationManager", "ApplicationObservation", "ServiceObservation", "RouterObservation", "LinkObservation",] # fmt: on From f95ba8cbbcd92779a66684c6bd2dbbf51052e5d2 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 22 Oct 2024 11:01:35 +0100 Subject: [PATCH 035/224] #2913: Fix remaining pydantic errors. --- src/primaite/game/agent/rewards.py | 141 ++++++------------ .../_game/_agent/test_sticky_rewards.py | 12 +- 2 files changed, 48 insertions(+), 105 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 9777441b..1f870e83 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -28,7 +28,7 @@ the structure: ``` """ from abc import ABC, abstractmethod -from typing import Any, ClassVar, Callable, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from typing import Any, Callable, ClassVar, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union from pydantic import BaseModel from typing_extensions import Never @@ -118,6 +118,12 @@ class DummyReward(AbstractReward, identifier="DummyReward"): class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" + node_hostname: str + folder_name: str + file_name: str + location_in_state: List[str] = [""] + reward: float = 0.0 + class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for DatabaseFileIntegrity.""" @@ -125,27 +131,6 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): folder_name: str file_name: str - def __init__(self, node_hostname: str, folder_name: str, file_name: str) -> None: - """Initialise the reward component. - - :param node_hostname: Hostname of the node which contains the database file. - :type node_hostname: str - :param folder_name: folder which contains the database file. - :type folder_name: str - :param file_name: name of the database file. - :type file_name: str - """ - self.location_in_state = [ - "network", - "nodes", - node_hostname, - "file_system", - "folders", - folder_name, - "files", - file_name, - ] - def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. @@ -156,6 +141,17 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): :return: Reward value :rtype: float """ + self.location_in_state = [ + "network", + "nodes", + self.node_hostname, + "file_system", + "folders", + self.folder_name, + "files", + self.file_name, + ] + database_file_state = access_from_nested_dict(state, self.location_in_state) if database_file_state is NOT_PRESENT_IN_STATE: _LOGGER.debug( @@ -195,6 +191,12 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): """Reward function component which penalises the agent when the web server returns a 404 error.""" + node_hostname: str + service_name: str + sticky: bool = True + location_in_state: List[str] = [""] + reward: float = 0.0 + class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebServer404Penalty.""" @@ -202,22 +204,6 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): service_name: str sticky: bool = True - def __init__(self, node_hostname: str, service_name: str, sticky: bool = True) -> None: - """Initialise the reward component. - - :param node_hostname: Hostname of the node which contains the web server service. - :type node_hostname: str - :param service_name: Name of the web server service. - :type service_name: str - :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate - the reward if there were any responses this timestep. - :type sticky: bool - """ - self.sticky: bool = sticky - self.reward: float = 0.0 - """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" - self.location_in_state = ["network", "nodes", node_hostname, "services", service_name] - def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. @@ -228,6 +214,13 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): :return: Reward value :rtype: float """ + self.location_in_state = [ + "network", + "nodes", + self.node_hostname, + "services", + self.service_name, + ] web_service_state = access_from_nested_dict(state, self.location_in_state) # if webserver is no longer installed on the node, return 0 @@ -274,6 +267,7 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePenalty"): """Penalises the agent when the web browser fails to fetch a webpage.""" + node_hostname: str = "" sticky: bool = True reward: float = 0.0 @@ -287,22 +281,6 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe sticky: bool = True reward: float = 0.0 - # def __init__(self, node_hostname: str, sticky: bool = True) -> None: - # """ - # Initialise the reward component. - - # :param node_hostname: Hostname of the node which has the web browser. - # :type node_hostname: str - # :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate - # the reward if there were any responses this timestep. - # :type sticky: bool - # """ - # self._node: str = node_hostname - # self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] - # self.sticky: bool = sticky - # self.reward: float = 0.0 - # """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" - def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ Calculate the reward based on current simulation state, and the recent agent action. @@ -317,7 +295,13 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe :return: Reward value :rtype: float """ - self.location_in_state: List[str] = ["network", "nodes", self.node_hostname, "applications", "WebBrowser"] + self.location_in_state = [ + "network", + "nodes", + self.node_hostname, + "applications", + "WebBrowser", + ] web_browser_state = access_from_nested_dict(state, self.location_in_state) if web_browser_state is NOT_PRESENT_IN_STATE: @@ -371,6 +355,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdminDatabaseUnreachablePenalty"): """Penalises the agent when the green db clients fail to connect to the database.""" + node_hostname: str = "" _node: str = node_hostname sticky: bool = True @@ -382,22 +367,6 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi node_hostname: str sticky: bool = True - # def __init__(self, node_hostname: str, sticky: bool = True) -> None: - # """ - # Initialise the reward component. - - # :param node_hostname: Hostname of the node where the database client sits. - # :type node_hostname: str - # :param sticky: If True, calculate the reward based on the most recent response status. If False, only calculate - # the reward if there were any responses this timestep. - # :type sticky: bool - # """ - # self._node: str = node_hostname - # self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] - # self.sticky: bool = sticky - # self.reward: float = 0.0 - # """Reward value calculated last time any responses were seen. Used for persisting sticky rewards.""" - def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ Calculate the reward based on current simulation state, and the recent agent action. @@ -449,6 +418,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi class SharedReward(AbstractReward, identifier="SharedReward"): """Adds another agent's reward to the overall reward.""" + agent_name: str class ConfigSchema(AbstractReward.ConfigSchema): @@ -456,19 +426,6 @@ class SharedReward(AbstractReward, identifier="SharedReward"): agent_name: str - # def __init__(self, agent_name: Optional[str] = None) -> None: - # """ - # Initialise the shared reward. - - # The agent_name is a placeholder value. It starts off as none, but it must be set before this reward can work - # correctly. - - # :param agent_name: The name whose reward is an input - # :type agent_name: Optional[str] - # """ - # # self.agent_name = agent_name - # """Agent whose reward to track.""" - def default_callback(agent_name: str) -> Never: """ Default callback to prevent calling this reward until it's properly initialised. @@ -508,6 +465,7 @@ class SharedReward(AbstractReward, identifier="SharedReward"): class ActionPenalty(AbstractReward, identifier="ActionPenalty"): """Apply a negative reward when taking any action except DONOTHING.""" + action_penalty: float = -1.0 do_nothing_penalty: float = 0.0 @@ -517,21 +475,6 @@ class ActionPenalty(AbstractReward, identifier="ActionPenalty"): action_penalty: float = -1.0 do_nothing_penalty: float = 0.0 - # def __init__(self, action_penalty: float, do_nothing_penalty: float) -> None: - # """ - # Initialise the reward. - - # Reward or penalise agents for doing nothing or taking actions. - - # :param action_penalty: Reward to give agents for taking any action except DONOTHING - # :type action_penalty: float - # :param do_nothing_penalty: Reward to give agent for taking the DONOTHING action - # :type do_nothing_penalty: float - # """ - # super().__init__(action_penalty=action_penalty, do_nothing_penalty=do_nothing_penalty) - # self.action_penalty = action_penalty - # self.do_nothing_penalty = do_nothing_penalty - def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the penalty to be applied. diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 58f0fcc1..2ad1a322 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -11,7 +11,7 @@ from primaite.interface.request import RequestResponse class TestWebServer404PenaltySticky: def test_non_sticky(self): - reward = WebServer404Penalty("computer", "WebService", sticky=False) + reward = WebServer404Penalty(node_hostname="computer", service_name="WebService", sticky=False) # no response codes yet, reward is 0 codes = [] @@ -38,7 +38,7 @@ class TestWebServer404PenaltySticky: assert reward.calculate(state, last_action_response) == -1.0 def test_sticky(self): - reward = WebServer404Penalty("computer", "WebService", sticky=True) + reward = WebServer404Penalty(node_hostname="computer", service_name="WebService", sticky=True) # no response codes yet, reward is 0 codes = [] @@ -67,7 +67,7 @@ class TestWebServer404PenaltySticky: class TestWebpageUnavailabilitySticky: def test_non_sticky(self): - reward = WebpageUnavailablePenalty("computer", sticky=False) + reward = WebpageUnavailablePenalty(node_hostname="computer", sticky=False) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] @@ -127,7 +127,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == -1.0 def test_sticky(self): - reward = WebpageUnavailablePenalty("computer", sticky=True) + reward = WebpageUnavailablePenalty(node_hostname="computer", sticky=True) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] @@ -188,7 +188,7 @@ class TestWebpageUnavailabilitySticky: class TestGreenAdminDatabaseUnreachableSticky: def test_non_sticky(self): - reward = GreenAdminDatabaseUnreachablePenalty("computer", sticky=False) + reward = GreenAdminDatabaseUnreachablePenalty(node_hostname="computer", sticky=False) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] @@ -244,7 +244,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == -1.0 def test_sticky(self): - reward = GreenAdminDatabaseUnreachablePenalty("computer", sticky=True) + reward = GreenAdminDatabaseUnreachablePenalty(node_hostname="computer", sticky=True) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] From 318f8926f0f7f0c85f3ce07bf551ca5ada88bbf9 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 22 Oct 2024 12:14:30 +0100 Subject: [PATCH 036/224] #2913: Fix remaining test errors. --- src/primaite/game/agent/rewards.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 1f870e83..2386bed5 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -272,7 +272,6 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe sticky: bool = True reward: float = 0.0 location_in_state: List[str] = [""] - _node: str = node_hostname class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebpageUnavailablePenalty.""" @@ -311,7 +310,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe request_attempted = last_action_response.request == [ "network", "node", - self._node, + self.node_hostname, "application", "WebBrowser", "execute", @@ -326,7 +325,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe elif web_browser_state is NOT_PRESENT_IN_STATE or not web_browser_state["history"]: _LOGGER.debug( "Web browser reward could not be calculated because the web browser history on node", - f"{self._node} was not reported in the simulation state. Returning 0.0", + f"{self.node_hostname} was not reported in the simulation state. Returning 0.0", ) self.reward = 0.0 else: @@ -357,7 +356,6 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi """Penalises the agent when the green db clients fail to connect to the database.""" node_hostname: str = "" - _node: str = node_hostname sticky: bool = True reward: float = 0.0 @@ -385,7 +383,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi request_attempted = last_action_response.request == [ "network", "node", - self._node, + self.node_hostname, "application", "DatabaseClient", "execute", From 37bdbaf0d1ebb624b0b279d6a2ba8ba7cfd142f7 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 22 Oct 2024 16:15:04 +0100 Subject: [PATCH 037/224] #2913: Fix JSON breakage and old-style PORTS and PROTOCOL usage. --- .../Command-&-Control-E2E-Demonstration.ipynb | 10 +++--- .../create-simulation_demo.ipynb | 31 ++++++++++--------- .../network_simulator_demo.ipynb | 14 ++++----- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 6e6819fa..368dccf8 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -1781,9 +1781,11 @@ "outputs": [], "source": [ "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", - "from primaite.simulator.network.transmission.transport_layer import Port\n", + "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", + "from primaite.utils.validation.port import PORT_LOOKUP\n", + "\n", "# As we're configuring via the PrimAITE API we need to pass the actual IPProtocol/Port (Agents leverage the simulation via the game layer and thus can pass strings).\n", - "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol["UDP"], masquerade_port=Port["DNS"])\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=PROTOCOL_LOOKUP[\"UDP\"], masquerade_port=PORT_LOOKUP[\"DNS\"])\n", "c2_beacon.establish()\n", "c2_beacon.show()" ] @@ -1804,7 +1806,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -1818,7 +1820,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb index f573f251..7ce8baaf 100644 --- a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb +++ b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -56,7 +56,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -121,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -132,7 +132,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -159,16 +159,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "from primaite.simulator.system.applications.application import Application, ApplicationOperatingState\n", "from primaite.simulator.system.software import SoftwareHealthState, SoftwareCriticality\n", - "from primaite.simulator.network.transmission.transport_layer import Port\n", - "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", "from primaite.simulator.file_system.file_system import FileSystem\n", + "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", + "from primaite.utils.validation.port import PORT_LOOKUP\n", + "\n", "\n", "# no applications exist yet so we will create our own.\n", "class MSPaint(Application, identifier=\"MSPaint\"):\n", @@ -178,16 +179,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ - "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=Port["HTTP"], protocol = IPProtocol["NONE"],operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" + "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=PORT_LOOKUP[\"HTTP\"], protocol = PROTOCOL_LOOKUP[\"NONE\"],operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -203,7 +204,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -212,7 +213,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -249,7 +250,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb index 2d5b4772..b09baa85 100644 --- a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb +++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "3", "metadata": { "tags": [] @@ -75,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "4", "metadata": { "tags": [] @@ -532,12 +532,12 @@ }, "outputs": [], "source": [ - "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", - "from primaite.simulator.network.transmission.transport_layer import Port\n", "from primaite.simulator.network.hardware.nodes.network.router import ACLAction\n", + "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", + "\n", "network.get_node_by_hostname(\"router_1\").acl.add_rule(\n", " action=ACLAction.DENY,\n", - " protocol=IPProtocol["ICMP"],\n", + " protocol=PROTOCOL_LOOKUP[\"ICMP\"],\n", " src_ip_address=\"192.168.10.22\",\n", " position=1\n", ")" @@ -650,7 +650,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -664,7 +664,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, From c3f266e40116514cae6d842351524b42927e43d9 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 22 Oct 2024 16:26:57 +0100 Subject: [PATCH 038/224] #2913: Remove unneeded import and pre-commit changes. --- .../Command-&-Control-E2E-Demonstration.ipynb | 1 - .../create-simulation_demo.ipynb | 22 +++++++++---------- .../network_simulator_demo.ipynb | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 368dccf8..d2972fa9 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -1780,7 +1780,6 @@ "metadata": {}, "outputs": [], "source": [ - "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", "from primaite.utils.validation.port import PORT_LOOKUP\n", "\n", diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb index 7ce8baaf..117ea019 100644 --- a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb +++ b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -56,7 +56,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -85,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -121,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -132,7 +132,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -159,7 +159,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -188,7 +188,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -204,7 +204,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -213,7 +213,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb index b09baa85..4b620eee 100644 --- a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb +++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "3", "metadata": { "tags": [] @@ -75,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "4", "metadata": { "tags": [] From 85216bec942a472f9007dba41e1be2a2c0846456 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 22 Oct 2024 16:48:30 +0100 Subject: [PATCH 039/224] #2913: Rename notebook to replace '&'. --- ...stration.ipynb => Command-and-Control-E2E-Demonstration.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/primaite/notebooks/{Command-&-Control-E2E-Demonstration.ipynb => Command-and-Control-E2E-Demonstration.ipynb} (100%) diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb similarity index 100% rename from src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb rename to src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb From 518b934e09086d7dcc1c9ebc7e0b763dbbf85b5e Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 22 Oct 2024 17:02:54 +0100 Subject: [PATCH 040/224] #2912 - Corrections to some actions & fixing some linting. TODO: Action Manager errors --- src/primaite/game/agent/actions/acl.py | 38 ++++---- src/primaite/game/agent/actions/config.py | 2 +- src/primaite/game/agent/actions/manager.py | 14 +-- src/primaite/game/agent/actions/node.py | 97 ++++++++++++++++++- src/primaite/game/agent/actions/service.py | 7 +- src/primaite/game/agent/actions/session.py | 7 +- src/primaite/notebooks/Action-masking.ipynb | 7 +- .../Command-&-Control-E2E-Demonstration.ipynb | 4 +- .../notebooks/Training-an-SB3-Agent.ipynb | 4 +- .../create-simulation_demo.ipynb | 6 +- .../network_simulator_demo.ipynb | 4 +- 11 files changed, 149 insertions(+), 41 deletions(-) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 1048dc1e..cc89bfba 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -3,10 +3,16 @@ from typing import Dict, List, Literal from pydantic import BaseModel, Field, field_validator, ValidationInfo -from primaite.game.agent.actions.manager import AbstractAction +from primaite.game.agent.actions.manager import AbstractAction, ActionManager from primaite.interface.request import RequestFormat -__all__ = ("RouterACLAddRuleAction", "RouterACLRemoveRuleAction", "FirewallACLAddRuleAction", "FirewallACLRemoveRuleAction") +__all__ = ( + "RouterACLAddRuleAction", + "RouterACLRemoveRuleAction", + "FirewallACLAddRuleAction", + "FirewallACLRemoveRuleAction", +) + class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" @@ -23,8 +29,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): dest_ip_id: int dest_wildcard_id: int dest_port_id: int - protocol_id: int - + protocol_name: str class ACLRuleOptions(BaseModel): """Validator for ACL_ADD_RULE options.""" @@ -68,10 +73,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): return v @classmethod - def form_request( - cls, - config: ConfigSchema - ) -> List[str]: + def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" # Validate incoming data. parsed_options = RouterACLAddRuleAction.ACLRuleOptions( @@ -84,7 +86,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): dest_wildcard_id=config.dest_wildcard_id, source_port_id=config.source_port_id, dest_port_id=config.dest_port_id, - protocol_id=config.protocol_id, + protocol=config.protocol_name, ) if parsed_options.permission == 1: permission_str = "PERMIT" @@ -96,40 +98,40 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): if parsed_options.protocol_id == 1: protocol = "ALL" else: - protocol = self.manager.get_internet_protocol_by_idx(parsed_options.protocol_id - 2) + protocol = cls.manager.get_internet_protocol_by_idx(parsed_options.protocol_id - 2) # subtract 2 to account for UNUSED=0 and ALL=1. if parsed_options.source_ip_id == 1: src_ip = "ALL" else: - src_ip = self.manager.get_ip_address_by_idx(parsed_options.source_ip_id - 2) + src_ip = cls.manager.get_ip_address_by_idx(parsed_options.source_ip_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 - src_wildcard = self.manager.get_wildcard_by_idx(parsed_options.source_wildcard_id) + src_wildcard = cls.manager.get_wildcard_by_idx(parsed_options.source_wildcard_id) if parsed_options.source_port_id == 1: src_port = "ALL" else: - src_port = self.manager.get_port_by_idx(parsed_options.source_port_id - 2) + src_port = cls.manager.get_port_by_idx(parsed_options.source_port_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 if parsed_options.dest_ip_id == 1: dst_ip = "ALL" else: - dst_ip = self.manager.get_ip_address_by_idx(parsed_options.dest_ip_id - 2) + dst_ip = cls.manager.get_ip_address_by_idx(parsed_options.dest_ip_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 - dst_wildcard = self.manager.get_wildcard_by_idx(parsed_options.dest_wildcard_id) + dst_wildcard = cls.manager.get_wildcard_by_idx(parsed_options.dest_wildcard_id) if parsed_options.dest_port_id == 1: dst_port = "ALL" else: - dst_port = self.manager.get_port_by_idx(parsed_options.dest_port_id - 2) + dst_port = cls.manager.get_port_by_idx(parsed_options.dest_port_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 return [ "network", "node", - target_router, + config.target_router, "acl", "add_rule", permission_str, @@ -140,7 +142,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): str(dst_ip), dst_wildcard, dst_port, - position, + config.position, ] diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index e92d443b..582e8ec7 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo -from primaite.game.agent.actions.manager import AbstractAction +from primaite.game.agent.actions.manager import AbstractAction, ActionManager from primaite.interface.request import RequestFormat __all__ = ( diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 2f47ea7c..7677b39a 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -87,6 +87,8 @@ class ActionManager: # ip_list: List[str] = [], # to allow us to map an index to an ip address. # wildcard_list: List[str] = [], # to allow mapping from wildcard index to act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions + *args, + **kwargs, ) -> None: """Init method for ActionManager. @@ -116,27 +118,27 @@ class ActionManager: :param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions. :type act_map: Optional[Dict[int, Dict]] """ - self.node_names: List[str] = [n["node_name"] for n in nodes] + # self.node_names: List[str] = [n["node_name"] for n in nodes] """List of node names in this action space. The list order is the mapping between node index and node name.""" - self.application_names: List[List[str]] = [] + # self.application_names: List[List[str]] = [] """ List of applications per node. The list order gives the two-index mapping between (node_id, app_id) to app name. The first index corresponds to node id, the second index is the app id on that particular node. For instance, self.application_names[0][2] is the name of the third application on the first node. """ - self.service_names: List[List[str]] = [] + # self.service_names: List[List[str]] = [] """ List of services per node. The list order gives the two-index mapping between (node_id, svc_id) to svc name. The first index corresponds to node id, the second index is the service id on that particular node. For instance, self.service_names[0][2] is the name of the third service on the first node. """ - self.folder_names: List[List[str]] = [] + # self.folder_names: List[List[str]] = [] """ List of folders per node. The list order gives the two-index mapping between (node_id, folder_id) to folder name. The first index corresponds to node id, the second index is the folder id on that particular node. For instance, self.folder_names[0][2] is the name of the third folder on the first node. """ - self.file_names: List[List[List[str]]] = [] + # self.file_names: List[List[List[str]]] = [] """ List of files per folder per node. The list order gives the three-index mapping between (node_id, folder_id, file_id) to file name. The first index corresponds to node id, the second index is the @@ -203,7 +205,7 @@ class ActionManager: # and `options` is an optional dict of options to pass to the init method of the action class act_type = act_spec.get("type") act_options = act_spec.get("options", {}) - self.actions[act_type] = self.act_class_identifiers[act_type](self, **global_action_args, **act_options) + # self.actions[act_type] = self.act_class_identifiers[act_type](self, **global_action_args, **act_options) self.action_map: Dict[int, Tuple[str, Dict]] = {} """ diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 011ff4dc..f95ba6df 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,5 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import ClassVar +from abc import abstractmethod +from typing import ClassVar, List, Optional, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -61,3 +62,97 @@ class NodeResetAction(NodeAbstractAction, identifier="node_reset"): """Configuration schema for NodeResetAction.""" verb: str = "reset" + + +class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_action"): + """Base class for NodeNMAP actions.""" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Base Configuration Schema for NodeNMAP actions.""" + + target_ip_address: Union[str, List[str]] + show: bool = False + node_name: str + + @classmethod + @abstractmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + pass + + +class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_scan"): + class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): + pass + + @classmethod + def form_request(cls, config: ConfigSchema) -> List[str]: # noqa + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [ + "network", + "node", + config.node_name, + "application", + "NMAP", + "ping_scan", + {"target_ip_address": config.target_ip_address, "show": config.show}, + ] + + +class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_scan"): + """Action which performs an NMAP port scan.""" + + class ConfigSchema(AbstractAction.ConfigSchema): + target_protocol: Optional[Union[str, List[str]]] = (None,) + target_port: Optional[Union[str, List[str]]] = (None,) + show: Optional[bool] = (False,) + + @classmethod + def form_request( + cls, + config: ConfigSchema, + ) -> List[str]: # noqa + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [ + "network", + "node", + config.source_node, + "application", + "NMAP", + "port_scan", + { + "target_ip_address": config.target_ip_address, + "target_port": config.target_port, + "target_protocol": config.target_protocol, + "show": config.show, + }, + ] + + +class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, identifier="node_network_service_recon"): + """Action which performs an NMAP network service recon (ping scan followed by port scan).""" + + class ConfigSchema(AbstractAction.ConfigSchema): + target_protocol: Optional[Union[str, List[str]]] = (None,) + target_port: Optional[Union[str, List[str]]] = (None,) + show: Optional[bool] = (False,) + + @classmethod + def form_request( + cls, + config: ConfigSchema, + ) -> List[str]: # noqa + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [ + "network", + "node", + config.source_node, + "application", + "NMAP", + "network_service_recon", + { + "target_ip_address": config.target_ip_address, + "target_port": config.target_port, + "target_protocol": config.target_protocol, + "show": config.show, + }, + ] diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index cf277b5d..bccfaba2 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -18,6 +18,11 @@ __all__ = ( class NodeServiceAbstractAction(AbstractAction, identifier="node_service_abstract"): + """Abstract Action for Node Service related actions. + + Any actions which use node_name and service_name can inherit from this class. + """ + class ConfigSchema(AbstractAction.ConfigSchema): node_name: str service_name: str @@ -34,7 +39,7 @@ class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_ """Action which scans a service.""" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): - """Configuration Schema for NodeServiceScanAction""" + """Configuration Schema for NodeServiceScanAction.""" verb: str = "scan" diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index eb035ff3..f77a85b1 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -19,8 +19,11 @@ class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstrac @classmethod @abstractmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: - """Abstract method. Should return the action formatted as a request which - can be ingested by the PrimAITE simulation.""" + """ + Abstract method for request forming. + + Should return the action formatted as a request which can be ingested by the PrimAITE simulation. + """ pass diff --git a/src/primaite/notebooks/Action-masking.ipynb b/src/primaite/notebooks/Action-masking.ipynb index ba70f2b4..d22e171d 100644 --- a/src/primaite/notebooks/Action-masking.ipynb +++ b/src/primaite/notebooks/Action-masking.ipynb @@ -19,7 +19,8 @@ "source": [ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.config.load import data_manipulation_config_path\n", - "from prettytable import PrettyTable\n" + "from prettytable import PrettyTable\n", + "UDP=\"UDP\"" ] }, { @@ -195,7 +196,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -209,7 +210,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 6e6819fa..a697ca3e 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -1783,7 +1783,7 @@ "from primaite.simulator.network.transmission.network_layer import IPProtocol\n", "from primaite.simulator.network.transmission.transport_layer import Port\n", "# As we're configuring via the PrimAITE API we need to pass the actual IPProtocol/Port (Agents leverage the simulation via the game layer and thus can pass strings).\n", - "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol["UDP"], masquerade_port=Port["DNS"])\n", + "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", masquerade_protocol=IPProtocol[\"UDP\"], masquerade_port=Port[\"DNS\"])\n", "c2_beacon.establish()\n", "c2_beacon.show()" ] @@ -1804,7 +1804,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index 892736fe..5255b0ad 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -168,7 +168,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -182,7 +182,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb index f573f251..30417b84 100644 --- a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb +++ b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb @@ -182,7 +182,7 @@ "metadata": {}, "outputs": [], "source": [ - "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=Port["HTTP"], protocol = IPProtocol["NONE"],operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" + "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=Port[\"HTTP\"], protocol = IPProtocol[\"NONE\"],operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" ] }, { @@ -249,7 +249,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -263,7 +263,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb index 2d5b4772..8406dbdf 100644 --- a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb +++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb @@ -537,7 +537,7 @@ "from primaite.simulator.network.hardware.nodes.network.router import ACLAction\n", "network.get_node_by_hostname(\"router_1\").acl.add_rule(\n", " action=ACLAction.DENY,\n", - " protocol=IPProtocol["ICMP"],\n", + " protocol=IPProtocol[\"ICMP\"],\n", " src_ip_address=\"192.168.10.22\",\n", " position=1\n", ")" @@ -650,7 +650,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, From 5cd629a82163c486e9a9c33ec503265c11adfc28 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 23 Oct 2024 18:45:57 +0100 Subject: [PATCH 041/224] #2912 - Fixed actionmanager issue and moved abstractaction to solve import error --- .../scenario_with_placeholders/scenario.yaml | 44 ++++++++--------- src/primaite/game/agent/actions/__init__.py | 2 + src/primaite/game/agent/actions/abstract.py | 48 ++++++++++++++++++ .../game/agent/actions/application.py | 2 +- src/primaite/game/agent/actions/manager.py | 49 ++++--------------- src/primaite/notebooks/Action-masking.ipynb | 3 +- 6 files changed, 83 insertions(+), 65 deletions(-) create mode 100644 src/primaite/game/agent/actions/abstract.py diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index dfd200f3..8c83bf79 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -55,50 +55,50 @@ agents: action_space: action_list: - - type: DONOTHING - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE + - type: do_nothing + - type: node_shutdown + - type: node_startup + - type: host_nic_enable + - type: host_nic_enable action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_SHUTDOWN + action: node_shutdown options: - node_id: 0 + node_name: client_1 2: - action: NODE_SHUTDOWN + action: node_shutdown options: - node_id: 1 + node_name: server 3: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: client_1 4: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: server 5: - action: HOST_NIC_DISABLE + action: host_nic_disable options: - node_id: 0 + node_name: client_1 nic_id: 0 6: - action: HOST_NIC_DISABLE + action: host_nic_disable options: - node_id: 1 + node_name: server nic_id: 0 7: - action: HOST_NIC_ENABLE + action: host_nic_enable options: - node_id: 0 + node_name: client_1 nic_id: 0 8: - action: HOST_NIC_ENABLE + action: host_nic_enable options: - node_id: 1 + node_name: server nic_id: 0 options: nodes: diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 428c6c58..3540b128 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -1,6 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ( + abstract, acl, application, config, @@ -17,6 +18,7 @@ from primaite.game.agent.actions.manager import ActionManager __all__ = ( "acl", + "abstract", "application", "config", "file", diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py new file mode 100644 index 00000000..5250e532 --- /dev/null +++ b/src/primaite/game/agent/actions/abstract.py @@ -0,0 +1,48 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations +from abc import ABC +from typing import Any, ClassVar, Dict, Type + +from pydantic import BaseModel, ConfigDict + +from primaite.interface.request import RequestFormat + +class AbstractAction(BaseModel): + """Base class for actions.""" + + # notes: + # we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. + # all the init methods in the old actions are just used for holding a verb and shape, which are not really used. + # the config schema should be used to the actual parameters for formatting the action itself. + # (therefore there's no need for creating action instances, just the action class contains logic for converting + # CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be + # instantiated.) + class ConfigSchema(BaseModel, ABC): + """Base configuration schema for Actions.""" + + model_config = ConfigDict(extra="forbid") + type: str + + _registry: ClassVar[Dict[str, Type[AbstractAction]]] = {} + + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Cannot create new action under reserved name {identifier}") + cls._registry[identifier] = cls + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [] + + @classmethod + def from_config(cls, config: Dict) -> "AbstractAction": + """Create an action component from a config dictionary""" + + type_id = config.get("type") + + if type_id in cls._registry: + return cls(type=type_id, model_config=config) + else: + return [] \ No newline at end of file diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 3a254d57..e0496dc7 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar -from primaite.game.agent.actions.manager import AbstractAction +from primaite.game.agent.actions.abstract import AbstractAction from primaite.interface.request import RequestFormat __all__ = ( diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 7677b39a..98ffbd00 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -14,48 +14,17 @@ agents: from __future__ import annotations import itertools -from abc import ABC -from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type +from typing import Dict, List, Literal, Optional, Tuple from gymnasium import spaces -from pydantic import BaseModel, ConfigDict # from primaite.game.game import PrimaiteGame # TODO: Breaks things +from primaite.game.agent.actions.abstract import AbstractAction from primaite.interface.request import RequestFormat # TODO: Make sure that actions are backwards compatible where the old YAML format is used. -class AbstractAction(BaseModel): - """Base class for actions.""" - - # notes: - # we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. - # all the init methods in the old actions are just used for holding a verb and shape, which are not really used. - # the config schema should be used to the actual parameters for formatting the action itself. - # (therefore there's no need for creating action instances, just the action class contains logic for converting - # CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be - # instantiated.) - class ConfigSchema(BaseModel, ABC): # TODO: not sure if this better named something like `Options` - """Base configuration schema for Actions.""" - - model_config = ConfigDict(extra="forbid") - type: str - - _registry: ClassVar[Dict[str, Type[AbstractAction]]] = {} - - def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: - super().__init_subclass__(**kwargs) - if identifier in cls._registry: - raise ValueError(f"Cannot create new action under reserved name {identifier}") - cls._registry[identifier] = cls - - @classmethod - def form_request(self, config: ConfigSchema) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [] - - class DoNothingAction(AbstractAction, identifier="do_nothing"): """Do Nothing Action.""" @@ -204,8 +173,8 @@ class ActionManager: # where `type` decides which AbstractAction subclass should be used # and `options` is an optional dict of options to pass to the init method of the action class act_type = act_spec.get("type") - act_options = act_spec.get("options", {}) - # self.actions[act_type] = self.act_class_identifiers[act_type](self, **global_action_args, **act_options) + # act_options = act_spec.get("options", {}) # Don't need this anymore I think? + self.actions[act_type] = AbstractAction._registry[act_type] self.action_map: Dict[int, Tuple[str, Dict]] = {} """ @@ -235,10 +204,10 @@ class ActionManager: :return: An action map maps consecutive integers to a combination of Action type and parameter choices. An example output could be: - {0: ("DONOTHING", {'dummy': 0}), - 1: ("NODE_OS_SCAN", {'node_id': 0}), - 2: ("NODE_OS_SCAN", {'node_id': 1}), - 3: ("NODE_FOLDER_SCAN", {'node_id:0, folder_id:0}), + {0: ("do_nothing", {'dummy': 0}), + 1: ("node_os_scan", {'node_name': computer}), + 2: ("node_os_scan", {'node_name': server}), + 3: ("node_folder_scan", {'node_name:computer, folder_name:downloads}), ... #etc... } :rtype: Dict[int, Tuple[AbstractAction, Dict]] @@ -269,7 +238,7 @@ class ActionManager: def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" act_obj = self.actions[action_identifier] - return act_obj.form_request(**action_options) + return act_obj.form_request(action_options) @property def space(self) -> spaces.Space: diff --git a/src/primaite/notebooks/Action-masking.ipynb b/src/primaite/notebooks/Action-masking.ipynb index d22e171d..858b4bb6 100644 --- a/src/primaite/notebooks/Action-masking.ipynb +++ b/src/primaite/notebooks/Action-masking.ipynb @@ -19,8 +19,7 @@ "source": [ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.config.load import data_manipulation_config_path\n", - "from prettytable import PrettyTable\n", - "UDP=\"UDP\"" + "from prettytable import PrettyTable" ] }, { From 6f6e4131b4cd39a108a93415cfdba20b1895b34a Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 29 Oct 2024 16:54:19 +0000 Subject: [PATCH 042/224] #2913: Handle case where server_ip_address is None --- src/primaite/simulator/system/applications/database_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index cd4b2a03..e030b306 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -308,6 +308,8 @@ class DatabaseClient(Application, identifier="DatabaseClient"): """ if not self._can_perform_action(): return None + if self.server_ip_address is None: + return None connection_request_id = str(uuid4()) self._client_connection_requests[connection_request_id] = None From 3c1bb2d546a051eca445b88d7e700a3dea3f1861 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 29 Oct 2024 16:57:11 +0000 Subject: [PATCH 043/224] #2913: Integration test fixes. --- src/primaite/simulator/system/core/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 75322e86..59390d68 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -16,7 +16,7 @@ from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP from primaite.utils.validation.port import Port, PORT_LOOKUP if TYPE_CHECKING: - from primaite.simulator.network.hardware.base import NetworkInterface + from primaite.simulator.network.hardware.base import NetworkInterface, Node from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog From 9fd862763b4dfac4f4015871d705cddd4d072eb5 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 30 Oct 2024 11:11:07 +0000 Subject: [PATCH 044/224] #2913: Ensure optional software in config file is enabled. --- src/primaite/game/game.py | 2 +- src/primaite/simulator/system/applications/database_client.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c8fbac4e..7c2f49e7 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -376,7 +376,7 @@ class PrimaiteGame: if service_class is not None: _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") - new_node.software_manager.install(service_class) + new_node.software_manager.install(service_class, **service_cfg.get('options', {})) new_service = new_node.software_manager.software[service_class.__name__] # fixing duration for the service diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index e030b306..2b2be7b2 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -309,6 +309,9 @@ class DatabaseClient(Application, identifier="DatabaseClient"): if not self._can_perform_action(): return None if self.server_ip_address is None: + self.sys_log.warning( + f"{self.name}: Database server IP address not provided." + ) return None connection_request_id = str(uuid4()) From 97094aba795bfdeb5141f6d0080636635c74a377 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 30 Oct 2024 11:15:39 +0000 Subject: [PATCH 045/224] #2913: Pre-commit changes. --- src/primaite/game/game.py | 2 +- src/primaite/simulator/system/applications/database_client.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 7c2f49e7..7c5c93bc 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -376,7 +376,7 @@ class PrimaiteGame: if service_class is not None: _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") - new_node.software_manager.install(service_class, **service_cfg.get('options', {})) + new_node.software_manager.install(service_class, **service_cfg.get("options", {})) new_service = new_node.software_manager.software[service_class.__name__] # fixing duration for the service diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 2b2be7b2..2079194a 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -309,9 +309,7 @@ class DatabaseClient(Application, identifier="DatabaseClient"): if not self._can_perform_action(): return None if self.server_ip_address is None: - self.sys_log.warning( - f"{self.name}: Database server IP address not provided." - ) + self.sys_log.warning(f"{self.name}: Database server IP address not provided.") return None connection_request_id = str(uuid4()) From 77219db0411f50ddf422dbb9eba8cfe0c5aee0e5 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 30 Oct 2024 16:32:49 +0000 Subject: [PATCH 046/224] #2913: Remove dns_server option from config files. --- tests/assets/configs/basic_switched_network.yaml | 2 -- tests/assets/configs/fix_duration_one_item.yaml | 6 ++---- tests/assets/configs/software_fix_duration.yaml | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index fed0f52d..799bb571 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -202,8 +202,6 @@ simulation: port_scan_p_of_success: 0.8 services: - type: DNSClient - options: - dns_server: 192.168.1.10 - type: DNSServer options: domain_mapping: diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index bd0fb61f..4163bcfd 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -200,8 +200,6 @@ simulation: port_scan_p_of_success: 0.8 services: - type: DNSClient - options: - dns_server: 192.168.1.10 - type: DNSServer options: domain_mapping: @@ -232,8 +230,8 @@ simulation: server_password: arcd services: - type: DNSClient - options: - dns_server: 192.168.1.10 + # options: + # dns_server: 192.168.1.10 links: - endpoint_a_hostname: switch_1 diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 1a28258b..2b72e85f 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -209,7 +209,7 @@ simulation: services: - type: DNSClient options: - dns_server: 192.168.1.10 + # dns_server: 192.168.1.10 fix_duration: 3 - type: DNSServer options: @@ -250,8 +250,6 @@ simulation: server_password: arcd services: - type: DNSClient - options: - dns_server: 192.168.1.10 links: - endpoint_a_hostname: switch_1 From 7d977c809538ce236d38b21aec1c97787abfb29a Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 30 Oct 2024 16:33:14 +0000 Subject: [PATCH 047/224] #2913: Fix config path for test. --- .../integration_tests/extensions/test_extendable_config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 5addcbd7..71a60194 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -5,6 +5,7 @@ from primaite.config.load import get_extended_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer +from tests import TEST_ASSETS_ROOT from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config from tests.integration_tests.extensions.applications.extended_application import ExtendedApplication from tests.integration_tests.extensions.nodes.giga_switch import GigaSwitch @@ -13,11 +14,12 @@ from tests.integration_tests.extensions.nodes.giga_switch import GigaSwitch from tests.integration_tests.extensions.nodes.super_computer import SuperComputer from tests.integration_tests.extensions.services.extended_service import ExtendedService +CONFIG_PATH = TEST_ASSETS_ROOT / "configs/extended_config.yaml" + def test_extended_example_config(): """Test that the example config can be parsed properly.""" - config_path = os.path.join("tests", "assets", "configs", "extended_config.yaml") - game = load_config(config_path) + game = load_config(CONFIG_PATH) network: Network = game.simulation.network assert len(network.nodes) == 10 # 10 nodes in example network From 844a3a60fa4f26d95baa0585d8112a566fde0208 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 30 Oct 2024 18:34:05 +0000 Subject: [PATCH 048/224] #2912 - Steps to get test_actions passing the refactored actions. Some linting changes and YAML updates. --- src/primaite/game/agent/actions/abstract.py | 22 +-- src/primaite/game/agent/actions/acl.py | 145 ++++++++++-------- .../game/agent/actions/application.py | 51 +++++- src/primaite/game/agent/actions/config.py | 22 +-- src/primaite/game/agent/actions/file.py | 86 ++++++++++- src/primaite/game/agent/actions/folder.py | 37 ++++- src/primaite/game/agent/actions/host_nic.py | 18 ++- src/primaite/game/agent/actions/manager.py | 120 +-------------- src/primaite/game/agent/actions/network.py | 13 +- src/primaite/game/agent/actions/node.py | 3 +- src/primaite/game/agent/actions/service.py | 20 ++- src/primaite/game/agent/actions/session.py | 35 ++++- .../configs/firewall_actions_network.yaml | 4 +- ...etwork_service_recon_red_agent_config.yaml | 5 +- .../nmap_ping_scan_red_agent_config.yaml | 7 +- .../nmap_port_scan_red_agent_config.yaml | 6 +- tests/conftest.py | 96 ++++++------ .../game_layer/test_actions.py | 86 +++++------ .../_game/_agent/test_probabilistic_agent.py | 24 +-- 19 files changed, 480 insertions(+), 320 deletions(-) diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 5250e532..b96b14c9 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -1,5 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations + from abc import ABC from typing import Any, ClassVar, Dict, Type @@ -7,6 +8,7 @@ from pydantic import BaseModel, ConfigDict from primaite.interface.request import RequestFormat + class AbstractAction(BaseModel): """Base class for actions.""" @@ -34,15 +36,17 @@ class AbstractAction(BaseModel): @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [] - + pass + @classmethod def from_config(cls, config: Dict) -> "AbstractAction": - """Create an action component from a config dictionary""" - - type_id = config.get("type") + """Create an action component from a config dictionary.""" + # set attributes for action based off config dict + # if config["type"] not in cls._registry: + # raise ValueError(f"Invalid action reward type {config['type']}") - if type_id in cls._registry: - return cls(type=type_id, model_config=config) - else: - return [] \ No newline at end of file + for attribute, value in config.items(): + if not hasattr(cls.ConfigSchema, attribute): + setattr(cls.ConfigSchema, attribute, value) + + return cls diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index cc89bfba..8f5a79da 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -14,6 +14,16 @@ __all__ = ( ) +class ACLAbstractAction(AbstractAction, identifier="acl_abstract_action"): + """Base class for ACL actions.""" + + + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration Schema base for ACL abstract actions.""" + + + + class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" @@ -26,9 +36,9 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): source_ip_id: int source_wildcard_id: int source_port_id: int - dest_ip_id: int - dest_wildcard_id: int - dest_port_id: int + dst_ip: str + dst_wildcard_id: int + dst_port: int protocol_name: str class ACLRuleOptions(BaseModel): @@ -46,13 +56,13 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Rule source IP wildcard. By default, use the wildcard at index 0 from action manager.""" source_port_id: int = Field(default=1, ge=1) """Rule source port. By default, all source ports.""" - dest_ip_id: int = Field(default=1, ge=1) + dst_ip_id: int = Field(default=1, ge=1) """Rule destination IP address. By default, all ip addresses.""" - dest_wildcard_id: int = Field(default=0, ge=0) + dst_wildcard_id: int = Field(default=0, ge=0) """Rule destination IP wildcard. By default, use the wildcard at index 0 from action manager.""" - dest_port_id: int = Field(default=1, ge=1) + dst_port_id: int = Field(default=1, ge=1) """Rule destination port. By default, all destination ports.""" - protocol_id: int = Field(default=1, ge=1) + protocol_name: str = "ALL" """Rule protocol. By default, all protocols.""" @field_validator( @@ -62,7 +72,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): "dest_ip_id", "dest_port_id", "dest_wildcard_id", - "protocol_id", + "protocol_name", mode="before", ) @classmethod @@ -82,10 +92,10 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): permission=config.permission, source_ip_id=config.source_ip_id, source_wildcard_id=config.source_wildcard_id, - dest_ip_id=config.dest_ip_id, + dest_ip_id=config.dst_ip, dest_wildcard_id=config.dest_wildcard_id, source_port_id=config.source_port_id, - dest_port_id=config.dest_port_id, + dest_port_id=config.dst_port_id, protocol=config.protocol_name, ) if parsed_options.permission == 1: @@ -95,10 +105,10 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): # else: # _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") - if parsed_options.protocol_id == 1: + if parsed_options.protocol_name == "ALL": protocol = "ALL" else: - protocol = cls.manager.get_internet_protocol_by_idx(parsed_options.protocol_id - 2) + protocol = parsed_options.protocol_name # subtract 2 to account for UNUSED=0 and ALL=1. if parsed_options.source_ip_id == 1: @@ -120,7 +130,9 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): else: dst_ip = cls.manager.get_ip_address_by_idx(parsed_options.dest_ip_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 - dst_wildcard = cls.manager.get_wildcard_by_idx(parsed_options.dest_wildcard_id) + dst_ip=config.dst_ip + + dst_wildcard = config.dest_wildcard_id if parsed_options.dest_port_id == 1: dst_port = "ALL" @@ -134,14 +146,14 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): config.target_router, "acl", "add_rule", - permission_str, + config.permission_str, protocol, str(src_ip), - src_wildcard, - src_port, - str(dst_ip), - dst_wildcard, - dst_port, + config.src_wildcard, + config.src_port, + str(config.dst_ip), + config.dst_wildcard, + config.dst_port, config.position, ] @@ -161,9 +173,27 @@ class RouterACLRemoveRuleAction(AbstractAction, identifier="router_acl_remove_ru return ["network", "node", config.target_router, "acl", "remove_rule", config.position] -class FirewallACLAddRuleAction(AbstractAction, identifier="firewall_acl_add_rule"): +class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_rule"): """Action which adds a rule to a firewall port's ACL.""" + max_acl_rules: int + num_ips: int + num_ports: int + num_protocols: int + num_permissions: int = 3 + permission: str + + class ConfigSchema(ACLAbstractAction.ConfigSchema): + """Configuration schema for FirewallACLAddRuleAction.""" + + max_acl_rules: int + num_ips: int + num_ports: int + num_protocols: int + num_permissions: int = 3 + permission: str + + def __init__( self, manager: "ActionManager", @@ -198,92 +228,85 @@ class FirewallACLAddRuleAction(AbstractAction, identifier="firewall_acl_add_rule "protocol_id": num_protocols, } - def form_request( - self, - target_firewall_nodename: str, - firewall_port_name: str, - firewall_port_direction: str, - position: int, - permission: int, - source_ip_id: int, - source_wildcard_id: int, - dest_ip_id: int, - dest_wildcard_id: int, - source_port_id: int, - dest_port_id: int, - protocol_id: int, - ) -> List[str]: + + + @classmethod + def form_request(cls, config:ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if permission == 0: + if config.permission == 0: permission_str = "UNUSED" return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - elif permission == 1: + elif config.permission == 1: permission_str = "PERMIT" - elif permission == 2: + elif config.permission == 2: permission_str = "DENY" # else: # _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") - if protocol_id == 0: + if config.protocol_id == 0: return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - if protocol_id == 1: + if config.protocol_id == 1: protocol = "ALL" else: - protocol = self.manager.get_internet_protocol_by_idx(protocol_id - 2) + # protocol = self.manager.get_internet_protocol_by_idx(protocol_id - 2) # subtract 2 to account for UNUSED=0 and ALL=1. + pass - if source_ip_id == 0: + if config.source_ip_id == 0: return ["do_nothing"] # invalid formulation - elif source_ip_id == 1: + elif config.source_ip_id == 1: src_ip = "ALL" else: - src_ip = self.manager.get_ip_address_by_idx(source_ip_id - 2) + # src_ip = self.manager.get_ip_address_by_idx(source_ip_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 - if source_port_id == 0: + if config.source_port_id == 0: return ["do_nothing"] # invalid formulation - elif source_port_id == 1: + elif config.source_port_id == 1: src_port = "ALL" else: - src_port = self.manager.get_port_by_idx(source_port_id - 2) + # src_port = self.manager.get_port_by_idx(source_port_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 + pass - if dest_ip_id == 0: + if config.dest_ip_id == 0: return ["do_nothing"] # invalid formulation - elif dest_ip_id == 1: + elif config.dest_ip_id == 1: dst_ip = "ALL" else: - dst_ip = self.manager.get_ip_address_by_idx(dest_ip_id - 2) + # dst_ip = self.manager.get_ip_address_by_idx(dest_ip_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 + pass - if dest_port_id == 0: + if config.dest_port_id == 0: return ["do_nothing"] # invalid formulation - elif dest_port_id == 1: + elif config.dest_port_id == 1: dst_port = "ALL" else: - dst_port = self.manager.get_port_by_idx(dest_port_id - 2) + # dst_port = self.manager.get_port_by_idx(dest_port_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 - src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) - dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) + # src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) + # dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) + pass return [ "network", "node", - target_firewall_nodename, - firewall_port_name, - firewall_port_direction, + config.target_firewall_nodename, + config.firewall_port_name, + config.firewall_port_direction, "acl", "add_rule", permission_str, protocol, str(src_ip), - src_wildcard, + config.src_wildcard, src_port, str(dst_ip), - dst_wildcard, + config.dst_wildcard, dst_port, - position, + config.position, ] diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index e0496dc7..942ebe90 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -35,12 +35,21 @@ class NodeApplicationAbstractAction(AbstractAction, identifier="node_application """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.application_name is None: return ["do_nothing"] - return ["network", "node", config.node_name, "application", config.application_name, cls.verb] + return [ + "network", + "node", + config.node_name, + "application", + config.application_name, + cls.model_fields["verb"].default, + ] class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="node_application_execute"): """Action which executes an application.""" + verb: str = "execute" + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationExecuteAction.""" @@ -50,6 +59,8 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="no class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_application_scan"): """Action which scans an application.""" + verb: str = "scan" + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationScanAction.""" @@ -59,6 +70,8 @@ class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_ class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node_application_close"): """Action which closes an application.""" + verb: str = "close" + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationCloseAction.""" @@ -68,6 +81,8 @@ class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_application_fix"): """Action which fixes an application.""" + verb: str = "fix" + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationFixAction.""" @@ -77,18 +92,50 @@ class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_a class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="node_application_install"): """Action which installs an application.""" + verb: str = "install" + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationInstallAction.""" verb: str = "install" - # TODO: Either changes to application form_request bits, or add that here. + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "software_manager", + "application", + cls.model_fields["verb"].default, + config.application_name, + ] class NodeApplicationRemoveAction(NodeApplicationAbstractAction, identifier="node_application_remove"): """Action which removes/uninstalls an application.""" + verb: str = "uninstall" + class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationRemoveAction.""" verb: str = "uninstall" + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "software_manager", + "application", + cls.model_fields["verb"].default, + config.application_name, + ] diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 582e8ec7..beda8f27 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -18,7 +18,7 @@ __all__ = ( ) -class ConfigureRansomwareScriptAction(AbstractAction, identifier="configure_ransomware"): +class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_ransomware_configure"): """Action which sets config parameters for a ransomware script on a node.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -66,7 +66,7 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): return ["network", "node", node_name, "application", "DoSBot", "configure", config] -class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2"): +class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): """Action which configures a C2 Beacon based on the parameters given.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -105,14 +105,14 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2"): """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] - config = ConfigureC2BeaconAction._Opts( - c2_server_ip_address=config["c2_server_ip_address"], - keep_alive_frequency=config["keep_alive_frequency"], - masquerade_port=config["masquerade_port"], - masquerade_protocol=config["masquerade_protocol"], + configuration = ConfigureC2BeaconAction._Opts( + c2_server_ip_address=config.c2_server_ip_address, + keep_alive_frequency=config.keep_alive_frequency, + masquerade_port=config.masquerade_port, + masquerade_protocol=config.masquerade_protocol, ) - ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema + ConfigureC2BeaconAction._Opts.model_validate(configuration) # check that options adhere to schema return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] @@ -142,7 +142,7 @@ class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_c ] -class TerminalC2ServerAction(AbstractAction, identifier="terminal_c2_server"): +class TerminalC2ServerAction(AbstractAction, identifier="c2_server_terminal_command"): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" class _Opts(BaseModel): @@ -173,7 +173,7 @@ class TerminalC2ServerAction(AbstractAction, identifier="terminal_c2_server"): return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] -class RansomwareLaunchC2ServerAction(AbstractAction, identifier="ransomware_launch"): +class RansomwareLaunchC2ServerAction(AbstractAction, identifier="c2_server_ransomware_launch"): """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -190,7 +190,7 @@ class RansomwareLaunchC2ServerAction(AbstractAction, identifier="ransomware_laun return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] -class ExfiltrationC2ServerAction(AbstractAction, identifier="exfiltration_c2_server"): +class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfiltrate"): """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" class _Opts(BaseModel): diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index 6935a11c..6d1f95b9 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -44,22 +44,45 @@ class NodeFileAbstractAction(AbstractAction, identifier="node_file_abstract_acti config.folder_name, "file", config.file_name, - cls.verb, + cls.model_fields["verb"].default, ] class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create"): """Action which creates a new file in a given folder.""" + verb: str = "create" + force: bool = False + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCreateAction.""" verb: str = "create" + force: bool = False + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.folder_name is None or config.file_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "file_system", + cls.model_fields["verb"].default, + "file", + config.folder_name, + config.file_name, + cls.model_fields["force"].default, + ] class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): """Action which scans a file.""" + verb: str = "scan" + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileScanAction.""" @@ -69,15 +92,35 @@ class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete"): """Action which deletes a file.""" + verb: str = "delete" + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileDeleteAction.""" verb: str = "delete" + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.folder_name is None or config.file_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "file_system", + cls.model_fields["verb"].default, + "file", + config.folder_name, + config.file_name, + ] + class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restore"): """Action which restores a file.""" + verb: str = "restore" + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileRestoreAction.""" @@ -87,6 +130,8 @@ class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restor class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrupt"): """Action which corrupts a file.""" + verb: str = "corrupt" + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCorruptAction.""" @@ -96,7 +141,46 @@ class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrup class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access"): """Action which increases a file's access count.""" + verb: str = "access" + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileAccessAction.""" verb: str = "access" + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.folder_name is None or config.file_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "file_system", + cls.model_fields["verb"].default, + config.folder_name, + config.file_name, + ] + + +class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_checkhash"): + """Action which checks the hash of a file.""" + + verb: str = "checkhash" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration schema for NodeFileCheckhashAction.""" + + verb: str = "checkhash" + + +class NodeFileRepairAction(NodeFileAbstractAction, identifier="node_file_repair"): + """Action which repairs a file""" + + verb: str = "repair" + + class ConfigSchema(NodeFileAbstractAction.ConfigSchema): + """Configuration Schema for NodeFileRepairAction.""" + + verb: str = "repair" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index 74820eb0..e430efb7 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -34,12 +34,22 @@ class NodeFolderAbstractAction(AbstractAction, identifier="node_folder_abstract" """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None: return ["do_nothing"] - return ["network", "node", config.node_name, "file_system", "folder", config.folder_name, cls.verb] + return [ + "network", + "node", + config.node_name, + "file_system", + "folder", + config.folder_name, + cls.model_fields["verb"].default, + ] class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_scan"): """Action which scans a folder.""" + verb: str = "scan" + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderScanAction.""" @@ -49,6 +59,8 @@ class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_sca class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folder_checkhash"): """Action which checks the hash of a folder.""" + verb: str = "checkhash" + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderCheckhashAction.""" @@ -58,6 +70,8 @@ class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folde class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_repair"): """Action which repairs a folder.""" + verb: str = "repair" + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderRepairAction.""" @@ -67,16 +81,35 @@ class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_r class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_restore"): """Action which restores a folder.""" + verb: str = "restore" + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderRestoreAction.""" verb: str = "restore" -class NodeFolderCreateAction(AbstractAction, identifier="node_folder_create"): +class NodeFolderCreateAction(NodeFolderAbstractAction, identifier="node_folder_create"): """Action which creates a new folder.""" + verb: str = "create" + class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderCreateAction.""" verb: str = "create" + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.folder_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "file_system", + cls.model_fields["verb"].default, + "folder", + config.folder_name, + ] diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 4f66f9b9..1ad2e52f 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -13,11 +13,12 @@ class HostNICAbstractAction(AbstractAction, identifier="host_nic_abstract"): class. """ + node_name: str + nic_num: str + class ConfigSchema(AbstractAction.ConfigSchema): """Base Configuration schema for HostNIC actions.""" - num_nodes: str - max_nics_per_node: str node_name: str nic_num: str @@ -26,12 +27,21 @@ class HostNICAbstractAction(AbstractAction, identifier="host_nic_abstract"): """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.nic_num is None: return ["do_nothing"] - return ["network", "node", config.node_name, "network_interface", config.nic_num, cls.verb] + return [ + "network", + "node", + config.node_name, + "network_interface", + config.nic_num, + cls.model_fields["verb"].default, + ] class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): """Action which enables a NIC.""" + verb: str = "enable" + class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICEnableAction.""" @@ -41,6 +51,8 @@ class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): class HostNICDisableAction(HostNICAbstractAction, identifier="host_nic_disable"): """Action which disables a NIC.""" + verb: str = "disable" + class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICDisableAction.""" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 98ffbd00..26814bf2 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -33,7 +33,8 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): type: Literal["do_nothing"] = "do_nothing" - def form_request(self, options: ConfigSchema) -> RequestFormat: + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return ["do_nothing"] @@ -44,17 +45,6 @@ class ActionManager: def __init__( self, actions: List[Dict], # stores list of actions available to agent - # nodes: List[Dict], # extra configuration for each node - # max_folders_per_node: int = 2, # allows calculating shape - # max_files_per_folder: int = 2, # allows calculating shape - # max_services_per_node: int = 2, # allows calculating shape - # max_applications_per_node: int = 2, # allows calculating shape - # max_nics_per_node: int = 8, # allows calculating shape - # max_acl_rules: int = 10, # allows calculating shape - # protocols: List[str] = ["TCP", "UDP", "ICMP"], # allow mapping index to protocol - # ports: List[str] = ["HTTP", "DNS", "ARP", "FTP", "NTP"], # allow mapping index to port - # ip_list: List[str] = [], # to allow us to map an index to an ip address. - # wildcard_list: List[str] = [], # to allow mapping from wildcard index to act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions *args, **kwargs, @@ -66,114 +56,12 @@ class ActionManager: :param actions: List of action specs which should be made available to the agent. The keys of each spec are: 'type' and 'options' for passing any options to the action class's init method :type actions: List[dict] - :param nodes: Extra configuration for each node. - :type nodes: List[Dict] - :param max_folders_per_node: Maximum number of folders per node. Used for calculating action shape. - :type max_folders_per_node: int - :param max_files_per_folder: Maximum number of files per folder. Used for calculating action shape. - :type max_files_per_folder: int - :param max_services_per_node: Maximum number of services per node. Used for calculating action shape. - :type max_services_per_node: int - :param max_nics_per_node: Maximum number of NICs per node. Used for calculating action shape. - :type max_nics_per_node: int - :param max_acl_rules: Maximum number of ACL rules per router. Used for calculating action shape. - :type max_acl_rules: int - :param protocols: List of protocols that are available in the simulation. Used for calculating action shape. - :type protocols: List[str] - :param ports: List of ports that are available in the simulation. Used for calculating action shape. - :type ports: List[str] - :param ip_list: List of IP addresses that known to this agent. Used for calculating action shape. - :type ip_list: Optional[List[str]] :param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions. :type act_map: Optional[Dict[int, Dict]] """ - # self.node_names: List[str] = [n["node_name"] for n in nodes] - """List of node names in this action space. The list order is the mapping between node index and node name.""" - # self.application_names: List[List[str]] = [] - """ - List of applications per node. The list order gives the two-index mapping between (node_id, app_id) to app name. - The first index corresponds to node id, the second index is the app id on that particular node. - For instance, self.application_names[0][2] is the name of the third application on the first node. - """ - # self.service_names: List[List[str]] = [] - """ - List of services per node. The list order gives the two-index mapping between (node_id, svc_id) to svc name. - The first index corresponds to node id, the second index is the service id on that particular node. - For instance, self.service_names[0][2] is the name of the third service on the first node. - """ - # self.folder_names: List[List[str]] = [] - """ - List of folders per node. The list order gives the two-index mapping between (node_id, folder_id) to folder - name. The first index corresponds to node id, the second index is the folder id on that particular node. - For instance, self.folder_names[0][2] is the name of the third folder on the first node. - """ - # self.file_names: List[List[List[str]]] = [] - """ - List of files per folder per node. The list order gives the three-index mapping between - (node_id, folder_id, file_id) to file name. The first index corresponds to node id, the second index is the - folder id on that particular node, and the third index is the file id in that particular folder. - For instance, self.file_names[0][2][1] is the name of the second file in the third folder on the first node. - """ - - # Populate lists of apps, services, files, folders, etc on nodes. - # for node in nodes: - # app_list = [a["application_name"] for a in node.get("applications", [])] - # while len(app_list) < max_applications_per_node: - # app_list.append(None) - # self.application_names.append(app_list) - - # svc_list = [s["service_name"] for s in node.get("services", [])] - # while len(svc_list) < max_services_per_node: - # svc_list.append(None) - # self.service_names.append(svc_list) - - # folder_list = [f["folder_name"] for f in node.get("folders", [])] - # while len(folder_list) < max_folders_per_node: - # folder_list.append(None) - # self.folder_names.append(folder_list) - - # file_sublist = [] - # for folder in node.get("folders", [{"files": []}]): - # file_list = [f["file_name"] for f in folder.get("files", [])] - # while len(file_list) < max_files_per_folder: - # file_list.append(None) - # file_sublist.append(file_list) - # while len(file_sublist) < max_folders_per_node: - # file_sublist.append([None] * max_files_per_folder) - # self.file_names.append(file_sublist) - # self.protocols: List[str] = protocols - # self.ports: List[str] = ports - - # self.ip_address_list: List[str] = ip_list - # self.wildcard_list: List[str] = wildcard_list - # if self.wildcard_list == []: - # self.wildcard_list = ["NONE"] - # # action_args are settings which are applied to the action space as a whole. - # global_action_args = { - # "num_nodes": len(self.node_names), - # "num_folders": max_folders_per_node, - # "num_files": max_files_per_folder, - # "num_services": max_services_per_node, - # "num_applications": max_applications_per_node, - # "num_nics": max_nics_per_node, - # "num_acl_rules": max_acl_rules, - # "num_protocols": len(self.protocols), - # "num_ports": len(self.protocols), - # "num_ips": len(self.ip_address_list), - # "max_acl_rules": max_acl_rules, - # "max_nics_per_node": max_nics_per_node, - # } self.actions: Dict[str, AbstractAction] = {} for act_spec in actions: - # each action is provided into the action space config like this: - # - type: ACTION_TYPE - # options: - # option_1: value1 - # option_2: value2 - # where `type` decides which AbstractAction subclass should be used - # and `options` is an optional dict of options to pass to the init method of the action class act_type = act_spec.get("type") - # act_options = act_spec.get("options", {}) # Don't need this anymore I think? self.actions[act_type] = AbstractAction._registry[act_type] self.action_map: Dict[int, Tuple[str, Dict]] = {} @@ -237,8 +125,8 @@ class ActionManager: def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" - act_obj = self.actions[action_identifier] - return act_obj.form_request(action_options) + act_obj = self.actions[action_identifier].from_config(config=action_options) + return act_obj.form_request(config=act_obj.ConfigSchema) @property def space(self) -> spaces.Space: diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index 63eff218..af3793a2 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -24,12 +24,21 @@ class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstrac """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.target_nodename is None or config.port_id is None: return ["do_nothing"] - return ["network", "node", config.target_nodename, "network_interface", config.port_id, cls.verb] + return [ + "network", + "node", + config.target_nodename, + "network_interface", + config.port_id, + cls.model_fields["verb"].default, + ] class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_port_enable"): """Action which enables are port on a router or a firewall.""" + verb: str = "enable" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for NetworkPortEnableAction.""" @@ -39,6 +48,8 @@ class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_por class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_port_disable"): """Action which disables are port on a router or a firewall.""" + verb: str = "disable" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for NetworkPortDisableAction.""" diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index f95ba6df..29833b15 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -101,7 +101,8 @@ class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_scan"): """Action which performs an NMAP port scan.""" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): + source_node: str target_protocol: Optional[Union[str, List[str]]] = (None,) target_port: Optional[Union[str, List[str]]] = (None,) show: Optional[bool] = (False,) diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index bccfaba2..dbdd57d3 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -32,12 +32,14 @@ class NodeServiceAbstractAction(AbstractAction, identifier="node_service_abstrac @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["network", "node", config.node_name, "service", config.service_name, cls.verb] + return ["network", "node", config.node_name, "service", config.service_name, cls.model_fields["verb"].default] class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_scan"): """Action which scans a service.""" + verb: str = "scan" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceScanAction.""" @@ -47,6 +49,8 @@ class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_stop"): """Action which stops a service.""" + verb: str = "stop" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceStopAction.""" @@ -56,6 +60,8 @@ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service_start"): """Action which starts a service.""" + verb: str = "start" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceStartAction.""" @@ -65,6 +71,8 @@ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service_pause"): """Action which pauses a service.""" + verb: str = "pause" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServicePauseAction.""" @@ -74,6 +82,8 @@ class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_service_resume"): """Action which resumes a service.""" + verb: str = "resume" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceResumeAction.""" @@ -83,6 +93,8 @@ class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_servic class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_service_restart"): """Action which restarts a service.""" + verb: str = "restart" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceRestartAction.""" @@ -92,6 +104,8 @@ class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_servi class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_service_disable"): """Action which disables a service.""" + verb: str = "disable" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceDisableAction.""" @@ -101,6 +115,8 @@ class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_servi class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_service_enable"): """Action which enables a service.""" + verb: str = "enable" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceEnableAction.""" @@ -110,6 +126,8 @@ class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_servic class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_fix"): """Action which fixes a service.""" + verb: str = "fix" + class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceFixAction.""" diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index f77a85b1..76b97cbd 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -30,6 +30,9 @@ class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstrac class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_session_remote_login"): """Action which performs a remote session login.""" + username: str + password: str + class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLoginAction.""" @@ -54,7 +57,7 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_ ] -class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node_session_remote_logout"): +class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node_session_remote_logoff"): """Action which performs a remote session logout.""" class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): @@ -68,3 +71,33 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node if config.node_name is None or config.remote_ip is None: return ["do_nothing"] return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip] + + +class NodeAccountsChangePasswordAction(NodeSessionAbstractAction, identifier="node_accounts_change_password"): + """Action which changes the password for a user.""" + + username: str + current_password: str + new_password: str + + class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): + """Configuration schema for NodeAccountsChangePasswordAction.""" + + username: str + current_password: str + new_password: str + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + return [ + "network", + "node", + config.node_name, + "service", + "UserManager", + "change_password", + config.username, + config.current_password, + cls.new_password, + ] diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 2292616d..18e27de7 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -96,8 +96,8 @@ agents: action_space: action_list: - - type: DONOTHING - - type: FIREWALL_ACL_ADDRULE + - type: do_nothing + - type: FIREWALL_ACL_ADDRULE firewall_acl_add_rule - type: FIREWALL_ACL_REMOVERULE - type: NETWORK_PORT_DISABLE - type: NETWORK_PORT_ENABLE diff --git a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml index c5508f13..ec50ecdf 100644 --- a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml +++ b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml @@ -34,15 +34,16 @@ agents: max_services_per_node: 1 max_applications_per_node: 1 action_list: - - type: NODE_NMAP_NETWORK_SERVICE_RECON + - type: node_network_service_recon action_map: 0: - action: NODE_NMAP_NETWORK_SERVICE_RECON + action: node_network_service_recon options: source_node: client_1 target_ip_address: 192.168.10.0/24 target_port: 80 target_protocol: tcp + show: false reward_function: reward_components: diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index 33ba3d19..eb7b6752 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -34,13 +34,14 @@ agents: max_services_per_node: 1 max_applications_per_node: 1 action_list: - - type: NODE_NMAP_PING_SCAN + - type: node_nmap_ping_scan action_map: 0: - action: NODE_NMAP_PING_SCAN + action: node_nmap_ping_scan options: - source_node: client_1 + node_name: client_1 target_ip_address: 192.168.1.0/24 + show: False reward_function: reward_components: diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 8ed715c1..15e2cb6a 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -34,19 +34,21 @@ agents: max_services_per_node: 1 max_applications_per_node: 1 action_list: - - type: NODE_NMAP_PORT_SCAN + - type: node_nmap_port_scan action_map: 0: - action: NODE_NMAP_PORT_SCAN + action: node_nmap_port_scan options: source_node: client_1 target_ip_address: 192.168.10.0/24 + target_protocol: tcp target_port: - 21 - 53 - 80 - 123 - 219 + show: false reward_function: reward_components: diff --git a/tests/conftest.py b/tests/conftest.py index 64fe0699..8bfc78a4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -419,54 +419,54 @@ def game_and_agent(): install_stuff_to_sim(sim) actions = [ - {"type": "DONOTHING"}, - {"type": "NODE_SERVICE_SCAN"}, - {"type": "NODE_SERVICE_STOP"}, - {"type": "NODE_SERVICE_START"}, - {"type": "NODE_SERVICE_PAUSE"}, - {"type": "NODE_SERVICE_RESUME"}, - {"type": "NODE_SERVICE_RESTART"}, - {"type": "NODE_SERVICE_DISABLE"}, - {"type": "NODE_SERVICE_ENABLE"}, - {"type": "NODE_SERVICE_FIX"}, - {"type": "NODE_APPLICATION_EXECUTE"}, - {"type": "NODE_APPLICATION_SCAN"}, - {"type": "NODE_APPLICATION_CLOSE"}, - {"type": "NODE_APPLICATION_FIX"}, - {"type": "NODE_APPLICATION_INSTALL"}, - {"type": "NODE_APPLICATION_REMOVE"}, - {"type": "NODE_FILE_CREATE"}, - {"type": "NODE_FILE_SCAN"}, - {"type": "NODE_FILE_CHECKHASH"}, - {"type": "NODE_FILE_DELETE"}, - {"type": "NODE_FILE_REPAIR"}, - {"type": "NODE_FILE_RESTORE"}, - {"type": "NODE_FILE_CORRUPT"}, - {"type": "NODE_FILE_ACCESS"}, - {"type": "NODE_FOLDER_CREATE"}, - {"type": "NODE_FOLDER_SCAN"}, - {"type": "NODE_FOLDER_CHECKHASH"}, - {"type": "NODE_FOLDER_REPAIR"}, - {"type": "NODE_FOLDER_RESTORE"}, - {"type": "NODE_OS_SCAN"}, - {"type": "NODE_SHUTDOWN"}, - {"type": "NODE_STARTUP"}, - {"type": "NODE_RESET"}, - {"type": "ROUTER_ACL_ADDRULE"}, - {"type": "ROUTER_ACL_REMOVERULE"}, - {"type": "HOST_NIC_ENABLE"}, - {"type": "HOST_NIC_DISABLE"}, - {"type": "NETWORK_PORT_ENABLE"}, - {"type": "NETWORK_PORT_DISABLE"}, - {"type": "CONFIGURE_C2_BEACON"}, - {"type": "C2_SERVER_RANSOMWARE_LAUNCH"}, - {"type": "C2_SERVER_RANSOMWARE_CONFIGURE"}, - {"type": "C2_SERVER_TERMINAL_COMMAND"}, - {"type": "C2_SERVER_DATA_EXFILTRATE"}, - {"type": "NODE_ACCOUNTS_CHANGE_PASSWORD"}, - {"type": "SSH_TO_REMOTE"}, - {"type": "SESSIONS_REMOTE_LOGOFF"}, - {"type": "NODE_SEND_REMOTE_COMMAND"}, + {"type": "do_nothing"}, + {"type": "node_service_scan"}, + {"type": "node_service_stop"}, + {"type": "node_service_start"}, + {"type": "node_service_pause"}, + {"type": "node_service_resume"}, + {"type": "node_service_restart"}, + {"type": "node_service_disable"}, + {"type": "node_service_enable"}, + {"type": "node_service_fix"}, + {"type": "node_application_execute"}, + {"type": "node_application_scan"}, + {"type": "node_application_close"}, + {"type": "node_application_fix"}, + {"type": "node_application_install"}, + {"type": "node_application_remove"}, + {"type": "node_file_create"}, + {"type": "node_file_scan"}, + {"type": "node_file_checkhash"}, + {"type": "node_file_delete"}, + {"type": "node_file_repair"}, + {"type": "node_file_restore"}, + {"type": "node_file_corrupt"}, + {"type": "node_file_access"}, + {"type": "node_folder_create"}, + {"type": "node_folder_scan"}, + {"type": "node_folder_checkhash"}, + {"type": "node_folder_repair"}, + {"type": "node_folder_restore"}, + {"type": "node_os_scan"}, + {"type": "node_shutdown"}, + {"type": "node_startup"}, + {"type": "node_reset"}, + {"type": "router_acl_add_rule"}, + {"type": "router_acl_remove_rule"}, + {"type": "host_nic_enable"}, + {"type": "host_nic_disable"}, + {"type": "network_port_enable"}, + {"type": "network_port_disable"}, + {"type": "configure_c2_beacon"}, + {"type": "c2_server_ransomware_launch"}, + {"type": "c2_server_ransomware_configure"}, + {"type": "c2_server_terminal_command"}, + {"type": "c2_server_data_exfiltrate"}, + {"type": "node_accounts_change_password"}, + {"type": "node_session_remote_login"}, + {"type": "node_session_remote_logoff"}, + {"type": "node_send_remote_command"}, ] action_space = ActionManager( diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index e03a7d26..21aa31de 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -35,7 +35,7 @@ def test_do_nothing_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]) """Test that the DoNothingAction can form a request and that it is accepted by the simulation.""" game, agent = game_and_agent - action = ("DONOTHING", {}) + action = ("do_nothing", {}) agent.store_action(action) game.step() @@ -56,7 +56,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("NODE_SERVICE_SCAN", {"node_id": 1, "service_id": 0}) + action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -67,7 +67,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("NODE_SERVICE_SCAN", {"node_id": 1, "service_id": 0}) + action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.COMPROMISED @@ -88,7 +88,7 @@ def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA svc.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a patch action - action = ("NODE_SERVICE_FIX", {"node_id": 1, "service_id": 0}) + action = ("node_service_fix", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() @@ -96,7 +96,7 @@ def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA assert svc.health_state_actual == SoftwareHealthState.FIXING # 4: perform a few do-nothing steps and check that the service is now in the good state - action = ("DONOTHING", {}) + action = ("do_nothing", {}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -121,7 +121,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox # 2: Add a rule to block client 1 from reaching server 2 on router action = ( - "ROUTER_ACL_ADDRULE", + "router_acl_add_rule", { "target_router": "router", "position": 4, # 4th rule @@ -130,7 +130,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox "dest_ip_id": 6, # 10.0.2.3 (server_2) "dest_port_id": 1, # ALL "source_port_id": 1, # ALL - "protocol_id": 1, # ALL + "protocol_name": "ALL", # ALL "source_wildcard_id": 0, "dest_wildcard_id": 0, }, @@ -186,7 +186,7 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P # 2: Remove rule that allows HTTP traffic across the network action = ( - "ROUTER_ACL_REMOVERULE", + "router_acl_remove_rule", { "target_router": "router", "position": 3, # 4th rule @@ -219,10 +219,10 @@ def test_host_nic_disable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA # 2: Disable the NIC on client_1 action = ( - "HOST_NIC_DISABLE", + "host_nic_disable", { - "node_id": 0, # client_1 - "nic_id": 0, # the only nic (eth-1) + "node_name": "client_1", # client_1 + "nic_num": 1, # the only nic (eth-1) }, ) agent.store_action(action) @@ -250,10 +250,10 @@ def test_host_nic_enable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAg # 2: Use action to enable nic action = ( - "HOST_NIC_ENABLE", + "host_nic_enable", { - "node_id": 0, # client_1 - "nic_id": 0, # the only nic (eth-1) + "node_name": "client_1", # client_1 + "nic_num": 1, # the only nic (eth-1) }, ) agent.store_action(action) @@ -277,11 +277,11 @@ def test_node_file_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAge # 2: perform a scan and make sure nothing has changed action = ( - "NODE_FILE_SCAN", + "node_file_scan", { - "node_id": 0, # client_1, - "folder_id": 0, # downloads, - "file_id": 0, # cat.png + "node_name": "client_1", # client_1, + "folder_name": "downloads", # downloads, + "file_name": "cat.png", # cat.png }, ) agent.store_action(action) @@ -314,11 +314,11 @@ def test_node_file_delete_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA # 2: delete the file action = ( - "NODE_FILE_DELETE", + "node_file_delete", { - "node_id": 0, # client_1 - "folder_id": 0, # downloads - "file_id": 0, # cat.png + "node_name": "client_1", # client_1 + "folder_name": "downloads", # downloads + "file_name": "cat.png", # cat.png }, ) agent.store_action(action) @@ -334,15 +334,11 @@ def test_node_file_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): """Test that a file is created.""" game, agent = game_and_agent - client_1 = game.simulation.network.get_node_by_hostname("client_1") # + client_1 = game.simulation.network.get_node_by_hostname("client_1") action = ( - "NODE_FILE_CREATE", - { - "node_id": 0, - "folder_name": "test", - "file_name": "file.txt", - }, + "node_file_create", + {"node_name": "client_1", "folder_name": "test", "file_name": "file.txt", "force": "False"}, ) agent.store_action(action) game.step() @@ -357,9 +353,9 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): client_1 = game.simulation.network.get_node_by_hostname("client_1") # action = ( - "NODE_FILE_CREATE", + "node_file_create", { - "node_id": 0, + "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", }, @@ -370,9 +366,9 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): assert client_1.file_system.get_file(folder_name="test", file_name="file.txt").num_access == 0 action = ( - "NODE_FILE_ACCESS", + "node_file_access", { - "node_id": 0, + "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", }, @@ -390,9 +386,9 @@ def test_node_folder_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): client_1 = game.simulation.network.get_node_by_hostname("client_1") # action = ( - "NODE_FOLDER_CREATE", + "node_folder_create", { - "node_id": 0, + "node_name": "client_1", "folder_name": "test", }, ) @@ -418,7 +414,7 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG # 2: Disable the NIC on client_1 action = ( - "NETWORK_PORT_DISABLE", + "network_port_disable", { "target_nodename": "router", # router "port_id": 1, # port 1 @@ -450,7 +446,7 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa # 2: Use action to enable port action = ( - "NETWORK_PORT_ENABLE", + "network_port_enable", { "target_nodename": "router", # router "port_id": 1, # port 1 @@ -480,7 +476,7 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P assert browser.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("NODE_APPLICATION_SCAN", {"node_id": 0, "application_id": 0}) + action = ("node_application_scan", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() assert browser.health_state_actual == SoftwareHealthState.GOOD @@ -491,7 +487,7 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P assert browser.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("NODE_APPLICATION_SCAN", {"node_id": 0, "application_id": 0}) + action = ("node_application_scan", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() assert browser.health_state_actual == SoftwareHealthState.COMPROMISED @@ -512,7 +508,7 @@ def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, Pr browser.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a fix action - action = ("NODE_APPLICATION_FIX", {"node_id": 0, "application_id": 0}) + action = ("node_application_fix", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() @@ -520,7 +516,7 @@ def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, Pr assert browser.health_state_actual == SoftwareHealthState.FIXING # 4: perform a few do-nothing steps and check that the application is now in the good state - action = ("DONOTHING", {}) + action = ("do_nothing", {}) agent.store_action(action) game.step() assert browser.health_state_actual == SoftwareHealthState.GOOD @@ -538,7 +534,7 @@ def test_node_application_close_integration(game_and_agent: Tuple[PrimaiteGame, assert browser.operating_state == ApplicationOperatingState.RUNNING # 2: Apply a close action - action = ("NODE_APPLICATION_CLOSE", {"node_id": 0, "application_id": 0}) + action = ("node_application_close", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() @@ -549,7 +545,7 @@ def test_node_application_install_and_uninstall_integration(game_and_agent: Tupl """Test that the NodeApplicationInstallAction and NodeApplicationRemoveAction can form a request and that it is accepted by the simulation. - When you initiate a install action, the Application will be installed and configured on the node. + When you initiate an install action, the Application will be installed and configured on the node. The remove action will uninstall the application from the node.""" game, agent = game_and_agent @@ -557,13 +553,13 @@ def test_node_application_install_and_uninstall_integration(game_and_agent: Tupl assert client_1.software_manager.software.get("DoSBot") is None - action = ("NODE_APPLICATION_INSTALL", {"node_id": 0, "application_name": "DoSBot"}) + action = ("node_application_install", {"node_name": "client_1", "application_name": "DoSBot"}) agent.store_action(action) game.step() assert client_1.software_manager.software.get("DoSBot") is not None - action = ("NODE_APPLICATION_REMOVE", {"node_id": 0, "application_name": "DoSBot"}) + action = ("node_application_remove", {"node_name": "client_1", "application_name": "DoSBot"}) agent.store_action(action) game.step() diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index ec18f1fb..2fd2da0c 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -27,9 +27,9 @@ def test_probabilistic_agent(): action_space = ActionManager( actions=[ - {"type": "DONOTHING"}, - {"type": "NODE_APPLICATION_EXECUTE"}, - {"type": "NODE_FILE_DELETE"}, + {"type": "do_nothing"}, + {"type": "node_application_execute"}, + {"type": "node_file_delete"}, ], nodes=[ { @@ -47,9 +47,15 @@ def test_probabilistic_agent(): protocols=["TCP", "UDP", "ICMP"], ports=["HTTP", "DNS", "ARP"], act_map={ - 0: {"action": "DONOTHING", "options": {}}, - 1: {"action": "NODE_APPLICATION_EXECUTE", "options": {"node_id": 0, "application_id": 0}}, - 2: {"action": "NODE_FILE_DELETE", "options": {"node_id": 0, "folder_id": 0, "file_id": 0}}, + 0: {"action": "do_nothing", "options": {}}, + 1: { + "action": "node_application_execute", + "options": {"node_name": "client_1", "application_name": "WebBrowser"}, + }, + 2: { + "action": "node_file_delete", + "options": {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, + }, }, ) observation_space = ObservationManager(NestedObservation(components={})) @@ -70,11 +76,11 @@ def test_probabilistic_agent(): node_file_delete_count = 0 for _ in range(N_TRIALS): a = pa.get_action(0) - if a == ("DONOTHING", {}): + if a == ("do_nothing", {}): do_nothing_count += 1 - elif a == ("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}): + elif a == ("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"}): node_application_execute_count += 1 - elif a == ("NODE_FILE_DELETE", {"node_id": 0, "folder_id": 0, "file_id": 0}): + elif a == ("node_file_delete", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}): node_file_delete_count += 1 else: raise AssertionError("Probabilistic agent produced an unexpected action.") From eb827f7e0a98b8c4dc9a94dc0e739a7c213d1d9f Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 31 Oct 2024 14:42:26 +0000 Subject: [PATCH 049/224] #2913: How-To guide initial commit. --- .../how_to_guides/extensible_rewards.rst | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/source/how_to_guides/extensible_rewards.rst diff --git a/docs/source/how_to_guides/extensible_rewards.rst b/docs/source/how_to_guides/extensible_rewards.rst new file mode 100644 index 00000000..3505d66c --- /dev/null +++ b/docs/source/how_to_guides/extensible_rewards.rst @@ -0,0 +1,72 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _about: + +Extensible Rewards +****************** + +Changes to reward class structure. +================================== + +Reward classes are inherited from AbstractReward (a sub-class of Pydantic's BaseModel). +Within the reward class is a ConfigSchema class responsible for ensuring config file data is in the +correct format. The `.from_config()` method is generally unchanged. + +Inheriting from `BaseModel` removes the need for an `__init__` method bu means that object +attributes need to be passed by keyword. + +.. code:: Python + +class AbstractReward(BaseModel): + """Base class for reward function components.""" + + class ConfigSchema(BaseModel, ABC): + """Config schema for AbstractReward.""" + + type: str + + _registry: ClassVar[Dict[str, Type["AbstractReward"]]] = {} + + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Duplicate node adder {identifier}") + cls._registry[identifier] = cls + + @classmethod + def from_config(cls, config: Dict) -> "AbstractReward": + """Create a reward function component from a config dictionary. + + :param config: dict of options for the reward component's constructor + :type config: dict + :return: The reward component. + :rtype: AbstractReward + """ + if config["type"] not in cls._registry: + raise ValueError(f"Invalid reward type {config['type']}") + adder_class = cls._registry[config["type"]] + adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config)) + return cls + + @abstractmethod + def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: + """Calculate the reward for the current state. + + :param state: Current simulation state + :type state: Dict + :param last_action_response: Current agent history state + :type last_action_response: AgentHistoryItem state + :return: Reward value + :rtype: float + """ + return 0.0 + + +Changes to YAML file. +===================== +.. code:: YAML + + There's no longer a need to provide a `dns_server` as an option in the simulation section + of the config file. \ No newline at end of file From 6b29362bf955ece37b39b31d34ee189231f9b5e5 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 31 Oct 2024 14:42:50 +0000 Subject: [PATCH 050/224] #2913: Tidy up config files. --- tests/assets/configs/fix_duration_one_item.yaml | 2 -- tests/assets/configs/software_fix_duration.yaml | 1 - 2 files changed, 3 deletions(-) diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index 4163bcfd..c74590a1 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -230,8 +230,6 @@ simulation: server_password: arcd services: - type: DNSClient - # options: - # dns_server: 192.168.1.10 links: - endpoint_a_hostname: switch_1 diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 2b72e85f..6a705b37 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -209,7 +209,6 @@ simulation: services: - type: DNSClient options: - # dns_server: 192.168.1.10 fix_duration: 3 - type: DNSServer options: From b849ea6312c21366256fc297297017112dab1d9b Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 4 Nov 2024 17:41:43 +0000 Subject: [PATCH 051/224] #2913: Remove from_config() and refactor (WIP). --- src/primaite/game/agent/rewards.py | 167 +++++------------- .../game_layer/test_rewards.py | 11 +- 2 files changed, 48 insertions(+), 130 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 2386bed5..03764e4b 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -46,6 +46,14 @@ WhereType = Optional[Iterable[Union[str, int]]] class AbstractReward(BaseModel): """Base class for reward function components.""" + config: "AbstractReward.ConfigSchema" + + # def __init__(self, schema_name, **kwargs): + # super.__init__(self, **kwargs) + # # Create ConfigSchema class + # self.config_class = type(schema_name, (BaseModel, ABC), **kwargs) + # self.config = self.config_class() + class ConfigSchema(BaseModel, ABC): """Config schema for AbstractReward.""" @@ -56,7 +64,7 @@ class AbstractReward(BaseModel): def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) if identifier in cls._registry: - raise ValueError(f"Duplicate node adder {identifier}") + raise ValueError(f"Duplicate reward {identifier}") cls._registry[identifier] = cls @classmethod @@ -70,9 +78,10 @@ class AbstractReward(BaseModel): """ if config["type"] not in cls._registry: raise ValueError(f"Invalid reward type {config['type']}") - adder_class = cls._registry[config["type"]] - adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config)) - return cls + reward_class = cls._registry[config["type"]] + reward_config = reward_class.ConfigSchema(**config) + reward_class(config=reward_config) + return reward_class @abstractmethod def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: @@ -103,30 +112,18 @@ class DummyReward(AbstractReward, identifier="DummyReward"): """ return 0.0 - @classmethod - def from_config(cls, config: dict) -> "DummyReward": - """Create a reward function component from a config dictionary. - - :param config: dict of options for the reward component's constructor. Should be empty. - :type config: dict - :return: The reward component. - :rtype: DummyReward - """ - return cls() - class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" - node_hostname: str - folder_name: str - file_name: str + config: "DatabaseFileIntegrity.ConfigSchema" location_in_state: List[str] = [""] reward: float = 0.0 class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for DatabaseFileIntegrity.""" + type: str = "DatabaseFileIntegrity" node_hostname: str folder_name: str file_name: str @@ -144,12 +141,12 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): self.location_in_state = [ "network", "nodes", - self.node_hostname, + self.config.node_hostname, "file_system", "folders", - self.folder_name, + self.config.folder_name, "files", - self.file_name, + self.config.file_name, ] database_file_state = access_from_nested_dict(state, self.location_in_state) @@ -168,38 +165,18 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): else: return 0 - @classmethod - def from_config(cls, config: Dict) -> "DatabaseFileIntegrity": - """Create a reward function component from a config dictionary. - - :param config: dict of options for the reward component's constructor - :type config: Dict - :return: The reward component. - :rtype: DatabaseFileIntegrity - """ - node_hostname = config.get("node_hostname") - folder_name = config.get("folder_name") - file_name = config.get("file_name") - if not (node_hostname and folder_name and file_name): - msg = f"{cls.__name__} could not be initialised with parameters {config}" - _LOGGER.error(msg) - raise ValueError(msg) - - return cls(node_hostname=node_hostname, folder_name=folder_name, file_name=file_name) - class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): """Reward function component which penalises the agent when the web server returns a 404 error.""" - node_hostname: str - service_name: str - sticky: bool = True + config: "WebServer404Penalty.ConfigSchema" location_in_state: List[str] = [""] reward: float = 0.0 class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebServer404Penalty.""" + type: str = "WebServer404Penalty" node_hostname: str service_name: str sticky: bool = True @@ -217,9 +194,9 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): self.location_in_state = [ "network", "nodes", - self.node_hostname, + self.config.node_hostname, "services", - self.service_name, + self.config.service_name, ] web_service_state = access_from_nested_dict(state, self.location_in_state) @@ -242,43 +219,20 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): return self.reward - @classmethod - def from_config(cls, config: Dict) -> "WebServer404Penalty": - """Create a reward function component from a config dictionary. - - :param config: dict of options for the reward component's constructor - :type config: Dict - :return: The reward component. - :rtype: WebServer404Penalty - """ - node_hostname = config.get("node_hostname") - service_name = config.get("service_name") - if not (node_hostname and service_name): - msg = ( - f"{cls.__name__} could not be initialised from config because node_name and service_ref were not " - "found in reward config." - ) - _LOGGER.warning(msg) - raise ValueError(msg) - sticky = config.get("sticky", True) - - return cls(node_hostname=node_hostname, service_name=service_name, sticky=sticky) - class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePenalty"): """Penalises the agent when the web browser fails to fetch a webpage.""" - node_hostname: str = "" - sticky: bool = True - reward: float = 0.0 - location_in_state: List[str] = [""] + config: "WebpageUnavailablePenalty.ConfigSchema" + reward: float = 0.0 # XXX: Private attribute? + location_in_state: List[str] = [""] # Calculate in __init__()? class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebpageUnavailablePenalty.""" + type: str = "WebpageUnavailablePenalty" node_hostname: str = "" sticky: bool = True - reward: float = 0.0 def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """ @@ -297,7 +251,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe self.location_in_state = [ "network", "nodes", - self.node_hostname, + self.config.node_hostname, "applications", "WebBrowser", ] @@ -310,14 +264,14 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe request_attempted = last_action_response.request == [ "network", "node", - self.node_hostname, + self.config.node_hostname, "application", "WebBrowser", "execute", ] # skip calculating if sticky and no new codes, reusing last step value - if not request_attempted and self.sticky: + if not request_attempted and self.config.sticky: return self.reward if last_action_response.response.status != "success": @@ -339,29 +293,17 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe return self.reward - @classmethod - def from_config(cls, config: dict) -> AbstractReward: - """ - Build the reward component object from config. - - :param config: Configuration dictionary. - :type config: Dict - """ - node_hostname = config.get("node_hostname") - sticky = config.get("sticky", True) - return cls(node_hostname=node_hostname, sticky=sticky) - class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdminDatabaseUnreachablePenalty"): """Penalises the agent when the green db clients fail to connect to the database.""" - node_hostname: str = "" - sticky: bool = True + config: "GreenAdminDatabaseUnreachablePenalty.ConfigSchema" reward: float = 0.0 class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for GreenAdminDatabaseUnreachablePenalty.""" + type: str = "GreenAdminDatabaseUnreachablePenalty" node_hostname: str sticky: bool = True @@ -383,7 +325,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi request_attempted = last_action_response.request == [ "network", "node", - self.node_hostname, + self.config.node_hostname, "application", "DatabaseClient", "execute", @@ -392,7 +334,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi if request_attempted: # if agent makes request, always recalculate fresh value last_action_response.reward_info = {"connection_attempt_status": last_action_response.response.status} self.reward = 1.0 if last_action_response.response.status == "success" else -1.0 - elif not self.sticky: # if no new request and not sticky, set reward to 0 + elif not self.config.sticky: # if no new request and not sticky, set reward to 0 last_action_response.reward_info = {"connection_attempt_status": "n/a"} self.reward = 0.0 else: # if no new request and sticky, reuse reward value from last step @@ -401,27 +343,16 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi return self.reward - @classmethod - def from_config(cls, config: Dict) -> AbstractReward: - """ - Build the reward component object from config. - - :param config: Configuration dictionary. - :type config: Dict - """ - node_hostname = config.get("node_hostname") - sticky = config.get("sticky", True) - return cls(node_hostname=node_hostname, sticky=sticky) - class SharedReward(AbstractReward, identifier="SharedReward"): """Adds another agent's reward to the overall reward.""" - agent_name: str + config: "SharedReward.ConfigSchema" class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for SharedReward.""" + type: str = "SharedReward" agent_name: str def default_callback(agent_name: str) -> Never: @@ -447,29 +378,18 @@ class SharedReward(AbstractReward, identifier="SharedReward"): :return: Reward value :rtype: float """ - return self.callback(self.agent_name) - - @classmethod - def from_config(cls, config: Dict) -> "SharedReward": - """ - Build the SharedReward object from config. - - :param config: Configuration dictionary - :type config: Dict - """ - agent_name = config.get("agent_name") - return cls(agent_name=agent_name) + return self.callback(self.config.agent_name) class ActionPenalty(AbstractReward, identifier="ActionPenalty"): """Apply a negative reward when taking any action except DONOTHING.""" - action_penalty: float = -1.0 - do_nothing_penalty: float = 0.0 + config: "ActionPenalty.ConfigSchema" class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for ActionPenalty.""" + type: str = "ActionPenalty" action_penalty: float = -1.0 do_nothing_penalty: float = 0.0 @@ -484,16 +404,9 @@ class ActionPenalty(AbstractReward, identifier="ActionPenalty"): :rtype: float """ if last_action_response.action == "DONOTHING": - return self.do_nothing_penalty + return self.config.do_nothing_penalty else: - return self.action_penalty - - @classmethod - def from_config(cls, config: Dict) -> "ActionPenalty": - """Build the ActionPenalty object from config.""" - action_penalty = config.get("action_penalty", -1.0) - do_nothing_penalty = config.get("do_nothing_penalty", 0.0) - return cls(action_penalty=action_penalty, do_nothing_penalty=do_nothing_penalty) + return self.config.action_penalty class RewardFunction: diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index bf707feb..d4236d1b 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -23,7 +23,9 @@ def test_WebpageUnavailablePenalty(game_and_agent): # set up the scenario, configure the web browser to the correct url game, agent = game_and_agent agent: ControlledAgent - comp = WebpageUnavailablePenalty(node_hostname="client_1") + schema = WebpageUnavailablePenalty.ConfigSchema(node_hostname="client_1", sticky=True) + comp = WebpageUnavailablePenalty(config=schema) + client_1 = game.simulation.network.get_node_by_hostname("client_1") browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() @@ -74,7 +76,8 @@ def test_uc2_rewards(game_and_agent): ACLAction.PERMIT, src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"], position=2 ) - comp = GreenAdminDatabaseUnreachablePenalty(node_hostname="client_1") + schema = GreenAdminDatabaseUnreachablePenalty.ConfigSchema(node_hostname="client_1", sticky=True) + comp = GreenAdminDatabaseUnreachablePenalty(config=schema) request = ["network", "node", "client_1", "application", "DatabaseClient", "execute"] response = game.simulation.apply_request(request) @@ -147,7 +150,9 @@ def test_action_penalty(): """Test that the action penalty is correctly applied when agent performs any action""" # Create an ActionPenalty Reward - Penalty = ActionPenalty(action_penalty=-0.75, do_nothing_penalty=0.125) + schema = ActionPenalty.ConfigSchema(action_penalty=-0.75, do_nothing_penalty=0.125) + # Penalty = ActionPenalty(action_penalty=-0.75, do_nothing_penalty=0.125) + Penalty = ActionPenalty(schema) # Assert that penalty is applied if action isn't DONOTHING reward_value = Penalty.calculate( From 370bcfc47682049cafe238f97e089b5d8b5ea013 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 6 Nov 2024 11:35:06 +0000 Subject: [PATCH 052/224] #2913: Make rewards work with config file. --- src/primaite/game/agent/rewards.py | 51 ++++++++----------- src/primaite/game/game.py | 2 +- .../game_layer/test_rewards.py | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 03764e4b..029597a0 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -79,9 +79,8 @@ class AbstractReward(BaseModel): if config["type"] not in cls._registry: raise ValueError(f"Invalid reward type {config['type']}") reward_class = cls._registry[config["type"]] - reward_config = reward_class.ConfigSchema(**config) - reward_class(config=reward_config) - return reward_class + reward_obj = reward_class(config=reward_class.ConfigSchema(**config)) + return reward_obj @abstractmethod def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: @@ -97,7 +96,7 @@ class AbstractReward(BaseModel): return 0.0 -class DummyReward(AbstractReward, identifier="DummyReward"): +class DummyReward(AbstractReward, identifier="DUMMY"): """Dummy reward function component which always returns 0.0.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: @@ -113,7 +112,7 @@ class DummyReward(AbstractReward, identifier="DummyReward"): return 0.0 -class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): +class DatabaseFileIntegrity(AbstractReward, identifier="DATABASE_FILE_INTEGRITY"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" config: "DatabaseFileIntegrity.ConfigSchema" @@ -123,7 +122,7 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for DatabaseFileIntegrity.""" - type: str = "DatabaseFileIntegrity" + type: str = "DATABASE_FILE_INTEGRITY" node_hostname: str folder_name: str file_name: str @@ -166,7 +165,7 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DatabaseFileIntegrity"): return 0 -class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): +class WebServer404Penalty(AbstractReward, identifier="WEB_SERVER_404_PENALTY"): """Reward function component which penalises the agent when the web server returns a 404 error.""" config: "WebServer404Penalty.ConfigSchema" @@ -176,7 +175,7 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebServer404Penalty.""" - type: str = "WebServer404Penalty" + type: str = "WEB_SERVER_404_PENALTY" node_hostname: str service_name: str sticky: bool = True @@ -220,7 +219,7 @@ class WebServer404Penalty(AbstractReward, identifier="WebServer404Penalty"): return self.reward -class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePenalty"): +class WebpageUnavailablePenalty(AbstractReward, identifier="WEBPAGE_UNAVAILABLE_PENALTY"): """Penalises the agent when the web browser fails to fetch a webpage.""" config: "WebpageUnavailablePenalty.ConfigSchema" @@ -230,7 +229,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebpageUnavailablePenalty.""" - type: str = "WebpageUnavailablePenalty" + type: str = "WEBPAGE_UNAVAILABLE_PENALTY" node_hostname: str = "" sticky: bool = True @@ -294,7 +293,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WebpageUnavailablePe return self.reward -class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdminDatabaseUnreachablePenalty"): +class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY"): """Penalises the agent when the green db clients fail to connect to the database.""" config: "GreenAdminDatabaseUnreachablePenalty.ConfigSchema" @@ -303,7 +302,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for GreenAdminDatabaseUnreachablePenalty.""" - type: str = "GreenAdminDatabaseUnreachablePenalty" + type: str = "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY" node_hostname: str sticky: bool = True @@ -344,7 +343,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GreenAdmi return self.reward -class SharedReward(AbstractReward, identifier="SharedReward"): +class SharedReward(AbstractReward, identifier="SHARED_REWARD"): """Adds another agent's reward to the overall reward.""" config: "SharedReward.ConfigSchema" @@ -352,7 +351,7 @@ class SharedReward(AbstractReward, identifier="SharedReward"): class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for SharedReward.""" - type: str = "SharedReward" + type: str = "SHARED_REWARD" agent_name: str def default_callback(agent_name: str) -> Never: @@ -381,7 +380,7 @@ class SharedReward(AbstractReward, identifier="SharedReward"): return self.callback(self.config.agent_name) -class ActionPenalty(AbstractReward, identifier="ActionPenalty"): +class ActionPenalty(AbstractReward, identifier="ACTION_PENALTY"): """Apply a negative reward when taking any action except DONOTHING.""" config: "ActionPenalty.ConfigSchema" @@ -389,7 +388,7 @@ class ActionPenalty(AbstractReward, identifier="ActionPenalty"): class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for ActionPenalty.""" - type: str = "ActionPenalty" + type: str = "ACTION_PENALTY" action_penalty: float = -1.0 do_nothing_penalty: float = 0.0 @@ -412,17 +411,6 @@ class ActionPenalty(AbstractReward, identifier="ActionPenalty"): class RewardFunction: """Manages the reward function for the agent.""" - rew_class_identifiers: Dict[str, Type[AbstractReward]] = { - "DUMMY": DummyReward, - "DATABASE_FILE_INTEGRITY": DatabaseFileIntegrity, - "WEB_SERVER_404_PENALTY": WebServer404Penalty, - "WEBPAGE_UNAVAILABLE_PENALTY": WebpageUnavailablePenalty, - "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY": GreenAdminDatabaseUnreachablePenalty, - "SHARED_REWARD": SharedReward, - "ACTION_PENALTY": ActionPenalty, - } - """List of reward class identifiers.""" - def __init__(self): """Initialise the reward function object.""" self.reward_components: List[Tuple[AbstractReward, float]] = [] @@ -457,7 +445,7 @@ class RewardFunction: @classmethod def from_config(cls, config: Dict) -> "RewardFunction": - """Create a reward function from a config dictionary. + """Create a reward function from a config dictionary and its related reward class. :param config: dict of options for the reward manager's constructor :type config: Dict @@ -468,8 +456,11 @@ class RewardFunction: for rew_component_cfg in config["reward_components"]: rew_type = rew_component_cfg["type"] + # XXX: If options key is missing add key then add type key. + if "options" not in rew_component_cfg: + rew_component_cfg["options"] = {} + rew_component_cfg["options"]["type"] = rew_type weight = rew_component_cfg.get("weight", 1.0) - rew_class = cls.rew_class_identifiers[rew_type] - rew_instance = rew_class.from_config(config=rew_component_cfg.get("options", {})) + rew_instance = AbstractReward.from_config(rew_component_cfg["options"]) new.register_component(component=rew_instance, weight=weight) return new diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 7c5c93bc..51d0306c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -629,7 +629,7 @@ class PrimaiteGame: for comp, weight in agent.reward_function.reward_components: if isinstance(comp, SharedReward): comp: SharedReward - graph[name].add(comp.agent_name) + graph[name].add(comp.config.agent_name) # while constructing the graph, we might as well set up the reward sharing itself. comp.callback = lambda agent_name: self.agents[agent_name].reward_function.current_reward diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index d4236d1b..6544c82d 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -25,7 +25,7 @@ def test_WebpageUnavailablePenalty(game_and_agent): agent: ControlledAgent schema = WebpageUnavailablePenalty.ConfigSchema(node_hostname="client_1", sticky=True) comp = WebpageUnavailablePenalty(config=schema) - + client_1 = game.simulation.network.get_node_by_hostname("client_1") browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() From 4c2ef6ea2a873d5d7bb965458db00bff21ef1dca Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 6 Nov 2024 14:52:22 +0000 Subject: [PATCH 053/224] #2913: Updated tests --- .../integration_tests/game_layer/test_rewards.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 6544c82d..742b2d35 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -18,7 +18,7 @@ from tests import TEST_ASSETS_ROOT from tests.conftest import ControlledAgent -def test_WebpageUnavailablePenalty(game_and_agent): +def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): """Test that we get the right reward for failing to fetch a website.""" # set up the scenario, configure the web browser to the correct url game, agent = game_and_agent @@ -55,7 +55,7 @@ def test_WebpageUnavailablePenalty(game_and_agent): assert agent.reward_function.current_reward == -0.7 -def test_uc2_rewards(game_and_agent): +def test_uc2_rewards(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): """Test that the reward component correctly applies a penalty when the selected client cannot reach the database.""" game, agent = game_and_agent agent: ControlledAgent @@ -142,8 +142,8 @@ def test_action_penalty_loads_from_config(): act_penalty_obj = comp[0] if act_penalty_obj is None: pytest.fail("Action penalty reward component was not added to the agent from config.") - assert act_penalty_obj.action_penalty == -0.75 - assert act_penalty_obj.do_nothing_penalty == 0.125 + assert act_penalty_obj.config.action_penalty == -0.75 + assert act_penalty_obj.config.do_nothing_penalty == 0.125 def test_action_penalty(): @@ -152,7 +152,7 @@ def test_action_penalty(): # Create an ActionPenalty Reward schema = ActionPenalty.ConfigSchema(action_penalty=-0.75, do_nothing_penalty=0.125) # Penalty = ActionPenalty(action_penalty=-0.75, do_nothing_penalty=0.125) - Penalty = ActionPenalty(schema) + Penalty = ActionPenalty(config=schema) # Assert that penalty is applied if action isn't DONOTHING reward_value = Penalty.calculate( @@ -183,11 +183,12 @@ def test_action_penalty(): assert reward_value == 0.125 -def test_action_penalty_e2e(game_and_agent): +def test_action_penalty_e2e(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): """Test that we get the right reward for doing actions to fetch a website.""" game, agent = game_and_agent agent: ControlledAgent - comp = ActionPenalty(action_penalty=-0.75, do_nothing_penalty=0.125) + schema = ActionPenalty.ConfigSchema(action_penalty=-0.75, do_nothing_penalty=0.125) + comp = ActionPenalty(config=schema) agent.reward_function.register_component(comp, 1.0) From 9d6536fa6aee13858e39957d215061fecce5966f Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 6 Nov 2024 15:08:38 +0000 Subject: [PATCH 054/224] #2913: Pre-commit fix --- docs/source/how_to_guides/extensible_rewards.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/source/how_to_guides/extensible_rewards.rst b/docs/source/how_to_guides/extensible_rewards.rst index 3505d66c..2551eee0 100644 --- a/docs/source/how_to_guides/extensible_rewards.rst +++ b/docs/source/how_to_guides/extensible_rewards.rst @@ -11,10 +11,13 @@ Changes to reward class structure. ================================== Reward classes are inherited from AbstractReward (a sub-class of Pydantic's BaseModel). -Within the reward class is a ConfigSchema class responsible for ensuring config file data is in the -correct format. The `.from_config()` method is generally unchanged. +Within the reward class there is a ConfigSchema class responsible for ensuring config file data is +in the correct format. The `.from_config()` method is generally unchanged but should initialise the +attributes edfined in the ConfigSchema. +Each class requires an identifier string which is used by the ConfigSchema class to verify that it +hasn't previously been added to the registry. -Inheriting from `BaseModel` removes the need for an `__init__` method bu means that object +Inheriting from `BaseModel` removes the need for an `__init__` method but means that object attributes need to be passed by keyword. .. code:: Python @@ -69,4 +72,4 @@ Changes to YAML file. .. code:: YAML There's no longer a need to provide a `dns_server` as an option in the simulation section - of the config file. \ No newline at end of file + of the config file. From e0b885cc79497bf21a010d3c7313d0fbaba0728c Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 7 Nov 2024 13:08:44 +0000 Subject: [PATCH 055/224] #2913: Changes to update test_sticky_rewards.py --- src/primaite/game/agent/rewards.py | 4 +-- .../_game/_agent/test_sticky_rewards.py | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 029597a0..05bca033 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -211,7 +211,7 @@ class WebServer404Penalty(AbstractReward, identifier="WEB_SERVER_404_PENALTY"): return 1.0 if status == 200 else -1.0 if status == 404 else 0.0 self.reward = sum(map(status2rew, codes)) / len(codes) # convert form HTTP codes to rewards and average - elif not self.sticky: # there are no codes, but reward is not sticky, set reward to 0 + elif not self.config.sticky: # there are no codes, but reward is not sticky, set reward to 0 self.reward = 0.0 else: # skip calculating if sticky and no new codes. instead, reuse last step's value pass @@ -278,7 +278,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WEBPAGE_UNAVAILABLE_ elif web_browser_state is NOT_PRESENT_IN_STATE or not web_browser_state["history"]: _LOGGER.debug( "Web browser reward could not be calculated because the web browser history on node", - f"{self.node_hostname} was not reported in the simulation state. Returning 0.0", + f"{self.config.node_hostname} was not reported in the simulation state. Returning 0.0", ) self.reward = 0.0 else: diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 2ad1a322..c758291f 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -11,7 +11,12 @@ from primaite.interface.request import RequestResponse class TestWebServer404PenaltySticky: def test_non_sticky(self): - reward = WebServer404Penalty(node_hostname="computer", service_name="WebService", sticky=False) + schema = WebServer404Penalty.ConfigSchema( + node_hostname="computer", + service_name="WebService", + sticky=False, + ) + reward = WebServer404Penalty(config=schema) # no response codes yet, reward is 0 codes = [] @@ -38,7 +43,12 @@ class TestWebServer404PenaltySticky: assert reward.calculate(state, last_action_response) == -1.0 def test_sticky(self): - reward = WebServer404Penalty(node_hostname="computer", service_name="WebService", sticky=True) + schema = WebServer404Penalty.ConfigSchema( + node_hostname="computer", + service_name="WebService", + sticky=True, + ) + reward = WebServer404Penalty(config=schema) # no response codes yet, reward is 0 codes = [] @@ -67,7 +77,8 @@ class TestWebServer404PenaltySticky: class TestWebpageUnavailabilitySticky: def test_non_sticky(self): - reward = WebpageUnavailablePenalty(node_hostname="computer", sticky=False) + schema = WebpageUnavailablePenalty.ConfigSchema(node_hostname="computer", sticky=False) + reward = WebpageUnavailablePenalty(config=schema) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] @@ -127,7 +138,8 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == -1.0 def test_sticky(self): - reward = WebpageUnavailablePenalty(node_hostname="computer", sticky=True) + schema = WebpageUnavailablePenalty.ConfigSchema(node_hostname="computer", sticky=True) + reward = WebpageUnavailablePenalty(config=schema) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] @@ -188,7 +200,11 @@ class TestWebpageUnavailabilitySticky: class TestGreenAdminDatabaseUnreachableSticky: def test_non_sticky(self): - reward = GreenAdminDatabaseUnreachablePenalty(node_hostname="computer", sticky=False) + schema = GreenAdminDatabaseUnreachablePenalty.ConfigSchema( + node_hostname="computer", + sticky=False, + ) + reward = GreenAdminDatabaseUnreachablePenalty(config=schema) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] @@ -214,7 +230,6 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent did nothing, because reward is not sticky, it goes back to 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] response = RequestResponse(status="success", data={}) - browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response @@ -244,7 +259,11 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == -1.0 def test_sticky(self): - reward = GreenAdminDatabaseUnreachablePenalty(node_hostname="computer", sticky=True) + schema = GreenAdminDatabaseUnreachablePenalty.ConfigSchema( + node_hostname="computer", + sticky=True, + ) + reward = GreenAdminDatabaseUnreachablePenalty(config=schema) # no response codes yet, reward is 0 action, params, request = "DO_NOTHING", {}, ["DONOTHING"] From 02d29f7fb9bb93ebe32c13503ee3e56dfe545369 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 7 Nov 2024 16:35:39 +0000 Subject: [PATCH 056/224] #2913: Updates to How-To guide --- .../how_to_guides/extensible_rewards.rst | 60 +++++++------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/docs/source/how_to_guides/extensible_rewards.rst b/docs/source/how_to_guides/extensible_rewards.rst index 2551eee0..4dd24110 100644 --- a/docs/source/how_to_guides/extensible_rewards.rst +++ b/docs/source/how_to_guides/extensible_rewards.rst @@ -6,65 +6,47 @@ Extensible Rewards ****************** +Extensible Rewards differ from the previous reward mechanism used in PrimAITE v3.x as new reward +types can be added without requiring a change to the RewardFunction class in rewards.py (PrimAITE +core repository). Changes to reward class structure. ================================== Reward classes are inherited from AbstractReward (a sub-class of Pydantic's BaseModel). -Within the reward class there is a ConfigSchema class responsible for ensuring config file data is -in the correct format. The `.from_config()` method is generally unchanged but should initialise the -attributes edfined in the ConfigSchema. +Within the reward class there is a ConfigSchema class responsible for ensuring the config file data +is in the correct format. This also means there is little (if no) requirement for and `__init__` +method. The `.from_config` method is no longer required as it's inherited from `AbstractReward`. Each class requires an identifier string which is used by the ConfigSchema class to verify that it hasn't previously been added to the registry. Inheriting from `BaseModel` removes the need for an `__init__` method but means that object attributes need to be passed by keyword. -.. code:: Python +To add a new reward class follow the example below. Note that the type attribute in the +`ConfigSchema` class should match the type used in the config file to define the reward. -class AbstractReward(BaseModel): - """Base class for reward function components.""" +.. code-block:: Python - class ConfigSchema(BaseModel, ABC): - """Config schema for AbstractReward.""" +class DatabaseFileIntegrity(AbstractReward, identifier="DATABASE_FILE_INTEGRITY"): + """Reward function component which rewards the agent for maintaining the integrity of a database file.""" - type: str + config: "DatabaseFileIntegrity.ConfigSchema" + location_in_state: List[str] = [""] + reward: float = 0.0 - _registry: ClassVar[Dict[str, Type["AbstractReward"]]] = {} + class ConfigSchema(AbstractReward.ConfigSchema): + """ConfigSchema for DatabaseFileIntegrity.""" - def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: - super().__init_subclass__(**kwargs) - if identifier in cls._registry: - raise ValueError(f"Duplicate node adder {identifier}") - cls._registry[identifier] = cls + type: str = "DATABASE_FILE_INTEGRITY" + node_hostname: str + folder_name: str + file_name: str - @classmethod - def from_config(cls, config: Dict) -> "AbstractReward": - """Create a reward function component from a config dictionary. - - :param config: dict of options for the reward component's constructor - :type config: dict - :return: The reward component. - :rtype: AbstractReward - """ - if config["type"] not in cls._registry: - raise ValueError(f"Invalid reward type {config['type']}") - adder_class = cls._registry[config["type"]] - adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config)) - return cls - - @abstractmethod def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the reward for the current state. + pass - :param state: Current simulation state - :type state: Dict - :param last_action_response: Current agent history state - :type last_action_response: AgentHistoryItem state - :return: Reward value - :rtype: float - """ - return 0.0 Changes to YAML file. From d757bd01f0f634965e5999582412801edb177f37 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 12 Nov 2024 14:49:44 +0000 Subject: [PATCH 057/224] #2912 - Updated to include __all__ and some test fixes. New extensible actions documentation page --- .../how_to_guides/extensible_actions.rst | 66 ++++++++ src/primaite/game/agent/actions.py | 26 +++- src/primaite/game/agent/actions/__init__.py | 2 +- src/primaite/game/agent/actions/abstract.py | 20 +-- src/primaite/game/agent/actions/acl.py | 145 +++++++----------- src/primaite/game/agent/actions/config.py | 24 +++ src/primaite/game/agent/actions/file.py | 2 + src/primaite/game/agent/actions/manager.py | 6 +- src/primaite/game/agent/actions/node.py | 10 +- src/primaite/game/agent/actions/session.py | 8 +- .../notebooks/Training-an-RLLib-Agent.ipynb | 4 +- .../configs/firewall_actions_network.yaml | 38 ++--- tests/conftest.py | 2 +- .../actions/test_configure_actions.py | 10 +- .../game_layer/test_actions.py | 36 ++--- 15 files changed, 239 insertions(+), 160 deletions(-) create mode 100644 docs/source/how_to_guides/extensible_actions.rst diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst new file mode 100644 index 00000000..a6c12303 --- /dev/null +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -0,0 +1,66 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _about: + +Extensible Actions +****************** + +Actions defined within PrimAITE have been updated to allow for easier creation of new bespoke actions, without the need to make changes to the ActionManager class within the core PrimAITE repository. + + +Developing Actions for PrimAITE +=============================== + +When developing new actions for PrimAITE, it's important to ensure new actions inherit from the AbstractAction class. This is so that the `ActionManager` has visibility +of the new action through the `AbstractAction` registry attribute. This also removes the need for actions to contain an `__init__` method. + +New actions to be used within PrimAITE require: + +#. **ConfigSchema**: + + This should be a nested class that defines the required configuration items for the new action. + + .. code-block:: python + + class ExampleAction(AbstractAction, identifier="Example_action"): + + class ConfigSchema(AbstractAction.ConfigSchema): + target_application: str + + The ConfigSchema is used when the class is called to form the action. + + +#. **Unique Identifier**: + + New actions should have a Unique identifier when declared. This is used by the `ActionManager` when forming/processing action commands from agents. See the example code block in ConfigSchema for how this should be implemented. + +#. **form_request method**: + + New actions need a `form_request()` method, to convert the action into a ``Requestformat`` that can be ingested by PrimAITE's `RequestManager`. + The below is an example of how this is done, taken from the `NodeFolderCreateAction`. + + .. code-block:: python + + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + if config.node_name is None or config.folder_name is None: + return ["do_nothing"] + return [ + "network", + "node", + config.node_name, + "file_system", + cls.model_fields["verb"].default, + "folder", + config.folder_name, + ] + +There is no longer a need for a `from_config()` method to be defined within new actions, as this is handled within the base `AbstractAction` class. + +Changes to YAML file. +===================== + +Action identifiers now follow the snake_case naming style, instead of the MACRO_CASE that has been seen previously. Please review any custom YAML files for any issues seen. This should be backwards compatible. diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 68e42fb1..885e0238 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -175,6 +175,10 @@ class NodeApplicationInstallAction(AbstractAction): class ConfigureDatabaseClientAction(AbstractAction): """Action which sets config parameters for a database client on a node.""" + model_config = ConfigDict(extra="forbid") + server_ip_address: Optional[str] = None + server_password: Optional[str] = None + class _Opts(BaseModel): """Schema for options that can be passed to this action.""" @@ -197,9 +201,10 @@ class ConfigureDatabaseClientAction(AbstractAction): class ConfigureRansomwareScriptAction(AbstractAction): """Action which sets config parameters for a ransomware script on a node.""" - class _Opts(BaseModel): + class _Opts(BaseModel, AbstractAction.ConfigSchema): """Schema for options that can be passed to this option.""" + node_name: str model_config = ConfigDict(extra="forbid") server_ip_address: Optional[str] = None server_password: Optional[str] = None @@ -208,13 +213,20 @@ class ConfigureRansomwareScriptAction(AbstractAction): def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) - def form_request(self, node_id: int, config: Dict) -> RequestFormat: + def form_request(self, config: _Opts) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: + if config.node_name is None: return ["do_nothing"] ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "RansomwareScript", "configure", config] + return [ + "network", + "node", + config.node_name, + "application", + "RansomwareScript", + "configure", + config.model_config, + ] class ConfigureDoSBotAction(AbstractAction): @@ -285,8 +297,8 @@ class NodeFolderAbstractAction(AbstractAction): class NodeFolderScanAction(NodeFolderAbstractAction): """Action which scans a folder.""" - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) + def __init__(self, manager: "ActionManager", node_name: str, folder_name, **kwargs) -> None: + super().__init__(manager, node_name=node_name, folder_name=folder_name, **kwargs) self.verb: str = "scan" diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 3540b128..016a09ba 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -17,8 +17,8 @@ from primaite.game.agent.actions import ( from primaite.game.agent.actions.manager import ActionManager __all__ = ( - "acl", "abstract", + "acl", "application", "config", "file", diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index b96b14c9..2ed168d9 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -8,17 +8,18 @@ from pydantic import BaseModel, ConfigDict from primaite.interface.request import RequestFormat +# notes: +# we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. +# all the init methods in the old actions are just used for holding a verb and shape, which are not really used. +# the config schema should be used to the actual parameters for formatting the action itself. +# (therefore there's no need for creating action instances, just the action class contains logic for converting +# CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be +# instantiated.) + class AbstractAction(BaseModel): """Base class for actions.""" - # notes: - # we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. - # all the init methods in the old actions are just used for holding a verb and shape, which are not really used. - # the config schema should be used to the actual parameters for formatting the action itself. - # (therefore there's no need for creating action instances, just the action class contains logic for converting - # CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be - # instantiated.) class ConfigSchema(BaseModel, ABC): """Base configuration schema for Actions.""" @@ -41,12 +42,7 @@ class AbstractAction(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAction": """Create an action component from a config dictionary.""" - # set attributes for action based off config dict - # if config["type"] not in cls._registry: - # raise ValueError(f"Invalid action reward type {config['type']}") - for attribute, value in config.items(): if not hasattr(cls.ConfigSchema, attribute): setattr(cls.ConfigSchema, attribute, value) - return cls diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 8f5a79da..72a0b262 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -17,27 +17,35 @@ __all__ = ( class ACLAbstractAction(AbstractAction, identifier="acl_abstract_action"): """Base class for ACL actions.""" - class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema base for ACL abstract actions.""" - - class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" + target_router: str + position: int + permission: Literal[1, 2] + src_ip: str + source_wildcard_id: int + source_port: str + dst_ip: str + dst_wildcard: int + dst_port: int + protocol_name: str + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for RouterACLAddRuleAction.""" target_router: str position: int permission: Literal[1, 2] - source_ip_id: int - source_wildcard_id: int - source_port_id: int + src_ip: str + src_wildcard: int + source_port: str dst_ip: str - dst_wildcard_id: int + dst_wildcard: int dst_port: int protocol_name: str @@ -50,95 +58,54 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """At what position to add the rule, must be specified.""" permission: Literal[1, 2] """Whether to allow or deny traffic, must be specified. 1 = PERMIT, 2 = DENY.""" - source_ip_id: int = Field(default=1, ge=1) + src_ip: str """Rule source IP address. By default, all ip addresses.""" source_wildcard_id: int = Field(default=0, ge=0) """Rule source IP wildcard. By default, use the wildcard at index 0 from action manager.""" - source_port_id: int = Field(default=1, ge=1) + source_port: int = Field(default=1, ge=1) """Rule source port. By default, all source ports.""" dst_ip_id: int = Field(default=1, ge=1) """Rule destination IP address. By default, all ip addresses.""" - dst_wildcard_id: int = Field(default=0, ge=0) + dst_wildcard: int = Field(default=0, ge=0) """Rule destination IP wildcard. By default, use the wildcard at index 0 from action manager.""" dst_port_id: int = Field(default=1, ge=1) """Rule destination port. By default, all destination ports.""" protocol_name: str = "ALL" """Rule protocol. By default, all protocols.""" - @field_validator( - "source_ip_id", - "source_port_id", - "source_wildcard_id", - "dest_ip_id", - "dest_port_id", - "dest_wildcard_id", - "protocol_name", - mode="before", - ) - @classmethod - def not_none(cls, v: str, info: ValidationInfo) -> int: - """If None is passed, use the default value instead.""" - if v is None: - return cls.model_fields[info.field_name].default - return v + # @field_validator( + # "source_ip_id", + # "source_port_id", + # "source_wildcard_id", + # "dest_ip_id", + # "dest_port_id", + # "dest_wildcard_id", + # "protocol_name", + # mode="before", + # ) + # @classmethod + # def not_none(cls, v: str, info: ValidationInfo) -> int: + # """If None is passed, use the default value instead.""" + # if v is None: + # return cls.model_fields[info.field_name].default + # return v @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" # Validate incoming data. - parsed_options = RouterACLAddRuleAction.ACLRuleOptions( - target_router=config.target_router, - position=config.position, - permission=config.permission, - source_ip_id=config.source_ip_id, - source_wildcard_id=config.source_wildcard_id, - dest_ip_id=config.dst_ip, - dest_wildcard_id=config.dest_wildcard_id, - source_port_id=config.source_port_id, - dest_port_id=config.dst_port_id, - protocol=config.protocol_name, - ) - if parsed_options.permission == 1: - permission_str = "PERMIT" - elif parsed_options.permission == 2: - permission_str = "DENY" - # else: - # _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") - - if parsed_options.protocol_name == "ALL": - protocol = "ALL" - else: - protocol = parsed_options.protocol_name - # subtract 2 to account for UNUSED=0 and ALL=1. - - if parsed_options.source_ip_id == 1: - src_ip = "ALL" - else: - src_ip = cls.manager.get_ip_address_by_idx(parsed_options.source_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - src_wildcard = cls.manager.get_wildcard_by_idx(parsed_options.source_wildcard_id) - - if parsed_options.source_port_id == 1: - src_port = "ALL" - else: - src_port = cls.manager.get_port_by_idx(parsed_options.source_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - if parsed_options.dest_ip_id == 1: - dst_ip = "ALL" - else: - dst_ip = cls.manager.get_ip_address_by_idx(parsed_options.dest_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - dst_ip=config.dst_ip - - dst_wildcard = config.dest_wildcard_id - - if parsed_options.dest_port_id == 1: - dst_port = "ALL" - else: - dst_port = cls.manager.get_port_by_idx(parsed_options.dest_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 + # parsed_options = RouterACLAddRuleAction.ACLRuleOptions( + # target_router=config.target_router, + # position=config.position, + # permission=config.permission, + # src_ip=config.src_ip, + # source_wildcard_id=config.source_wildcard_id, + # dst_ip_id=config.dst_ip, + # dst_wildcard=config.dst_wildcard, + # source_port_id=config.source_port_id, + # dest_port=config.dst_port, + # protocol=config.protocol_name, + # ) return [ "network", @@ -146,11 +113,11 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): config.target_router, "acl", "add_rule", - config.permission_str, - protocol, - str(src_ip), + config.permission, + config.protocol_name, + config.src_ip, config.src_wildcard, - config.src_port, + config.source_port, str(config.dst_ip), config.dst_wildcard, config.dst_port, @@ -193,7 +160,6 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r num_permissions: int = 3 permission: str - def __init__( self, manager: "ActionManager", @@ -228,10 +194,8 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r "protocol_id": num_protocols, } - - @classmethod - def form_request(cls, config:ConfigSchema) -> List[str]: + def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.permission == 0: permission_str = "UNUSED" @@ -260,6 +224,7 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r else: # src_ip = self.manager.get_ip_address_by_idx(source_ip_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 + pass if config.source_port_id == 0: return ["do_nothing"] # invalid formulation @@ -286,8 +251,8 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r else: # dst_port = self.manager.get_port_by_idx(dest_port_id - 2) # subtract 2 to account for UNUSED=0, and ALL=1 - # src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) - # dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) + # src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) + # dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) pass return [ @@ -298,7 +263,7 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r config.firewall_port_direction, "acl", "add_rule", - permission_str, + config.permission, protocol, str(src_ip), config.src_wildcard, diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index beda8f27..a4247e21 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -15,6 +15,7 @@ __all__ = ( "TerminalC2ServerAction", "RansomwareLaunchC2ServerAction", "ExfiltrationC2ServerAction", + "ConfigureDatabaseClientAction", ) @@ -230,3 +231,26 @@ class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfi } ExfiltrationC2ServerAction._Opts.model_validate(command_model) return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] + + +class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_database_client"): + """Action which sets config parameters for a database client on a node.""" + + node_name: str + model_config: ConfigDict = ConfigDict(extra="forbid") + + class ConfigSchema(AbstractAction.ConfigSchema): + """Schema for options that can be passed to this action.""" + + node_name: str + model_config = ConfigDict(extra="forbid") + + def __init__(self, manager: "ActionManager", **kwargs) -> None: + super().__init__(manager=manager) + + def form_request(self, config: ConfigSchema) -> RequestFormat: + """Return the action formatted as a request that can be ingested by the simulation.""" + + if config.node_name is None: + return ["do_nothing"] + return ["network", "node", config.node_name, "application", "DatabaseClient", "configure", config.model_config] diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index 6d1f95b9..c5ba1602 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -11,6 +11,8 @@ __all__ = ( "NodeFileRestoreAction", "NodeFileCorruptAction", "NodeFileAccessAction", + "NodeFileCheckhashAction", + "NodeFileRepairAction", ) diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 26814bf2..764b5b6e 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -24,6 +24,8 @@ from primaite.interface.request import RequestFormat # TODO: Make sure that actions are backwards compatible where the old YAML format is used. +__all__ = "DoNothingAction" + class DoNothingAction(AbstractAction, identifier="do_nothing"): """Do Nothing Action.""" @@ -69,7 +71,7 @@ class ActionManager: Action mapping that converts an integer to a specific action and parameter choice. For example : - {0: ("NODE_SERVICE_SCAN", {node_id:0, service_id:2})} + {0: ("node_service_scan", {node_name:"client_1", service_name:"WebBrowser"})} """ if act_map is None: # raise RuntimeError("Action map must be specified in the config file.") @@ -147,7 +149,7 @@ class ActionManager: Since the agent uses a discrete action space which acts as a flattened version of the component-based action space, action_map provides a mapping between an integer (chosen by the agent) and a meaningful action and values of parameters. For example action 0 can correspond to do nothing, action 1 can - correspond to "NODE_SERVICE_SCAN" with ``node_id=1`` and ``service_id=1``, action 2 can be " + correspond to "node_service_scan" with ``node_name="server"`` and ``service_name="WebBrowser"``, action 2 can be " 3. ``options`` ``options`` contains a dictionary of options which are passed to the ActionManager's __init__ method. These options are used to calculate the shape of the action space, and to provide additional information diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 29833b15..ab4209e5 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -5,7 +5,15 @@ from typing import ClassVar, List, Optional, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat -__all__ = ("NodeOSScanAction", "NodeShutdownAction", "NodeStartupAction", "NodeResetAction") +__all__ = ( + "NodeOSScanAction", + "NodeShutdownAction", + "NodeStartupAction", + "NodeResetAction", + "NodeNMAPPingScanAction", + "NodeNMAPPortScanAction", + "NodeNetworkServiceReconAction", +) class NodeAbstractAction(AbstractAction, identifier="node_abstract"): diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 76b97cbd..79ff0705 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -4,7 +4,11 @@ from abc import abstractmethod from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat -__all__ = ("NodeSessionsRemoteLoginAction", "NodeSessionsRemoteLogoutAction") +__all__ = ( + "NodeSessionsRemoteLoginAction", + "NodeSessionsRemoteLogoutAction", + "NodeAccountChangePasswordAction", +) class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstract"): @@ -73,7 +77,7 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip] -class NodeAccountsChangePasswordAction(NodeSessionAbstractAction, identifier="node_accounts_change_password"): +class NodeAccountChangePasswordAction(NodeSessionAbstractAction, identifier="node_account_change_password"): """Action which changes the password for a user.""" username: str diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index dbe8871c..0fd212f2 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -95,7 +95,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -109,7 +109,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 18e27de7..88b09a29 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -97,16 +97,16 @@ agents: action_space: action_list: - type: do_nothing - - type: FIREWALL_ACL_ADDRULE firewall_acl_add_rule - - type: FIREWALL_ACL_REMOVERULE - - type: NETWORK_PORT_DISABLE - - type: NETWORK_PORT_ENABLE + - type: firewall_acl_add_rule + - type: firewall_acl_remove_rule + - type: network_port_disable + - type: network_port_enable action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: FIREWALL_ACL_ADDRULE + action: firewall_acl_add_rule options: target_firewall_nodename: firewall firewall_port_name: internal @@ -121,14 +121,14 @@ agents: source_wildcard_id: 0 dest_wildcard_id: 0 2: - action: FIREWALL_ACL_REMOVERULE + action: firewall_acl_remove_rule options: target_firewall_nodename: firewall firewall_port_name: internal firewall_port_direction: inbound position: 1 3: - action: FIREWALL_ACL_ADDRULE + action: firewall_acl_add_rule options: target_firewall_nodename: firewall firewall_port_name: internal @@ -143,14 +143,14 @@ agents: source_wildcard_id: 0 dest_wildcard_id: 0 4: - action: FIREWALL_ACL_REMOVERULE + action: firewall_acl_remove_rule options: target_firewall_nodename: firewall firewall_port_name: internal firewall_port_direction: outbound position: 1 5: - action: FIREWALL_ACL_ADDRULE + action: firewall_acl_add_rule options: target_firewall_nodename: firewall firewall_port_name: dmz @@ -165,14 +165,14 @@ agents: source_wildcard_id: 0 dest_wildcard_id: 0 6: - action: FIREWALL_ACL_REMOVERULE + action: firewall_acl_remove_rule options: target_firewall_nodename: firewall firewall_port_name: dmz firewall_port_direction: inbound position: 1 7: - action: FIREWALL_ACL_ADDRULE + action: firewall_acl_add_rule options: target_firewall_nodename: firewall firewall_port_name: dmz @@ -187,14 +187,14 @@ agents: source_wildcard_id: 0 dest_wildcard_id: 0 8: - action: FIREWALL_ACL_REMOVERULE + action: firewall_acl_remove_rule options: target_firewall_nodename: firewall firewall_port_name: dmz firewall_port_direction: outbound position: 2 9: - action: FIREWALL_ACL_ADDRULE + action: firewall_acl_add_rule options: target_firewall_nodename: firewall firewall_port_name: external @@ -209,14 +209,14 @@ agents: source_wildcard_id: 0 dest_wildcard_id: 0 10: - action: FIREWALL_ACL_REMOVERULE + action: firewall_acl_remove_rule options: target_firewall_nodename: firewall firewall_port_name: external firewall_port_direction: inbound position: 10 11: - action: FIREWALL_ACL_ADDRULE + action: firewall_acl_add_rule options: target_firewall_nodename: firewall firewall_port_name: external @@ -231,19 +231,19 @@ agents: source_wildcard_id: 0 dest_wildcard_id: 0 12: - action: FIREWALL_ACL_REMOVERULE + action: firewall_acl_remove_rule options: target_firewall_nodename: firewall firewall_port_name: external firewall_port_direction: outbound position: 1 13: - action: NETWORK_PORT_DISABLE + action: network_port_disable options: target_nodename: firewall port_id: 3 14: - action: NETWORK_PORT_ENABLE + action: network_port_enable options: target_nodename: firewall port_id: 3 diff --git a/tests/conftest.py b/tests/conftest.py index 8bfc78a4..bd1b79ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -463,7 +463,7 @@ def game_and_agent(): {"type": "c2_server_ransomware_configure"}, {"type": "c2_server_terminal_command"}, {"type": "c2_server_data_exfiltrate"}, - {"type": "node_accounts_change_password"}, + {"type": "node_account_change_password"}, {"type": "node_session_remote_login"}, {"type": "node_session_remote_logoff"}, {"type": "node_send_remote_command"}, diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 508bd5a4..7bf45fb4 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -4,7 +4,7 @@ from ipaddress import IPv4Address import pytest from pydantic import ValidationError -from primaite.game.agent.actions import ( +from primaite.game.agent.actions.config import ( ConfigureDatabaseClientAction, ConfigureDoSBotAction, ConfigureRansomwareScriptAction, @@ -35,10 +35,10 @@ class TestConfigureDatabaseAction: db_client: DatabaseClient = client_1.software_manager.software["DatabaseClient"] action = ( - "CONFIGURE_DATABASE_CLIENT", + "configure_database_client", { - "node_id": 0, - "config": { + "node_name": "client_1", + "model_config": { "server_ip_address": "192.168.1.99", "server_password": "admin123", }, @@ -53,7 +53,7 @@ class TestConfigureDatabaseAction: def test_configure_ip(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["CONFIGURE_DATABASE_CLIENT"] = ConfigureDatabaseClientAction(agent.action_manager) + agent.action_manager.actions["configure_database_client"] = ConfigureDatabaseClientAction(agent.action_manager) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 21aa31de..baa4c725 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -124,15 +124,15 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox "router_acl_add_rule", { "target_router": "router", - "position": 4, # 4th rule - "permission": 2, # DENY - "source_ip_id": 3, # 10.0.1.2 (client_1) - "dest_ip_id": 6, # 10.0.2.3 (server_2) - "dest_port_id": 1, # ALL - "source_port_id": 1, # ALL - "protocol_name": "ALL", # ALL - "source_wildcard_id": 0, - "dest_wildcard_id": 0, + "position": 4, + "permission": "DENY", + "src_ip": "10.0.1.2", + "src_wildcard": 0, + "source_port": "ALL", + "dst_ip": "10.0.2.3", + "dst_wildcard": 0, + "dst_port": "ALL", + "protocol_name": "ALL", }, ) agent.store_action(action) @@ -148,18 +148,18 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox # 4: Add a rule to block server_1 from reaching server_2 on router (this should not affect comms as they are on same subnet) action = ( - "ROUTER_ACL_ADDRULE", + "router_acl_add_rule", { "target_router": "router", "position": 5, # 5th rule - "permission": 2, # DENY - "source_ip_id": 5, # 10.0.2.2 (server_1) - "dest_ip_id": 6, # 10.0.2.3 (server_2) - "dest_port_id": 1, # ALL - "source_port_id": 1, # ALL - "protocol_id": 1, # ALL - "source_wildcard_id": 0, - "dest_wildcard_id": 0, + "permission": "DENY", # DENY + "src_ip": "10.0.2.2", # 10.0.2.2 (server_1) + "src_wildcard": 0, + "source_port": "ALL", # ALL + "dst_ip": "10.0.2.3", # 10.0.2.3 (server_2) + "dst_wildcard": 0, + "dst_port": "ALL", # ALL + "protocol_name": "ALL", # ALL }, ) agent.store_action(action) From ed020f005fde98504749e0060241a512dcc83ce4 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 13 Nov 2024 10:40:51 +0000 Subject: [PATCH 058/224] #2912 - Pre-commit updates ahead of first draft PR. --- .../how_to_guides/extensible_actions.rst | 2 +- src/primaite/game/agent/actions.py | 2 +- src/primaite/game/agent/actions/acl.py | 174 ++---------------- src/primaite/game/agent/actions/config.py | 1 - src/primaite/game/agent/actions/file.py | 2 +- src/primaite/game/agent/actions/manager.py | 3 +- src/primaite/game/agent/actions/node.py | 8 + src/primaite/game/agent/actions/session.py | 4 +- .../game_layer/test_actions.py | 1 + 9 files changed, 36 insertions(+), 161 deletions(-) diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index a6c12303..bd78c8e1 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -29,7 +29,7 @@ New actions to be used within PrimAITE require: class ConfigSchema(AbstractAction.ConfigSchema): target_application: str - The ConfigSchema is used when the class is called to form the action. + The ConfigSchema is used when the class is called to form the action, within the `form_request` method, detailed below. #. **Unique Identifier**: diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 885e0238..02134650 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -297,7 +297,7 @@ class NodeFolderAbstractAction(AbstractAction): class NodeFolderScanAction(NodeFolderAbstractAction): """Action which scans a folder.""" - def __init__(self, manager: "ActionManager", node_name: str, folder_name, **kwargs) -> None: + def __init__(self, manager: "ActionManager", node_name: str, folder_name: str, **kwargs) -> None: super().__init__(manager, node_name=node_name, folder_name=folder_name, **kwargs) self.verb: str = "scan" diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 72a0b262..d6d5f4b4 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,9 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, List, Literal +from typing import List, Literal -from pydantic import BaseModel, Field, field_validator, ValidationInfo - -from primaite.game.agent.actions.manager import AbstractAction, ActionManager +from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat __all__ = ( @@ -20,6 +18,9 @@ class ACLAbstractAction(AbstractAction, identifier="acl_abstract_action"): class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema base for ACL abstract actions.""" + src_ip: str + protocol_name: str + class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" @@ -27,13 +28,11 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): target_router: str position: int permission: Literal[1, 2] - src_ip: str source_wildcard_id: int source_port: str dst_ip: str dst_wildcard: int dst_port: int - protocol_name: str class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for RouterACLAddRuleAction.""" @@ -47,66 +46,10 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): dst_ip: str dst_wildcard: int dst_port: int - protocol_name: str - - class ACLRuleOptions(BaseModel): - """Validator for ACL_ADD_RULE options.""" - - target_router: str - """On which router to add the rule, must be specified.""" - position: int - """At what position to add the rule, must be specified.""" - permission: Literal[1, 2] - """Whether to allow or deny traffic, must be specified. 1 = PERMIT, 2 = DENY.""" - src_ip: str - """Rule source IP address. By default, all ip addresses.""" - source_wildcard_id: int = Field(default=0, ge=0) - """Rule source IP wildcard. By default, use the wildcard at index 0 from action manager.""" - source_port: int = Field(default=1, ge=1) - """Rule source port. By default, all source ports.""" - dst_ip_id: int = Field(default=1, ge=1) - """Rule destination IP address. By default, all ip addresses.""" - dst_wildcard: int = Field(default=0, ge=0) - """Rule destination IP wildcard. By default, use the wildcard at index 0 from action manager.""" - dst_port_id: int = Field(default=1, ge=1) - """Rule destination port. By default, all destination ports.""" - protocol_name: str = "ALL" - """Rule protocol. By default, all protocols.""" - - # @field_validator( - # "source_ip_id", - # "source_port_id", - # "source_wildcard_id", - # "dest_ip_id", - # "dest_port_id", - # "dest_wildcard_id", - # "protocol_name", - # mode="before", - # ) - # @classmethod - # def not_none(cls, v: str, info: ValidationInfo) -> int: - # """If None is passed, use the default value instead.""" - # if v is None: - # return cls.model_fields[info.field_name].default - # return v @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - # Validate incoming data. - # parsed_options = RouterACLAddRuleAction.ACLRuleOptions( - # target_router=config.target_router, - # position=config.position, - # permission=config.permission, - # src_ip=config.src_ip, - # source_wildcard_id=config.source_wildcard_id, - # dst_ip_id=config.dst_ip, - # dst_wildcard=config.dst_wildcard, - # source_port_id=config.source_port_id, - # dest_port=config.dst_port, - # protocol=config.protocol_name, - # ) - return [ "network", "node", @@ -118,7 +61,7 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): config.src_ip, config.src_wildcard, config.source_port, - str(config.dst_ip), + config.dst_ip, config.dst_wildcard, config.dst_port, config.position, @@ -160,63 +103,11 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r num_permissions: int = 3 permission: str - def __init__( - self, - manager: "ActionManager", - max_acl_rules: int, - num_ips: int, - num_ports: int, - num_protocols: int, - **kwargs, - ) -> None: - """Init method for FirewallACLAddRuleAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - :param num_ips: Number of IP addresses in the simulation. - :type num_ips: int - :param num_ports: Number of ports in the simulation. - :type num_ports: int - :param num_protocols: Number of protocols in the simulation. - :type num_protocols: int - """ - super().__init__(manager=manager) - num_permissions = 3 - self.shape: Dict[str, int] = { - "position": max_acl_rules, - "permission": num_permissions, - "source_ip_id": num_ips, - "dest_ip_id": num_ips, - "source_port_id": num_ports, - "dest_port_id": num_ports, - "protocol_id": num_protocols, - } - @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.permission == 0: - permission_str = "UNUSED" - return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - elif config.permission == 1: - permission_str = "PERMIT" - elif config.permission == 2: - permission_str = "DENY" - # else: - # _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") - if config.protocol_id == 0: return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - - if config.protocol_id == 1: - protocol = "ALL" - else: - # protocol = self.manager.get_internet_protocol_by_idx(protocol_id - 2) - # subtract 2 to account for UNUSED=0 and ALL=1. - pass - if config.source_ip_id == 0: return ["do_nothing"] # invalid formulation elif config.source_ip_id == 1: @@ -235,26 +126,6 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r # subtract 2 to account for UNUSED=0, and ALL=1 pass - if config.dest_ip_id == 0: - return ["do_nothing"] # invalid formulation - elif config.dest_ip_id == 1: - dst_ip = "ALL" - else: - # dst_ip = self.manager.get_ip_address_by_idx(dest_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - pass - - if config.dest_port_id == 0: - return ["do_nothing"] # invalid formulation - elif config.dest_port_id == 1: - dst_port = "ALL" - else: - # dst_port = self.manager.get_port_by_idx(dest_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - # src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) - # dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) - pass - return [ "network", "node", @@ -264,13 +135,13 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r "acl", "add_rule", config.permission, - protocol, + config.protocol_name, str(src_ip), config.src_wildcard, src_port, - str(dst_ip), + config.dst_ip, config.dst_wildcard, - dst_port, + config.dst_port, config.position, ] @@ -278,29 +149,24 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r class FirewallACLRemoveRuleAction(AbstractAction, identifier="firewall_acl_remove_rule"): """Action which removes a rule from a firewall port's ACL.""" - def __init__(self, manager: "ActionManager", max_acl_rules: int, **kwargs) -> None: - """Init method for RouterACLRemoveRuleAction. + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for FirewallACLRemoveRuleAction.""" - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"position": max_acl_rules} + target_firewall_nodename: str + firewall_port_name: str + firewall_port_direction: str + position: int @classmethod - def form_request( - cls, target_firewall_nodename: str, firewall_port_name: str, firewall_port_direction: str, position: int - ) -> List[str]: + def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", "node", - target_firewall_nodename, - firewall_port_name, - firewall_port_direction, + config.target_firewall_nodename, + config.firewall_port_name, + config.firewall_port_direction, "acl", "remove_rule", - position, + config.position, ] diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index a4247e21..d7b436d7 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -250,7 +250,6 @@ class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_databa def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - if config.node_name is None: return ["do_nothing"] return ["network", "node", config.node_name, "application", "DatabaseClient", "configure", config.model_config] diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index c5ba1602..5d12b27a 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -178,7 +178,7 @@ class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_chec class NodeFileRepairAction(NodeFileAbstractAction, identifier="node_file_repair"): - """Action which repairs a file""" + """Action which repairs a file.""" verb: str = "repair" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 764b5b6e..6c8353b0 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -149,7 +149,8 @@ class ActionManager: Since the agent uses a discrete action space which acts as a flattened version of the component-based action space, action_map provides a mapping between an integer (chosen by the agent) and a meaningful action and values of parameters. For example action 0 can correspond to do nothing, action 1 can - correspond to "node_service_scan" with ``node_name="server"`` and ``service_name="WebBrowser"``, action 2 can be " + correspond to "node_service_scan" with ``node_name="server"`` and + ``service_name="WebBrowser"``, action 2 can be " 3. ``options`` ``options`` contains a dictionary of options which are passed to the ActionManager's __init__ method. These options are used to calculate the shape of the action space, and to provide additional information diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index ab4209e5..a69a8a5f 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -89,7 +89,11 @@ class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_acti class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_scan"): + """Action which performs an NMAP ping scan.""" + class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): + """Configuration schema for NodeNMAPPingScanAction.""" + pass @classmethod @@ -110,6 +114,8 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_ """Action which performs an NMAP port scan.""" class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): + """Configuration Schema for NodeNMAPPortScanAction.""" + source_node: str target_protocol: Optional[Union[str, List[str]]] = (None,) target_port: Optional[Union[str, List[str]]] = (None,) @@ -141,6 +147,8 @@ class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, identifier="node_net """Action which performs an NMAP network service recon (ping scan followed by port scan).""" class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for NodeNetworkServiceReconAction.""" + target_protocol: Optional[Union[str, List[str]]] = (None,) target_port: Optional[Union[str, List[str]]] = (None,) show: Optional[bool] = (False,) diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 79ff0705..dcae8b47 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -67,14 +67,14 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLogoutAction.""" - pass + verb: str = "remote_logoff" @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.remote_ip is None: return ["do_nothing"] - return ["network", "node", config.node_name, "service", "Terminal", "remote_logoff", config.remote_ip] + return ["network", "node", config.node_name, "service", "Terminal", config.verb, config.remote_ip] class NodeAccountChangePasswordAction(NodeSessionAbstractAction, identifier="node_account_change_password"): diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index baa4c725..f380ba7d 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -166,6 +166,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox game.step() # 5: Check that the ACL now has 6 rules, but that server_1 can still ping server_2 + print(router.acl.show()) assert router.acl.num_rules == 6 assert server_1.ping("10.0.2.3") # Can ping server_2 From 95fbe451371dbc6a96bf5c7dc9b9a2a1ccdba4a9 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 13 Nov 2024 15:32:48 +0000 Subject: [PATCH 059/224] #2912 - Updates so that all tests within test_actions.py pass --- src/primaite/game/agent/actions.py | 5 -- src/primaite/game/agent/actions/abstract.py | 3 +- src/primaite/game/agent/actions/acl.py | 37 +++++---- src/primaite/game/agent/actions/config.py | 2 +- .../configs/firewall_actions_network.yaml | 76 +++++++++---------- .../game_layer/test_actions.py | 9 ++- 6 files changed, 63 insertions(+), 69 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 02134650..64cbe0cf 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -175,10 +175,6 @@ class NodeApplicationInstallAction(AbstractAction): class ConfigureDatabaseClientAction(AbstractAction): """Action which sets config parameters for a database client on a node.""" - model_config = ConfigDict(extra="forbid") - server_ip_address: Optional[str] = None - server_password: Optional[str] = None - class _Opts(BaseModel): """Schema for options that can be passed to this action.""" @@ -204,7 +200,6 @@ class ConfigureRansomwareScriptAction(AbstractAction): class _Opts(BaseModel, AbstractAction.ConfigSchema): """Schema for options that can be passed to this option.""" - node_name: str model_config = ConfigDict(extra="forbid") server_ip_address: Optional[str] = None server_password: Optional[str] = None diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 2ed168d9..c18f0dbc 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -43,6 +43,5 @@ class AbstractAction(BaseModel): def from_config(cls, config: Dict) -> "AbstractAction": """Create an action component from a config dictionary.""" for attribute, value in config.items(): - if not hasattr(cls.ConfigSchema, attribute): - setattr(cls.ConfigSchema, attribute, value) + setattr(cls.ConfigSchema, attribute, value) return cls diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index d6d5f4b4..3beface9 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import List, Literal +from typing import List, Literal, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -92,6 +92,12 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r num_protocols: int num_permissions: int = 3 permission: str + target_firewall_nodename: str + src_ip: str + dst_ip: str + dst_wildcard: str + src_port: Union[int| None] + dst_port: Union[int | None] class ConfigSchema(ACLAbstractAction.ConfigSchema): """Configuration schema for FirewallACLAddRuleAction.""" @@ -102,29 +108,22 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r num_protocols: int num_permissions: int = 3 permission: str + target_firewall_nodename: str + src_ip: str + dst_ip: str + dst_wildcard: str + src_port: Union[int| None] @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.protocol_id == 0: + if config.protocol_name == None: return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - if config.source_ip_id == 0: + if config.src_ip == 0: return ["do_nothing"] # invalid formulation - elif config.source_ip_id == 1: - src_ip = "ALL" - else: - # src_ip = self.manager.get_ip_address_by_idx(source_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - pass + if config.src_port == 0: + return ["do_nothing"] # invalid configuration. - if config.source_port_id == 0: - return ["do_nothing"] # invalid formulation - elif config.source_port_id == 1: - src_port = "ALL" - else: - # src_port = self.manager.get_port_by_idx(source_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - pass return [ "network", @@ -136,9 +135,9 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r "add_rule", config.permission, config.protocol_name, - str(src_ip), + config.src_ip, config.src_wildcard, - src_port, + config.src_port, config.dst_ip, config.dst_wildcard, config.dst_port, diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index d7b436d7..dc7e98b9 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -37,7 +37,7 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_rans if config.node_name is None: return ["do_nothing"] ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", config] + return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", config.model_config] class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 88b09a29..a2b75be5 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -112,14 +112,14 @@ agents: firewall_port_name: internal firewall_port_direction: inbound position: 1 - permission: 1 - source_ip_id: 2 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: PERMIT + src_ip: 192.168.0.10 + dst_ip: ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: 0 + dst_wildcard: 0 2: action: firewall_acl_remove_rule options: @@ -134,12 +134,12 @@ agents: firewall_port_name: internal firewall_port_direction: outbound position: 1 - permission: 2 - source_ip_id: 2 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 2 - dest_port_id: 3 - protocol_id: 2 + permission: DENY + src_ip: 192.168.0.10 # client 1 + dest_ip: ALL # ALL + src_port: ARP + dst_port: DNS + protocol_name: ICMP source_wildcard_id: 0 dest_wildcard_id: 0 4: @@ -156,12 +156,12 @@ agents: firewall_port_name: dmz firewall_port_direction: inbound position: 1 - permission: 2 - source_ip_id: 3 # dmz_server - dest_ip_id: 2 # client_1 - source_port_id: 4 - dest_port_id: 4 - protocol_id: 4 + permission: DENY + src_ip: 192.168.10.10 # dmz_server + dst_ip: 192.168.0.10 # client_1 + src_port: HTTP + dst_port: HTTP + protocol_name: UDP source_wildcard_id: 0 dest_wildcard_id: 0 6: @@ -178,12 +178,12 @@ agents: firewall_port_name: dmz firewall_port_direction: outbound position: 2 - permission: 2 - source_ip_id: 3 # dmz_server - dest_ip_id: 2 # client_1 - source_port_id: 4 - dest_port_id: 4 - protocol_id: 3 + permission: DENY + src_ip: 192.168.10.10 # dmz_server + dst_ip: 192.168.0.10 # client_1 + src_port: HTTP + dst_port: HTTP + protocol_name: TCP source_wildcard_id: 0 dest_wildcard_id: 0 8: @@ -200,12 +200,12 @@ agents: firewall_port_name: external firewall_port_direction: inbound position: 10 - permission: 2 - source_ip_id: 4 # external_computer - dest_ip_id: 3 # dmz - source_port_id: 5 - dest_port_id: 5 - protocol_id: 2 + permission: DENY + src_ip: 192.168.20.10 # external_computer + dst_ip: 192.168.10.10 # dmz + src_port: POSTGRES_SERVER + dst_port: POSTGRES_SERVER + protocol_name: ICMP source_wildcard_id: 0 dest_wildcard_id: 0 10: @@ -222,12 +222,12 @@ agents: firewall_port_name: external firewall_port_direction: outbound position: 1 - permission: 2 - source_ip_id: 4 # external_computer - dest_ip_id: 2 # client_1 - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 + permission: DENY + src_ip: 192.168.20.10 # external_computer + dst_ip: 192.168.0.10 # client_1 + src_port: NONE + dst_port: NONE + protocol_name: none source_wildcard_id: 0 dest_wildcard_id: 0 12: diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index f380ba7d..c4350e1f 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -163,8 +163,9 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox }, ) agent.store_action(action) + print(agent.most_recent_action) game.step() - + print(agent.most_recent_action) # 5: Check that the ACL now has 6 rules, but that server_1 can still ping server_2 print(router.acl.show()) assert router.acl.num_rules == 6 @@ -653,9 +654,9 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.external_outbound_acl.acl[1].action.name == "DENY" assert firewall.external_outbound_acl.acl[1].src_ip_address == IPv4Address("192.168.20.10") assert firewall.external_outbound_acl.acl[1].dst_ip_address == IPv4Address("192.168.0.10") - assert firewall.external_outbound_acl.acl[1].dst_port is None - assert firewall.external_outbound_acl.acl[1].src_port is None - assert firewall.external_outbound_acl.acl[1].protocol is None + assert firewall.external_outbound_acl.acl[1].dst_port == PORT_LOOKUP["NONE"] + assert firewall.external_outbound_acl.acl[1].src_port == PORT_LOOKUP["NONE"] + assert firewall.external_outbound_acl.acl[1].protocol == PROTOCOL_LOOKUP["NONE"] env.step(12) # Remove ACL rule from External Outbound assert firewall.external_outbound_acl.num_rules == 1 From 4e7ca7a88a2ede458f2269ada0bf8a89f5eb8db4 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 15 Nov 2024 16:49:16 +0000 Subject: [PATCH 060/224] #2912 - Removal of excess comments --- src/primaite/game/agent/actions/abstract.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index c18f0dbc..9c13cc73 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -8,15 +8,6 @@ from pydantic import BaseModel, ConfigDict from primaite.interface.request import RequestFormat -# notes: -# we actually don't need to hold any state in actions, so there's no need to define any __init__ logic. -# all the init methods in the old actions are just used for holding a verb and shape, which are not really used. -# the config schema should be used to the actual parameters for formatting the action itself. -# (therefore there's no need for creating action instances, just the action class contains logic for converting -# CAOS actions to requests for simulator. Similar to the network node adder, that class also doesn't need to be -# instantiated.) - - class AbstractAction(BaseModel): """Base class for actions.""" From ce77df00ccf25d55d826fc8cf69303baad51cfb9 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 15 Nov 2024 17:22:47 +0000 Subject: [PATCH 061/224] #2912 - Updated changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9147947..16cc3ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.0.0] = TBC + +### Added + +### Changed +- Actions within PrimAITE are now extensible, allowing for plugin support. + + ## [3.3.0] - 2024-09-04 ### Added From 0439c3159e5948a8a7a36501aa835840e39d4fe2 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 15 Nov 2024 17:40:11 +0000 Subject: [PATCH 062/224] #2912 - Minor update to extensible actions documentation and moved old actions into _legacy --- docs/source/how_to_guides/extensible_actions.rst | 2 ++ src/primaite/{game/agent => _legacy}/actions.py | 0 2 files changed, 2 insertions(+) rename src/primaite/{game/agent => _legacy}/actions.py (100%) diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index bd78c8e1..576aa75f 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -25,8 +25,10 @@ New actions to be used within PrimAITE require: .. code-block:: python class ExampleAction(AbstractAction, identifier="Example_action"): + """An example action for demonstration purposes.""" class ConfigSchema(AbstractAction.ConfigSchema): + """The configuration schema with all attributes expected goes here.""" target_application: str The ConfigSchema is used when the class is called to form the action, within the `form_request` method, detailed below. diff --git a/src/primaite/game/agent/actions.py b/src/primaite/_legacy/actions.py similarity index 100% rename from src/primaite/game/agent/actions.py rename to src/primaite/_legacy/actions.py From 958502d055fcd66b353e0e30f687643452178753 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 15 Nov 2024 17:47:58 +0000 Subject: [PATCH 063/224] #2912 - Removal of todo and updated actions __init__.py --- src/primaite/game/agent/actions/__init__.py | 2 -- src/primaite/game/agent/actions/manager.py | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 016a09ba..625725fe 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -14,7 +14,6 @@ from primaite.game.agent.actions import ( service, session, ) -from primaite.game.agent.actions.manager import ActionManager __all__ = ( "abstract", @@ -29,5 +28,4 @@ __all__ = ( "node", "service", "session", - "ActionManager", ) diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 6c8353b0..3795d21d 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -22,9 +22,7 @@ from gymnasium import spaces from primaite.game.agent.actions.abstract import AbstractAction from primaite.interface.request import RequestFormat -# TODO: Make sure that actions are backwards compatible where the old YAML format is used. - -__all__ = "DoNothingAction" +__all__ = ("DoNothingAction", "ActionManager") class DoNothingAction(AbstractAction, identifier="do_nothing"): From b4bc59f6333bcb6b480e7fa62f8444492b36f0a9 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 19 Nov 2024 18:57:40 +0000 Subject: [PATCH 064/224] #2912 - Updates to actions refactor to align with rewards refactor for consistency across codebase. --- src/primaite/game/agent/actions/__init__.py | 1 + src/primaite/game/agent/actions/abstract.py | 10 +- src/primaite/game/agent/actions/acl.py | 77 +++++++------ .../game/agent/actions/application.py | 23 ++-- src/primaite/game/agent/actions/config.py | 108 ++++++++---------- src/primaite/game/agent/actions/file.py | 32 +++--- src/primaite/game/agent/actions/folder.py | 19 +-- src/primaite/game/agent/actions/host_nic.py | 13 ++- src/primaite/game/agent/actions/manager.py | 7 +- src/primaite/game/agent/actions/network.py | 17 +-- src/primaite/game/agent/actions/node.py | 27 ++++- src/primaite/game/agent/actions/service.py | 25 ++-- src/primaite/game/agent/actions/session.py | 13 ++- .../configs/firewall_actions_network.yaml | 14 +-- .../game_layer/test_actions.py | 4 +- 15 files changed, 209 insertions(+), 181 deletions(-) diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 625725fe..7f054591 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -14,6 +14,7 @@ from primaite.game.agent.actions import ( service, session, ) +from primaite.game.agent.actions.manager import ActionManager __all__ = ( "abstract", diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 9c13cc73..5c0594fd 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -11,6 +11,8 @@ from primaite.interface.request import RequestFormat class AbstractAction(BaseModel): """Base class for actions.""" + config: "AbstractAction.ConfigSchema" + class ConfigSchema(BaseModel, ABC): """Base configuration schema for Actions.""" @@ -33,6 +35,8 @@ class AbstractAction(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAction": """Create an action component from a config dictionary.""" - for attribute, value in config.items(): - setattr(cls.ConfigSchema, attribute, value) - return cls + if not config.get("type"): + config.update({"type": cls.__name__}) + print("oooh") + print(config) + return cls(config=cls.ConfigSchema(**config)) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 3beface9..11269a7e 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -25,27 +25,21 @@ class ACLAbstractAction(AbstractAction, identifier="acl_abstract_action"): class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" - target_router: str - position: int - permission: Literal[1, 2] - source_wildcard_id: int - source_port: str - dst_ip: str - dst_wildcard: int - dst_port: int + config: "RouterACLAddRuleAction.ConfigSchema" class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for RouterACLAddRuleAction.""" target_router: str + permission: str + protocol_name: str position: int - permission: Literal[1, 2] src_ip: str src_wildcard: int source_port: str dst_ip: str dst_wildcard: int - dst_port: int + dst_port: str @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: @@ -71,11 +65,13 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): class RouterACLRemoveRuleAction(AbstractAction, identifier="router_acl_remove_rule"): """Action which removes a rule from a router's ACL.""" + config: "RouterACLRemoveRuleAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for RouterACLRemoveRuleAction.""" target_router: str - position: str + position: int @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -86,33 +82,42 @@ class RouterACLRemoveRuleAction(AbstractAction, identifier="router_acl_remove_ru class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_rule"): """Action which adds a rule to a firewall port's ACL.""" - max_acl_rules: int - num_ips: int - num_ports: int - num_protocols: int - num_permissions: int = 3 - permission: str - target_firewall_nodename: str - src_ip: str - dst_ip: str - dst_wildcard: str - src_port: Union[int| None] - dst_port: Union[int | None] + config: "FirewallACLAddRuleAction.ConfigSchema" + + # max_acl_rules: int + # num_ips: int + # num_ports: int + # num_protocols: int + # num_permissions: int = 3 + # permission: str + # target_firewall_nodename: str + # src_ip: str + # dst_ip: str + # dst_wildcard: str + # src_port: Union[int| None] + # dst_port: Union[int | None] class ConfigSchema(ACLAbstractAction.ConfigSchema): """Configuration schema for FirewallACLAddRuleAction.""" - max_acl_rules: int - num_ips: int - num_ports: int - num_protocols: int - num_permissions: int = 3 - permission: str target_firewall_nodename: str + firewall_port_name: str + firewall_port_direction: str + position: int + permission: str src_ip: str - dst_ip: str - dst_wildcard: str - src_port: Union[int| None] + dest_ip: str + src_port: str + dst_port: str + protocol_name: str + source_wildcard_id: int + dest_wildcard_id: int + + # max_acl_rules: int + # num_ips: int + # num_ports: int + # num_protocols: int + # num_permissions: int = 3 @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: @@ -136,10 +141,10 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r config.permission, config.protocol_name, config.src_ip, - config.src_wildcard, + config.source_wildcard_id, config.src_port, - config.dst_ip, - config.dst_wildcard, + config.dest_ip, + config.dest_wildcard_id, config.dst_port, config.position, ] @@ -148,6 +153,8 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r class FirewallACLRemoveRuleAction(AbstractAction, identifier="firewall_acl_remove_rule"): """Action which removes a rule from a firewall port's ACL.""" + config:"FirewallACLRemoveRuleAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for FirewallACLRemoveRuleAction.""" diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 942ebe90..f515a8ec 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -22,13 +22,14 @@ class NodeApplicationAbstractAction(AbstractAction, identifier="node_application inherit from this base class. """ + config: "NodeApplicationAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Base Configuration schema for Node Application actions.""" node_name: str application_name: str - - verb: ClassVar[str] + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -41,14 +42,14 @@ class NodeApplicationAbstractAction(AbstractAction, identifier="node_application config.node_name, "application", config.application_name, - cls.model_fields["verb"].default, + config.verb, ] class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="node_application_execute"): """Action which executes an application.""" - verb: str = "execute" + config: "NodeApplicationExecuteAction.ConfigSchema" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationExecuteAction.""" @@ -59,7 +60,7 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="no class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_application_scan"): """Action which scans an application.""" - verb: str = "scan" + config: "NodeApplicationScanAction.ConfigSchema" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationScanAction.""" @@ -70,7 +71,7 @@ class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_ class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node_application_close"): """Action which closes an application.""" - verb: str = "close" + config: "NodeApplicationCloseAction.ConfigSchema" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationCloseAction.""" @@ -81,7 +82,7 @@ class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_application_fix"): """Action which fixes an application.""" - verb: str = "fix" + config: "NodeApplicationFixAction.ConfigSchema" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationFixAction.""" @@ -92,7 +93,7 @@ class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_a class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="node_application_install"): """Action which installs an application.""" - verb: str = "install" + config: "NodeApplicationInstallAction.ConfigSchema" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationInstallAction.""" @@ -110,7 +111,7 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="no config.node_name, "software_manager", "application", - cls.model_fields["verb"].default, + config.verb, config.application_name, ] @@ -118,7 +119,7 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="no class NodeApplicationRemoveAction(NodeApplicationAbstractAction, identifier="node_application_remove"): """Action which removes/uninstalls an application.""" - verb: str = "uninstall" + config: "NodeApplicationRemoveAction.ConfigSchema" class ConfigSchema(NodeApplicationAbstractAction.ConfigSchema): """Configuration schema for NodeApplicationRemoveAction.""" @@ -136,6 +137,6 @@ class NodeApplicationRemoveAction(NodeApplicationAbstractAction, identifier="nod config.node_name, "software_manager", "application", - cls.model_fields["verb"].default, + config.verb, config.application_name, ] diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index dc7e98b9..da9f77e6 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -22,6 +22,8 @@ __all__ = ( class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_ransomware_configure"): """Action which sets config parameters for a ransomware script on a node.""" + config: "ConfigureRansomwareScriptAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for ConfigureRansomwareScriptAction.""" @@ -36,16 +38,18 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_rans """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] - ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", config.model_config] class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): """Action which sets config parameters for a DoS bot on a node.""" - class _Opts(BaseModel): + config: "ConfigureDoSBotAction.ConfigSchema" + + class ConfigSchema(AbstractAction.ConfigSchema): """Schema for options that can be passed to this action.""" + node_name: str model_config = ConfigDict(extra="forbid") target_ip_address: Optional[str] = None target_port: Optional[str] = None @@ -58,18 +62,19 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): def __init__(self, manager: "ActionManager", **kwargs) -> None: super().__init__(manager=manager) - def form_request(self, node_id: int, config: Dict) -> RequestFormat: + def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: + if config.node_name is None: return ["do_nothing"] - self._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "DoSBot", "configure", config] + self.ConfigSchema.model_validate(config) # check that options adhere to schema + return ["network", "node", config.node_name, "application", "DoSBot", "configure", config] class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): """Action which configures a C2 Beacon based on the parameters given.""" + config: "ConfigureC2BeaconAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for ConfigureC2BeaconAction.""" @@ -79,14 +84,6 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): masquerade_protocol: str = Field(default="TCP") masquerade_port: str = Field(default="HTTP") - class _Opts(BaseModel): - """Schema for options that can be passed to this action.""" - - c2_server_ip_address: str - keep_alive_frequency: int = Field(default=5, ge=1) - masquerade_protocol: str = Field(default="TCP") - masquerade_port: str = Field(default="HTTP") - @field_validator( "c2_server_ip_address", "keep_alive_frequency", @@ -106,21 +103,23 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] - configuration = ConfigureC2BeaconAction._Opts( + configuration = ConfigureC2BeaconAction.ConfigSchema( c2_server_ip_address=config.c2_server_ip_address, keep_alive_frequency=config.keep_alive_frequency, masquerade_port=config.masquerade_port, masquerade_protocol=config.masquerade_protocol, ) - ConfigureC2BeaconAction._Opts.model_validate(configuration) # check that options adhere to schema + ConfigureC2BeaconAction.ConfigSchema.model_validate(configuration) # check that options adhere to schema - return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config.__dict__] + return ["network", "node", config.node_name, "application", "C2Beacon", "configure", configuration] class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_command"): """Action which sends a terminal command to a remote node via SSH.""" + config: "NodeSendRemoteCommandAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for NodeSendRemoteCommandAction.""" @@ -146,37 +145,37 @@ class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_c class TerminalC2ServerAction(AbstractAction, identifier="c2_server_terminal_command"): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" - class _Opts(BaseModel): + config: "TerminalC2ServerAction.ConfigSchema" + + class ConfigSchema(AbstractAction.ConfigSchema): """Schema for options that can be passed to this action.""" + node_name: str commands: Union[List[RequestFormat], RequestFormat] ip_address: Optional[str] username: Optional[str] password: Optional[str] - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, commands: List, ip_address: Optional[str], account: dict) -> RequestFormat: + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: + if config.node_name is None: return ["do_nothing"] command_model = { - "commands": commands, - "ip_address": ip_address, - "username": account["username"], - "password": account["password"], + "commands": config.commands, + "ip_address": config.ip_address, + "username": config.username, + "password": config.password, } - - TerminalC2ServerAction._Opts.model_validate(command_model) - return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] + return ["network", "node", config.node_name, "application", "C2Server", "terminal_command", command_model] class RansomwareLaunchC2ServerAction(AbstractAction, identifier="c2_server_ransomware_launch"): """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" + config: "RansomwareLaunchC2ServerAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for RansomwareLaunchC2ServerAction.""" @@ -194,9 +193,12 @@ class RansomwareLaunchC2ServerAction(AbstractAction, identifier="c2_server_ranso class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfiltrate"): """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" - class _Opts(BaseModel): + config: "ExfiltrationC2ServerAction.ConfigSchema" + + class ConfigSchema(AbstractAction.ConfigSchema): """Schema for options that can be passed to this action.""" + node_name: str username: Optional[str] password: Optional[str] target_ip_address: str @@ -204,40 +206,30 @@ class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfi target_folder_name: str exfiltration_folder_name: Optional[str] - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - + @classmethod def form_request( - self, - node_id: int, - account: dict, - target_ip_address: str, - target_file_name: str, - target_folder_name: str, - exfiltration_folder_name: Optional[str], + cls, + config: ConfigSchema ) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: + if config.node_name is None: return ["do_nothing"] command_model = { - "target_file_name": target_file_name, - "target_folder_name": target_folder_name, - "exfiltration_folder_name": exfiltration_folder_name, - "target_ip_address": target_ip_address, - "username": account["username"], - "password": account["password"], + "target_file_name": config.target_file_name, + "target_folder_name": config.target_folder_name, + "exfiltration_folder_name": config.exfiltration_folder_name, + "target_ip_address": config.target_ip_address, + "username": config.username, + "password": config.password, } - ExfiltrationC2ServerAction._Opts.model_validate(command_model) - return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] + return ["network", "node", config.node_name, "application", "C2Server", "exfiltrate", command_model] class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_database_client"): """Action which sets config parameters for a database client on a node.""" - node_name: str - model_config: ConfigDict = ConfigDict(extra="forbid") + config: "ConfigureDatabaseClientAction.ConfigSchema" class ConfigSchema(AbstractAction.ConfigSchema): """Schema for options that can be passed to this action.""" @@ -245,10 +237,8 @@ class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_databa node_name: str model_config = ConfigDict(extra="forbid") - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, config: ConfigSchema) -> RequestFormat: + @classmethod + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index 5d12b27a..b5e47c8a 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -23,14 +23,15 @@ class NodeFileAbstractAction(AbstractAction, identifier="node_file_abstract_acti only three parameters can inherit from this base class. """ + config: "NodeFileAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for NodeFileAbstractAction.""" node_name: str folder_name: str file_name: str - - verb: ClassVar[str] + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -46,15 +47,14 @@ class NodeFileAbstractAction(AbstractAction, identifier="node_file_abstract_acti config.folder_name, "file", config.file_name, - cls.model_fields["verb"].default, + config.verb, ] class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create"): """Action which creates a new file in a given folder.""" - verb: str = "create" - force: bool = False + config: "NodeFileCreateAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCreateAction.""" @@ -72,18 +72,18 @@ class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create" "node", config.node_name, "file_system", - cls.model_fields["verb"].default, + config.verb, "file", config.folder_name, config.file_name, - cls.model_fields["force"].default, + config.verb, ] class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): """Action which scans a file.""" - verb: str = "scan" + config: "NodeFileScanAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileScanAction.""" @@ -94,7 +94,7 @@ class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete"): """Action which deletes a file.""" - verb: str = "delete" + config: "NodeFileDeleteAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileDeleteAction.""" @@ -111,7 +111,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete" "node", config.node_name, "file_system", - cls.model_fields["verb"].default, + config.verb, "file", config.folder_name, config.file_name, @@ -121,7 +121,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete" class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restore"): """Action which restores a file.""" - verb: str = "restore" + config: "NodeFileRestoreAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileRestoreAction.""" @@ -132,7 +132,7 @@ class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restor class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrupt"): """Action which corrupts a file.""" - verb: str = "corrupt" + config: "NodeFileCorruptAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCorruptAction.""" @@ -143,7 +143,7 @@ class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrup class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access"): """Action which increases a file's access count.""" - verb: str = "access" + config: "NodeFileAccessAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileAccessAction.""" @@ -160,7 +160,7 @@ class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access" "node", config.node_name, "file_system", - cls.model_fields["verb"].default, + config.verb, config.folder_name, config.file_name, ] @@ -169,7 +169,7 @@ class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access" class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_checkhash"): """Action which checks the hash of a file.""" - verb: str = "checkhash" + config: "NodeFileCheckhashAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCheckhashAction.""" @@ -180,7 +180,7 @@ class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_chec class NodeFileRepairAction(NodeFileAbstractAction, identifier="node_file_repair"): """Action which repairs a file.""" - verb: str = "repair" + config: "NodeFileRepairAction.ConfigSchema" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration Schema for NodeFileRepairAction.""" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index e430efb7..a27ca89b 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -21,13 +21,14 @@ class NodeFolderAbstractAction(AbstractAction, identifier="node_folder_abstract" this base class. """ + config: "NodeFolderAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Base configuration schema for NodeFolder actions.""" node_name: str folder_name: str - - verb: ClassVar[str] + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -41,14 +42,14 @@ class NodeFolderAbstractAction(AbstractAction, identifier="node_folder_abstract" "file_system", "folder", config.folder_name, - cls.model_fields["verb"].default, + config.verb, ] class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_scan"): """Action which scans a folder.""" - verb: str = "scan" + config: "NodeFolderScanAction.ConfigSchema" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderScanAction.""" @@ -59,7 +60,7 @@ class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_sca class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folder_checkhash"): """Action which checks the hash of a folder.""" - verb: str = "checkhash" + config: "NodeFolderCheckhashAction.ConfigSchema" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderCheckhashAction.""" @@ -70,7 +71,7 @@ class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folde class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_repair"): """Action which repairs a folder.""" - verb: str = "repair" + config: "NodeFolderRepairAction.ConfigSchema" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderRepairAction.""" @@ -81,7 +82,7 @@ class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_r class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_restore"): """Action which restores a folder.""" - verb: str = "restore" + config: "NodeFolderRestoreAction.ConfigSchema" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderRestoreAction.""" @@ -92,7 +93,7 @@ class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_ class NodeFolderCreateAction(NodeFolderAbstractAction, identifier="node_folder_create"): """Action which creates a new folder.""" - verb: str = "create" + config: "NodeFolderCreateAction.ConfigSchema" class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderCreateAction.""" @@ -109,7 +110,7 @@ class NodeFolderCreateAction(NodeFolderAbstractAction, identifier="node_folder_c "node", config.node_name, "file_system", - cls.model_fields["verb"].default, + config.verb, "folder", config.folder_name, ] diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 1ad2e52f..6df241bc 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -13,14 +14,14 @@ class HostNICAbstractAction(AbstractAction, identifier="host_nic_abstract"): class. """ - node_name: str - nic_num: str + config: "HostNICAbstractAction.ConfigSchema" class ConfigSchema(AbstractAction.ConfigSchema): """Base Configuration schema for HostNIC actions.""" node_name: str - nic_num: str + nic_num: int + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -33,14 +34,14 @@ class HostNICAbstractAction(AbstractAction, identifier="host_nic_abstract"): config.node_name, "network_interface", config.nic_num, - cls.model_fields["verb"].default, + config.verb, ] class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): """Action which enables a NIC.""" - verb: str = "enable" + config: "HostNICEnableAction.ConfigSchema" class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICEnableAction.""" @@ -51,7 +52,7 @@ class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): class HostNICDisableAction(HostNICAbstractAction, identifier="host_nic_disable"): """Action which disables a NIC.""" - verb: str = "disable" + config: "HostNICDisableAction.ConfigSchema" class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICDisableAction.""" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 3795d21d..9ef94069 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -31,7 +31,8 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for DoNothingAction.""" - type: Literal["do_nothing"] = "do_nothing" + # type: Literal["do_nothing"] = "do_nothing" + type: str = "do_nothing" @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -126,13 +127,13 @@ class ActionManager: def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" act_obj = self.actions[action_identifier].from_config(config=action_options) - return act_obj.form_request(config=act_obj.ConfigSchema) + return act_obj.form_request(config=act_obj.config) @property def space(self) -> spaces.Space: """Return the gymnasium action space for this agent.""" return spaces.Discrete(len(self.action_map)) - + @classmethod def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": """ diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index af3793a2..346da9b7 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -11,13 +11,14 @@ __all__ = ("NetworkPortEnableAction", "NetworkPortDisableAction") class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstract"): """Base class for Network port actions.""" + config: "NetworkPortAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Base configuration schema for NetworkPort actions.""" target_nodename: str - port_id: str - - verb: ClassVar[str] + port_id: int + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -30,16 +31,16 @@ class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstrac config.target_nodename, "network_interface", config.port_id, - cls.model_fields["verb"].default, + config.verb, ] class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_port_enable"): """Action which enables are port on a router or a firewall.""" - verb: str = "enable" + config: "NetworkPortEnableAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(NetworkPortAbstractAction.ConfigSchema): """Configuration schema for NetworkPortEnableAction.""" verb: str = "enable" @@ -48,9 +49,9 @@ class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_por class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_port_disable"): """Action which disables are port on a router or a firewall.""" - verb: str = "disable" + config: "NetworkPortDisableAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(NetworkPortAbstractAction.ConfigSchema): """Configuration schema for NetworkPortDisableAction.""" verb: str = "disable" diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index a69a8a5f..3c70d495 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -23,22 +23,25 @@ class NodeAbstractAction(AbstractAction, identifier="node_abstract"): Any action which applies to a node and uses node_name as its only parameter can inherit from this base class. """ + config: "NodeAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Base Configuration schema for Node actions.""" node_name: str - - verb: ClassVar[str] + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["network", "node", config.node_name, cls.verb] + return ["network", "node", config.node_name, cls.config.verb] class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): """Action which scans a node's OS.""" + config: "NodeOSScanAction.ConfigSchema" + class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeOSScanAction.""" @@ -48,6 +51,8 @@ class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): """Action which shuts down a node.""" + config: "NodeShutdownAction.ConfigSchema" + class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeShutdownAction.""" @@ -57,6 +62,8 @@ class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): """Action which starts up a node.""" + config: "NodeStartupAction.ConfigSchema" + class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeStartupAction.""" @@ -66,6 +73,8 @@ class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): class NodeResetAction(NodeAbstractAction, identifier="node_reset"): """Action which resets a node.""" + config: "NodeResetAction.ConfigSchema" + class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeResetAction.""" @@ -75,22 +84,28 @@ class NodeResetAction(NodeAbstractAction, identifier="node_reset"): class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_action"): """Base class for NodeNMAP actions.""" + config: "NodeNMAPAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Base Configuration Schema for NodeNMAP actions.""" target_ip_address: Union[str, List[str]] - show: bool = False + show: bool = False node_name: str @classmethod @abstractmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: + # NMAP action requests don't share a common format for their requests + # This is just a placeholder to ensure the method is defined. pass class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_scan"): """Action which performs an NMAP ping scan.""" + config: "NodeNMAPPingScanAction.ConfigSchema" + class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): """Configuration schema for NodeNMAPPingScanAction.""" @@ -113,6 +128,8 @@ class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_scan"): """Action which performs an NMAP port scan.""" + config: "NodeNMAPPortScanAction.ConfigSchema" + class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): """Configuration Schema for NodeNMAPPortScanAction.""" @@ -146,6 +163,8 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_ class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, identifier="node_network_service_recon"): """Action which performs an NMAP network service recon (ping scan followed by port scan).""" + config: "NodeNetworkServiceReconAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for NodeNetworkServiceReconAction.""" diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index dbdd57d3..7ccffb0a 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -23,22 +23,23 @@ class NodeServiceAbstractAction(AbstractAction, identifier="node_service_abstrac Any actions which use node_name and service_name can inherit from this class. """ + config: "NodeServiceAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): node_name: str service_name: str - - verb: ClassVar[str] + verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["network", "node", config.node_name, "service", config.service_name, cls.model_fields["verb"].default] + return ["network", "node", config.node_name, "service", config.service_name, config.verb] class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_scan"): """Action which scans a service.""" - verb: str = "scan" + config: "NodeServiceScanAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceScanAction.""" @@ -49,7 +50,7 @@ class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_stop"): """Action which stops a service.""" - verb: str = "stop" + config: "NodeServiceStopAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceStopAction.""" @@ -60,7 +61,7 @@ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service_start"): """Action which starts a service.""" - verb: str = "start" + config: "NodeServiceStartAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceStartAction.""" @@ -71,7 +72,7 @@ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service_pause"): """Action which pauses a service.""" - verb: str = "pause" + config: "NodeServicePauseAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServicePauseAction.""" @@ -82,7 +83,7 @@ class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_service_resume"): """Action which resumes a service.""" - verb: str = "resume" + config: "NodeServiceResumeAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceResumeAction.""" @@ -93,7 +94,7 @@ class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_servic class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_service_restart"): """Action which restarts a service.""" - verb: str = "restart" + config: "NodeServiceRestartAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceRestartAction.""" @@ -104,7 +105,7 @@ class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_servi class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_service_disable"): """Action which disables a service.""" - verb: str = "disable" + config: "NodeServiceDisableAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceDisableAction.""" @@ -115,7 +116,7 @@ class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_servi class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_service_enable"): """Action which enables a service.""" - verb: str = "enable" + config: "NodeServiceEnableAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceEnableAction.""" @@ -126,7 +127,7 @@ class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_servic class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_fix"): """Action which fixes a service.""" - verb: str = "fix" + config: "NodeServiceFixAction.ConfigSchema" class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceFixAction.""" diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index dcae8b47..a0805a49 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -14,6 +14,8 @@ __all__ = ( class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstract"): """Base class for NodeSession actions.""" + config: "NodeSessionAbstractAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """Base configuration schema for NodeSessionAbstractActions.""" @@ -34,8 +36,7 @@ class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstrac class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_session_remote_login"): """Action which performs a remote session login.""" - username: str - password: str + config: "NodeSessionsRemoteLoginAction.ConfigSchema" class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLoginAction.""" @@ -64,6 +65,8 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node_session_remote_logoff"): """Action which performs a remote session logout.""" + config: "NodeSessionsRemoteLogoutAction.ConfigSchema" + class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLogoutAction.""" @@ -80,9 +83,7 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node class NodeAccountChangePasswordAction(NodeSessionAbstractAction, identifier="node_account_change_password"): """Action which changes the password for a user.""" - username: str - current_password: str - new_password: str + config: "NodeAccountChangePasswordAction.ConfigSchema" class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeAccountsChangePasswordAction.""" @@ -103,5 +104,5 @@ class NodeAccountChangePasswordAction(NodeSessionAbstractAction, identifier="nod "change_password", config.username, config.current_password, - cls.new_password, + config.new_password, ] diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index a2b75be5..4c3b5000 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -114,12 +114,12 @@ agents: position: 1 permission: PERMIT src_ip: 192.168.0.10 - dst_ip: ALL + dest_ip: ALL src_port: ALL dst_port: ALL protocol_name: ALL - src_wildcard: 0 - dst_wildcard: 0 + source_wildcard_id: 0 + dest_wildcard_id: 0 2: action: firewall_acl_remove_rule options: @@ -158,7 +158,7 @@ agents: position: 1 permission: DENY src_ip: 192.168.10.10 # dmz_server - dst_ip: 192.168.0.10 # client_1 + dest_ip: 192.168.0.10 # client_1 src_port: HTTP dst_port: HTTP protocol_name: UDP @@ -180,7 +180,7 @@ agents: position: 2 permission: DENY src_ip: 192.168.10.10 # dmz_server - dst_ip: 192.168.0.10 # client_1 + dest_ip: 192.168.0.10 # client_1 src_port: HTTP dst_port: HTTP protocol_name: TCP @@ -202,7 +202,7 @@ agents: position: 10 permission: DENY src_ip: 192.168.20.10 # external_computer - dst_ip: 192.168.10.10 # dmz + dest_ip: 192.168.10.10 # dmz src_port: POSTGRES_SERVER dst_port: POSTGRES_SERVER protocol_name: ICMP @@ -224,7 +224,7 @@ agents: position: 1 permission: DENY src_ip: 192.168.20.10 # external_computer - dst_ip: 192.168.0.10 # client_1 + dest_ip: 192.168.0.10 # client_1 src_port: NONE dst_port: NONE protocol_name: none diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index c4350e1f..a31f325a 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -56,7 +56,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_scan", {"type":"node_service_scan" ,"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -67,7 +67,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_scan", {"type":"node_service_scan", "node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.COMPROMISED From d3c52d0d7296ddd18f4f6fb7d84856a169577849 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 19 Nov 2024 18:58:51 +0000 Subject: [PATCH 065/224] #2912 - Remove some debugging print statements and apply pre-commit lint changes --- src/primaite/game/agent/actions/abstract.py | 3 +-- src/primaite/game/agent/actions/acl.py | 5 ++--- src/primaite/game/agent/actions/config.py | 15 ++++++++++----- src/primaite/game/agent/actions/host_nic.py | 1 + src/primaite/game/agent/actions/manager.py | 3 +-- src/primaite/game/agent/actions/node.py | 2 +- .../integration_tests/game_layer/test_actions.py | 4 ++-- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 5c0594fd..cd14ef6d 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, ConfigDict from primaite.interface.request import RequestFormat + class AbstractAction(BaseModel): """Base class for actions.""" @@ -37,6 +38,4 @@ class AbstractAction(BaseModel): """Create an action component from a config dictionary.""" if not config.get("type"): config.update({"type": cls.__name__}) - print("oooh") - print(config) return cls(config=cls.ConfigSchema(**config)) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 11269a7e..7ab49732 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -127,8 +127,7 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r if config.src_ip == 0: return ["do_nothing"] # invalid formulation if config.src_port == 0: - return ["do_nothing"] # invalid configuration. - + return ["do_nothing"] # invalid configuration. return [ "network", @@ -153,7 +152,7 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r class FirewallACLRemoveRuleAction(AbstractAction, identifier="firewall_acl_remove_rule"): """Action which removes a rule from a firewall port's ACL.""" - config:"FirewallACLRemoveRuleAction.ConfigSchema" + config: "FirewallACLRemoveRuleAction.ConfigSchema" class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for FirewallACLRemoveRuleAction.""" diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index da9f77e6..7c72e57d 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -38,7 +38,15 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_rans """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] - return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", config.model_config] + return [ + "network", + "node", + config.node_name, + "application", + "RansomwareScript", + "configure", + config.model_config, + ] class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): @@ -207,10 +215,7 @@ class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfi exfiltration_folder_name: Optional[str] @classmethod - def form_request( - cls, - config: ConfigSchema - ) -> RequestFormat: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 6df241bc..e2adf7d7 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,5 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar + from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 9ef94069..a413f6dc 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -31,7 +31,6 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for DoNothingAction.""" - # type: Literal["do_nothing"] = "do_nothing" type: str = "do_nothing" @classmethod @@ -133,7 +132,7 @@ class ActionManager: def space(self) -> spaces.Space: """Return the gymnasium action space for this agent.""" return spaces.Discrete(len(self.action_map)) - + @classmethod def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": """ diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 3c70d495..4ecc1393 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -90,7 +90,7 @@ class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_acti """Base Configuration Schema for NodeNMAP actions.""" target_ip_address: Union[str, List[str]] - show: bool = False + show: bool = False node_name: str @classmethod diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index a31f325a..a21ad34f 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -56,7 +56,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("node_service_scan", {"type":"node_service_scan" ,"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_scan", {"type": "node_service_scan", "node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -67,7 +67,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("node_service_scan", {"type":"node_service_scan", "node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_scan", {"type": "node_service_scan", "node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.COMPROMISED From 8f610a3dd9b9d6497d0216e1201b8ee8bceaa92b Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 19 Nov 2024 19:39:23 +0000 Subject: [PATCH 066/224] #2912 - Minor changes to documentation page for extensible actions --- .../how_to_guides/extensible_actions.rst | 4 +++- src/primaite/game/agent/actions/acl.py | 19 ------------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 576aa75f..6e44a905 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -27,6 +27,8 @@ New actions to be used within PrimAITE require: class ExampleAction(AbstractAction, identifier="Example_action"): """An example action for demonstration purposes.""" + config: "ExampleAction.ConfigSchema" + class ConfigSchema(AbstractAction.ConfigSchema): """The configuration schema with all attributes expected goes here.""" target_application: str @@ -55,7 +57,7 @@ New actions to be used within PrimAITE require: "node", config.node_name, "file_system", - cls.model_fields["verb"].default, + config.verb, "folder", config.folder_name, ] diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 7ab49732..e8ad59f5 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -84,19 +84,6 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r config: "FirewallACLAddRuleAction.ConfigSchema" - # max_acl_rules: int - # num_ips: int - # num_ports: int - # num_protocols: int - # num_permissions: int = 3 - # permission: str - # target_firewall_nodename: str - # src_ip: str - # dst_ip: str - # dst_wildcard: str - # src_port: Union[int| None] - # dst_port: Union[int | None] - class ConfigSchema(ACLAbstractAction.ConfigSchema): """Configuration schema for FirewallACLAddRuleAction.""" @@ -113,12 +100,6 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r source_wildcard_id: int dest_wildcard_id: int - # max_acl_rules: int - # num_ips: int - # num_ports: int - # num_protocols: int - # num_permissions: int = 3 - @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" From a3dc616126ae850c291aad4ac8cef0d4ddf88447 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 20 Nov 2024 17:19:35 +0000 Subject: [PATCH 067/224] #2869 - Starter changes in refactor of agent classes for refactor to become extensible. Identifiers added to classes and beginning of the inclusion of a ConfigSchema to base AbstractAgentClass --- src/primaite/game/agent/interface.py | 150 ++++++++++++------ .../scripted_agents/data_manipulation_bot.py | 21 ++- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 4 +- .../game/agent/scripted_agents/tap001.py | 55 ++++--- src/primaite/game/game.py | 20 +++ tests/conftest.py | 2 +- 7 files changed, 174 insertions(+), 80 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 14b97821..7adaab69 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,7 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Interface for agents.""" +from __future__ import annotations + from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union from gymnasium.core import ActType, ObsType from pydantic import BaseModel, model_validator @@ -69,7 +71,7 @@ class AgentSettings(BaseModel): """Settings for configuring the operation of an agent.""" start_settings: Optional[AgentStartSettings] = None - "Configuration for when an agent begins performing it's actions" + "Configuration for when an agent begins performing it's actions." flatten_obs: bool = True "Whether to flatten the observation space before passing it to the agent. True by default." action_masking: bool = False @@ -90,38 +92,78 @@ class AgentSettings(BaseModel): return cls(**config) -class AbstractAgent(ABC): +class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): """Base class for scripted and RL agents.""" - def __init__( - self, - agent_name: Optional[str], - action_space: Optional[ActionManager], - observation_space: Optional[ObservationManager], - reward_function: Optional[RewardFunction], - agent_settings: Optional[AgentSettings] = None, - ) -> None: - """ - Initialize an agent. + _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} + config: "AbstractAgent.ConfigSchema" + action_manager: Optional[ActionManager] + observation_manager: Optional[ObservationManager] + reward_function: Optional[RewardFunction] + + class ConfigSchema(BaseModel): + """ + Configuration Schema for AbstractAgents. + + :param type: Type of agent being generated. + :type type: str :param agent_name: Unique string identifier for the agent, for reporting and multi-agent purposes. - :type agent_name: Optional[str] - :param action_space: Action space for the agent. - :type action_space: Optional[ActionManager] + :type agent_name: str :param observation_space: Observation space for the agent. :type observation_space: Optional[ObservationSpace] :param reward_function: Reward function for the agent. :type reward_function: Optional[RewardFunction] - :param agent_settings: Configurable Options for Abstracted Agents + :param agent_settings: Configurable Options for Abstracted Agents. :type agent_settings: Optional[AgentSettings] """ - self.agent_name: str = agent_name or "unnamed_agent" - self.action_manager: Optional[ActionManager] = action_space - self.observation_manager: Optional[ObservationManager] = observation_space - self.reward_function: Optional[RewardFunction] = reward_function - self.agent_settings = agent_settings or AgentSettings() - self.history: List[AgentHistoryItem] = [] - self.logger = AgentLog(agent_name) + + type: str + agent_name: ClassVar[str] + agent_settings = Optional[AgentSettings] = None + history: List[AgentHistoryItem] = [] + logger: AgentLog = AgentLog(agent_name) + + # def __init__( + # self, + # agent_name: Optional[str], + # action_space: Optional[ActionManager], + # observation_space: Optional[ObservationManager], + # reward_function: Optional[RewardFunction], + # agent_settings: Optional[AgentSettings] = None, + # ) -> None: + # """ + # Initialize an agent. + + # :param agent_name: Unique string identifier for the agent, for reporting and multi-agent purposes. + # :type agent_name: Optional[str] + # :param action_space: Action space for the agent. + # :type action_space: Optional[ActionManager] + # :param observation_space: Observation space for the agent. + # :type observation_space: Optional[ObservationSpace] + # :param reward_function: Reward function for the agent. + # :type reward_function: Optional[RewardFunction] + # :param agent_settings: Configurable Options for Abstracted Agents + # :type agent_settings: Optional[AgentSettings] + # """ + # self.agent_name: str = agent_name or "unnamed_agent" + # self.action_manager: Optional[ActionManager] = action_space + # self.observation_manager: Optional[ObservationManager] = observation_space + # self.reward_function: Optional[RewardFunction] = reward_function + # self.agent_settings = agent_settings or AgentSettings() + # self.history: List[AgentHistoryItem] = [] + # self.logger = AgentLog(agent_name) + + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Cannot create a new agent under reserved name {identifier}") + cls._registry[identifier] = cls + + @classmethod + def from_config(cls, config: Dict) -> "AbstractAgent": + """Creates an agent component from a configuration dictionary.""" + return cls(config=cls.ConfigSchema(**config)) def update_observation(self, state: Dict) -> ObsType: """ @@ -130,7 +172,7 @@ class AbstractAgent(ABC): state : dict state directly from simulation.describe_state output : dict state according to CAOS. """ - return self.observation_manager.update(state) + return self.config.observation_manager.update(state) def update_reward(self, state: Dict) -> float: """ @@ -141,7 +183,7 @@ class AbstractAgent(ABC): :return: Reward from the state. :rtype: float """ - return self.reward_function.update(state=state, last_action_response=self.history[-1]) + return self.config.reward_function.update(state=state, last_action_response=self.history[-1]) @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -165,14 +207,14 @@ class AbstractAgent(ABC): # this will take something like APPLICATION.EXECUTE and add things like target_ip_address in simulator. # therefore the execution definition needs to be a mapping from CAOS into SIMULATOR """Format action into format expected by the simulator, and apply execution definition if applicable.""" - request = self.action_manager.form_request(action_identifier=action, action_options=options) + request = self.config.action_manager.form_request(action_identifier=action, action_options=options) return request def process_action_response( self, timestep: int, action: str, parameters: Dict[str, Any], request: RequestFormat, response: RequestResponse ) -> None: """Process the response from the most recent action.""" - self.history.append( + self.config.history.append( AgentHistoryItem( timestep=timestep, action=action, parameters=parameters, request=request, response=response ) @@ -180,10 +222,10 @@ class AbstractAgent(ABC): def save_reward_to_history(self) -> None: """Update the most recent history item with the reward value.""" - self.history[-1].reward = self.reward_function.current_reward + self.config.history[-1].reward = self.config.reward_function.current_reward -class AbstractScriptedAgent(AbstractAgent): +class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): """Base class for actors which generate their own behaviour.""" @abstractmethod @@ -192,26 +234,34 @@ class AbstractScriptedAgent(AbstractAgent): return super().get_action(obs=obs, timestep=timestep) -class ProxyAgent(AbstractAgent): +class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): """Agent that sends observations to an RL model and receives actions from that model.""" - def __init__( - self, - agent_name: Optional[str], - action_space: Optional[ActionManager], - observation_space: Optional[ObservationManager], - reward_function: Optional[RewardFunction], - agent_settings: Optional[AgentSettings] = None, - ) -> None: - super().__init__( - agent_name=agent_name, - action_space=action_space, - observation_space=observation_space, - reward_function=reward_function, - ) - self.most_recent_action: ActType - self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False - self.action_masking: bool = agent_settings.action_masking if agent_settings else False + class ConfigSchema(AbstractAgent.ConfigSchema): + """Configuration Schema for Proxy Agent.""" + + agent_settings = Union[AgentSettings | None] = None + most_reason_action: ActType + flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False + action_masking: bool = agent_settings.action_masking if agent_settings else False + + # def __init__( + # self, + # agent_name: Optional[str], + # action_space: Optional[ActionManager], + # observation_space: Optional[ObservationManager], + # reward_function: Optional[RewardFunction], + # agent_settings: Optional[AgentSettings] = None, + # ) -> None: + # super().__init__( + # agent_name=agent_name, + # action_space=action_space, + # observation_space=observation_space, + # reward_function=reward_function, + # ) + # self.most_recent_action: ActType + # self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False + # self.action_masking: bool = agent_settings.action_masking if agent_settings else False def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ @@ -224,7 +274,7 @@ class ProxyAgent(AbstractAgent): :return: Action to be taken in CAOS format. :rtype: Tuple[str, Dict] """ - return self.action_manager.get_action(self.most_recent_action) + return self.config.action_manager.get_action(self.most_recent_action) def store_action(self, action: ActType): """ @@ -232,4 +282,4 @@ class ProxyAgent(AbstractAgent): The environment is responsible for calling this method when it receives an action from the agent policy. """ - self.most_recent_action = action + self.config.most_recent_action = action diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 129fac1a..55b2d08b 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -7,12 +7,22 @@ from gymnasium.core import ObsType from primaite.game.agent.interface import AbstractScriptedAgent -class DataManipulationAgent(AbstractScriptedAgent): +class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation_Agent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" next_execution_timestep: int = 0 starting_node_idx: int = 0 + config: "DataManipulationAgent.ConfigSchema" + + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration Schema for DataManipulationAgent.""" + + # TODO: Could be worth moving this to a "AbstractTAPAgent" + starting_node_name: str + starting_application_name: str + next_execution_timestep: int + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setup_agent() @@ -38,12 +48,15 @@ class DataManipulationAgent(AbstractScriptedAgent): :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: - self.logger.debug(msg="Performing do NOTHING") - return "DONOTHING", {} + self.logger.debug(msg="Performing do nothing action") + return "do_nothing", {} self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) self.logger.info(msg="Performing a data manipulation attack!") - return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} + return "node_application_execute", { + "node_name": self.config.starting_node_name, + "application_name": self.config.starting_application_name, + } def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index cd44644f..b8df7838 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -12,7 +12,7 @@ from primaite.game.agent.observations.observation_manager import ObservationMana from primaite.game.agent.rewards import RewardFunction -class ProbabilisticAgent(AbstractScriptedAgent): +class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" class Settings(pydantic.BaseModel): diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index df9273f7..99b8a1e9 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -11,7 +11,7 @@ from primaite.game.agent.observations.observation_manager import ObservationMana from primaite.game.agent.rewards import RewardFunction -class RandomAgent(AbstractScriptedAgent): +class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): """Agent that ignores its observation and acts completely at random.""" def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -27,7 +27,7 @@ class RandomAgent(AbstractScriptedAgent): return self.action_manager.get_action(self.action_manager.space.sample()) -class PeriodicAgent(AbstractScriptedAgent): +class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): """Agent that does nothing most of the time, but executes application at regular intervals (with variance).""" class Settings(BaseModel): diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index c4f6062a..78cb9293 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -1,4 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations + import random from typing import Dict, Tuple @@ -7,20 +9,27 @@ from gymnasium.core import ObsType from primaite.game.agent.interface import AbstractScriptedAgent -class TAP001(AbstractScriptedAgent): +class TAP001(AbstractScriptedAgent, identifier="TAP001"): """ TAP001 | Mobile Malware -- Ransomware Variant. Scripted Red Agent. Capable of one action; launching the kill-chain (Ransomware Application) """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setup_agent() + # TODO: Link with DataManipulationAgent via a parent "TAP" agent class. - next_execution_timestep: int = 0 - starting_node_idx: int = 0 - installed: bool = False + config: "TAP001.ConfigSchema" + + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration Schema for TAP001 Agent.""" + + starting_node_name: str + next_execution_timestep: int = 0 + installed: bool = False + + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + # self.setup_agent() def _set_next_execution_timestep(self, timestep: int) -> None: """Set the next execution timestep with a configured random variance. @@ -28,9 +37,9 @@ class TAP001(AbstractScriptedAgent): :param timestep: The timestep to add variance to. """ random_timestep_increment = random.randint( - -self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance + -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance ) - self.next_execution_timestep = timestep + random_timestep_increment + self.config.next_execution_timestep = timestep + random_timestep_increment def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Waits until a specific timestep, then attempts to execute the ransomware application. @@ -45,28 +54,28 @@ class TAP001(AbstractScriptedAgent): :return: Action formatted in CAOS format :rtype: Tuple[str, Dict] """ - if timestep < self.next_execution_timestep: - return "DONOTHING", {} + if timestep < self.config.next_execution_timestep: + return "do_nothing", {} - self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) + self._set_next_execution_timestep(timestep + self.config.agent_settings.start_settings.frequency) - if not self.installed: - self.installed = True - return "NODE_APPLICATION_INSTALL", { - "node_id": self.starting_node_idx, + if not self.config.installed: + self.config.installed = True + return "node_application_install", { + "node_name": self.config.starting_node_name, "application_name": "RansomwareScript", } - return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} + return "node_application_execute", {"node_name": self.config.starting_node_name, "application_id": 0} def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" self._select_start_node() - self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) - for n, act in self.action_manager.action_map.items(): - if not act[0] == "NODE_APPLICATION_INSTALL": + self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) + for n, act in self.config.action_manager.action_map.items(): + if not act[0] == "node_application_install": continue - if act[1]["node_id"] == self.starting_node_idx: + if act[1]["node_name"] == self.config.starting_node_name: self.ip_address = act[1]["ip_address"] return raise RuntimeError("TAP001 agent could not find database server ip address in action map") @@ -74,5 +83,7 @@ class TAP001(AbstractScriptedAgent): def _select_start_node(self) -> None: """Set the starting starting node of the agent to be a random node from this agent's action manager.""" # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.action_manager.node_names) + num_nodes = len(self.config.action_manager.node_names) + # TODO: Change this to something? self.starting_node_idx = random.randint(0, num_nodes - 1) + self.logger.debug(f"Selected Starting node ID: {self.starting_node_idx}") diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c8fbac4e..2ef7b1c5 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -547,6 +547,26 @@ class PrimaiteGame: reward_function = RewardFunction.from_config(reward_function_cfg) # CREATE AGENT + + # TODO: MAKE THIS BIT WORK AND NOT THE IF/ELSE CHAIN OF HORRORS + + # Pass through: + # config + # action manager + # observation_manager + # reward_function + + new_agent_cfg = { + "action_manager": action_space, + "agent_name": agent_cfg["ref"], + "observation_manager": obs_space, + "agent_settings": agent_cfg.get("agent_settings", {}), + "reward_function": reward_function, + } + new_agent_cfg = agent_cfg["settings"] + # new_agent_cfg.update{} + new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=new_agent_cfg) + if agent_type == "ProbabilisticAgent": # TODO: implement non-random agents and fix this parsing settings = agent_cfg.get("agent_settings", {}) diff --git a/tests/conftest.py b/tests/conftest.py index 64fe0699..efdb515e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -264,7 +264,7 @@ def example_network() -> Network: return network -class ControlledAgent(AbstractAgent): +class ControlledAgent(AbstractAgent, identifier="Controlled_Agent"): """Agent that can be controlled by the tests.""" def __init__( From 75d4ef2dfd0ac0e9a5bc9cc45f96f41bd8f04977 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 20 Nov 2024 17:51:05 +0000 Subject: [PATCH 068/224] #2869 - eod commit. Updates to AbstractAgent.from_config, and some minor tweaks to PrimaiteGame --- src/primaite/game/agent/interface.py | 22 +++++++++++++------ .../scripted_agents/probabilistic_agent.py | 4 ++-- .../game/agent/scripted_agents/tap001.py | 2 +- src/primaite/game/game.py | 19 ++++++++-------- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 7adaab69..88557956 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -97,11 +97,12 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} - config: "AbstractAgent.ConfigSchema" action_manager: Optional[ActionManager] observation_manager: Optional[ObservationManager] reward_function: Optional[RewardFunction] + config: "AbstractAgent.ConfigSchema" + class ConfigSchema(BaseModel): """ Configuration Schema for AbstractAgents. @@ -163,7 +164,14 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": """Creates an agent component from a configuration dictionary.""" - return cls(config=cls.ConfigSchema(**config)) + obj = cls(config=cls.ConfigSchema(**config)) + + # Pull managers out of config section for ease of use (?) + obj.observation_manager = obj.config.observation_manager + obj.action_manager = obj.config.action_manager + obj.reward_function = obj.config.reward_function + + return obj def update_observation(self, state: Dict) -> ObsType: """ @@ -172,7 +180,7 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): state : dict state directly from simulation.describe_state output : dict state according to CAOS. """ - return self.config.observation_manager.update(state) + return self.observation_manager.update(state) def update_reward(self, state: Dict) -> float: """ @@ -183,7 +191,7 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): :return: Reward from the state. :rtype: float """ - return self.config.reward_function.update(state=state, last_action_response=self.history[-1]) + return self.reward_function.update(state=state, last_action_response=self.config.history[-1]) @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -201,13 +209,13 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): """ # in RL agent, this method will send CAOS observation to RL agent, then receive a int 0-39, # then use a bespoke conversion to take 1-40 int back into CAOS action - return ("DO_NOTHING", {}) + return ("do_nothing", {}) def format_request(self, action: Tuple[str, Dict], options: Dict[str, int]) -> List[str]: # this will take something like APPLICATION.EXECUTE and add things like target_ip_address in simulator. # therefore the execution definition needs to be a mapping from CAOS into SIMULATOR """Format action into format expected by the simulator, and apply execution definition if applicable.""" - request = self.config.action_manager.form_request(action_identifier=action, action_options=options) + request = self.action_manager.form_request(action_identifier=action, action_options=options) return request def process_action_response( @@ -222,7 +230,7 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): def save_reward_to_history(self) -> None: """Update the most recent history item with the reward value.""" - self.config.history[-1].reward = self.config.reward_function.current_reward + self.config.history[-1].reward = self.reward_function.current_reward class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index b8df7838..02ac5931 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -15,7 +15,7 @@ from primaite.game.agent.rewards import RewardFunction class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" - class Settings(pydantic.BaseModel): + class ConfigSchema(pydantic.BaseModel): """Config schema for Probabilistic agent settings.""" model_config = pydantic.ConfigDict(extra="forbid") @@ -60,7 +60,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent" # The random number seed for np.random is dependent on whether a random number seed is set # in the config file. If there is one it is processed by set_random_seed() in environment.py # and as a consequence the the sequence of rng_seed's used here will be repeatable. - self.settings = ProbabilisticAgent.Settings(**settings) + self.settings = ProbabilisticAgent.ConfigSchema(**settings) rng_seed = np.random.randint(0, 65535) self.rng = np.random.default_rng(rng_seed) diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index 78cb9293..7365fd88 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -16,7 +16,7 @@ class TAP001(AbstractScriptedAgent, identifier="TAP001"): Scripted Red Agent. Capable of one action; launching the kill-chain (Ransomware Application) """ - # TODO: Link with DataManipulationAgent via a parent "TAP" agent class. + # TODO: Link with DataManipulationAgent class via a parent "TAP" agent class. config: "TAP001.ConfigSchema" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 2ef7b1c5..d7e2ed4a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -555,17 +555,16 @@ class PrimaiteGame: # action manager # observation_manager # reward_function - - new_agent_cfg = { - "action_manager": action_space, - "agent_name": agent_cfg["ref"], - "observation_manager": obs_space, - "agent_settings": agent_cfg.get("agent_settings", {}), - "reward_function": reward_function, - } - new_agent_cfg = agent_cfg["settings"] + agent_config = agent_cfg.get("agent_settings", {}) + agent_config.update({"action_manager": action_space, + "observation_manager": obs_space, + "reward_function":reward_function}) # new_agent_cfg.update{} - new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=new_agent_cfg) + new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) + + # If blue agent is created, add to game.rl_agents + if agent_type == "ProxyAgent": + game.rl_agents[agent_cfg["ref"]] = new_agent if agent_type == "ProbabilisticAgent": # TODO: implement non-random agents and fix this parsing From 7435a4dee8ff59bcafddeffe21c55cc245a63d95 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 21 Nov 2024 14:45:35 +0000 Subject: [PATCH 069/224] #2869 - Commit before changing branches. Addition of properties to Agent classes and removal of if/else chain in game.py --- src/primaite/game/agent/interface.py | 63 +++++---------- .../scripted_agents/data_manipulation_bot.py | 23 ++++-- src/primaite/game/game.py | 80 +++---------------- 3 files changed, 45 insertions(+), 121 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 88557956..962e13f7 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -96,11 +96,6 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): """Base class for scripted and RL agents.""" _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} - - action_manager: Optional[ActionManager] - observation_manager: Optional[ObservationManager] - reward_function: Optional[RewardFunction] - config: "AbstractAgent.ConfigSchema" class ConfigSchema(BaseModel): @@ -121,39 +116,12 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): type: str agent_name: ClassVar[str] - agent_settings = Optional[AgentSettings] = None - history: List[AgentHistoryItem] = [] logger: AgentLog = AgentLog(agent_name) - - # def __init__( - # self, - # agent_name: Optional[str], - # action_space: Optional[ActionManager], - # observation_space: Optional[ObservationManager], - # reward_function: Optional[RewardFunction], - # agent_settings: Optional[AgentSettings] = None, - # ) -> None: - # """ - # Initialize an agent. - - # :param agent_name: Unique string identifier for the agent, for reporting and multi-agent purposes. - # :type agent_name: Optional[str] - # :param action_space: Action space for the agent. - # :type action_space: Optional[ActionManager] - # :param observation_space: Observation space for the agent. - # :type observation_space: Optional[ObservationSpace] - # :param reward_function: Reward function for the agent. - # :type reward_function: Optional[RewardFunction] - # :param agent_settings: Configurable Options for Abstracted Agents - # :type agent_settings: Optional[AgentSettings] - # """ - # self.agent_name: str = agent_name or "unnamed_agent" - # self.action_manager: Optional[ActionManager] = action_space - # self.observation_manager: Optional[ObservationManager] = observation_space - # self.reward_function: Optional[RewardFunction] = reward_function - # self.agent_settings = agent_settings or AgentSettings() - # self.history: List[AgentHistoryItem] = [] - # self.logger = AgentLog(agent_name) + history: List[AgentHistoryItem] = [] + action_manager: Optional[ActionManager] = None + observation_manager: Optional[ObservationManager] = None + reward_function: Optional[RewardFunction] = None + agent_settings = Optional[AgentSettings] = None def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -161,16 +129,25 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): raise ValueError(f"Cannot create a new agent under reserved name {identifier}") cls._registry[identifier] = cls + @property + def observation_manager(self) -> ObservationManager: + """Returns the agents observation manager.""" + return self.config.observation_manager + + @property + def action_manager(self) -> ActionManager: + """Returns the agents action manager.""" + return self.config.action_manager + + @property + def reward_function(self) -> RewardFunction: + """Returns the agents reward function.""" + return self.config.reward_function + @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": """Creates an agent component from a configuration dictionary.""" obj = cls(config=cls.ConfigSchema(**config)) - - # Pull managers out of config section for ease of use (?) - obj.observation_manager = obj.config.observation_manager - obj.action_manager = obj.config.action_manager - obj.reward_function = obj.config.reward_function - return obj def update_observation(self, state: Dict) -> ObsType: diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 55b2d08b..2f49decd 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -10,9 +10,6 @@ from primaite.game.agent.interface import AbstractScriptedAgent class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation_Agent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" - next_execution_timestep: int = 0 - starting_node_idx: int = 0 - config: "DataManipulationAgent.ConfigSchema" class ConfigSchema(AbstractScriptedAgent.ConfigSchema): @@ -27,13 +24,23 @@ class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation super().__init__(*args, **kwargs) self.setup_agent() + @property + def next_execution_timestep(self): + """Returns the agents next execution timestep.""" + return self.config.next_execution_timestep + + @property + def starting_node_name(self): + """Returns the agents starting node name.""" + return self.config.starting_node_name + def _set_next_execution_timestep(self, timestep: int) -> None: """Set the next execution timestep with a configured random variance. :param timestep: The timestep to add variance to. """ random_timestep_increment = random.randint( - -self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance + -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance ) self.next_execution_timestep = timestep + random_timestep_increment @@ -48,11 +55,11 @@ class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: - self.logger.debug(msg="Performing do nothing action") + self.config.logger.debug(msg="Performing do nothing action") return "do_nothing", {} - self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) - self.logger.info(msg="Performing a data manipulation attack!") + self._set_next_execution_timestep(timestep + self.config.agent_settings.start_settings.frequency) + self.config.logger.info(msg="Performing a data manipulation attack!") return "node_application_execute", { "node_name": self.config.starting_node_name, "application_name": self.config.starting_application_name, @@ -68,4 +75,4 @@ class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation # we are assuming that every node in the node manager has a data manipulation application at idx 0 num_nodes = len(self.action_manager.node_names) self.starting_node_idx = random.randint(0, num_nodes - 1) - self.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}") + self.config.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}") diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index d7e2ed4a..03f3feec 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -11,10 +11,6 @@ from primaite.game.agent.actions import ActionManager from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction, SharedReward -from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent -from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent -from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent -from primaite.game.agent.scripted_agents.tap001 import TAP001 from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.creation import NetworkNodeAdder @@ -178,7 +174,7 @@ class PrimaiteGame: obs = agent.observation_manager.current_observation action_choice, parameters = agent.get_action(obs, timestep=self.step_counter) if SIM_OUTPUT.save_agent_logs: - agent.logger.debug(f"Chosen Action: {action_choice}") + agent.config.logger.debug(f"Chosen Action: {action_choice}") request = agent.format_request(action_choice, parameters) response = self.simulation.apply_request(request) agent.process_action_response( @@ -548,77 +544,21 @@ class PrimaiteGame: # CREATE AGENT - # TODO: MAKE THIS BIT WORK AND NOT THE IF/ELSE CHAIN OF HORRORS - - # Pass through: - # config - # action manager - # observation_manager - # reward_function agent_config = agent_cfg.get("agent_settings", {}) - agent_config.update({"action_manager": action_space, - "observation_manager": obs_space, - "reward_function":reward_function}) + agent_config.update( + {"action_manager": action_space, "observation_manager": obs_space, "reward_function": reward_function} + ) # new_agent_cfg.update{} - new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) - - # If blue agent is created, add to game.rl_agents - if agent_type == "ProxyAgent": - game.rl_agents[agent_cfg["ref"]] = new_agent - - if agent_type == "ProbabilisticAgent": - # TODO: implement non-random agents and fix this parsing - settings = agent_cfg.get("agent_settings", {}) - new_agent = ProbabilisticAgent( - agent_name=agent_cfg["ref"], - action_space=action_space, - observation_space=obs_space, - reward_function=reward_function, - settings=settings, - ) - elif agent_type == "PeriodicAgent": - settings = PeriodicAgent.Settings(**agent_cfg.get("settings", {})) - new_agent = PeriodicAgent( - agent_name=agent_cfg["ref"], - action_space=action_space, - observation_space=obs_space, - reward_function=reward_function, - settings=settings, - ) - - elif agent_type == "ProxyAgent": - agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) - new_agent = ProxyAgent( - agent_name=agent_cfg["ref"], - action_space=action_space, - observation_space=obs_space, - reward_function=reward_function, - agent_settings=agent_settings, - ) - game.rl_agents[agent_cfg["ref"]] = new_agent - elif agent_type == "RedDatabaseCorruptingAgent": - agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) - - new_agent = DataManipulationAgent( - agent_name=agent_cfg["ref"], - action_space=action_space, - observation_space=obs_space, - reward_function=reward_function, - agent_settings=agent_settings, - ) - elif agent_type == "TAP001": - agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) - new_agent = TAP001( - agent_name=agent_cfg["ref"], - action_space=action_space, - observation_space=obs_space, - reward_function=reward_function, - agent_settings=agent_settings, - ) + if agent_type in AbstractAgent._registry: + new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) + # If blue agent is created, add to game.rl_agents + if agent_type == "ProxyAgent": + game.rl_agents[agent_cfg["ref"]] = new_agent else: msg = f"Configuration error: {agent_type} is not a valid agent type." _LOGGER.error(msg) raise ValueError(msg) + game.agents[agent_cfg["ref"]] = new_agent # Validate that if any agents are sharing rewards, they aren't forming an infinite loop. From 917386d63808f1138aae4f584d4d6b800c30acd1 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 27 Nov 2024 15:29:51 +0000 Subject: [PATCH 070/224] #2869 - Agents Refactor --- src/primaite/game/agent/interface.py | 25 +++------ .../agent/scripted_agents/abstract_tap.py | 34 ++++++++++++ .../scripted_agents/data_manipulation_bot.py | 29 +++------- .../agent/scripted_agents/random_agent.py | 43 +++++++-------- .../game/agent/scripted_agents/tap001.py | 53 ++++++++----------- src/primaite/game/game.py | 2 +- tests/conftest.py | 20 +++---- 7 files changed, 95 insertions(+), 111 deletions(-) create mode 100644 src/primaite/game/agent/scripted_agents/abstract_tap.py diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 962e13f7..402c7ce2 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -222,6 +222,8 @@ class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent") class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): """Agent that sends observations to an RL model and receives actions from that model.""" + config: "ProxyAgent.ConfigSchema" + class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Proxy Agent.""" @@ -230,23 +232,10 @@ class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False action_masking: bool = agent_settings.action_masking if agent_settings else False - # def __init__( - # self, - # agent_name: Optional[str], - # action_space: Optional[ActionManager], - # observation_space: Optional[ObservationManager], - # reward_function: Optional[RewardFunction], - # agent_settings: Optional[AgentSettings] = None, - # ) -> None: - # super().__init__( - # agent_name=agent_name, - # action_space=action_space, - # observation_space=observation_space, - # reward_function=reward_function, - # ) - # self.most_recent_action: ActType - # self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False - # self.action_masking: bool = agent_settings.action_masking if agent_settings else False + @property + def most_recent_action(self) -> ActType: + """Convenience method to access the agents most recent action.""" + return self.config.most_recent_action def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ @@ -267,4 +256,4 @@ class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): The environment is responsible for calling this method when it receives an action from the agent policy. """ - self.config.most_recent_action = action + self.most_recent_action = action diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py new file mode 100644 index 00000000..2523f9f7 --- /dev/null +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -0,0 +1,34 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations + +import random +from abc import abstractmethod + +from primaite.game.agent.interface import AbstractScriptedAgent + + +class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): + """Base class for TAP agents to inherit from.""" + + config: "AbstractTAPAgent.ConfigSchema" + + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration schema for Abstract TAP agents.""" + + starting_node_name: str + next_execution_timestep: int + + @abstractmethod + def setup_agent(self) -> None: + """Set up agent.""" + pass + + def _set_next_execution_timestep(self, timestep: int) -> None: + """Set the next execution timestep with a configured random variance. + + :param timestep: The timestep to add variance to. + """ + random_timestep_increment = random.randint( + -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance + ) + self.config.next_execution_timestep = timestep + random_timestep_increment diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 2f49decd..b375da66 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -4,46 +4,29 @@ from typing import Dict, Tuple from gymnasium.core import ObsType -from primaite.game.agent.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent -class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation_Agent"): +class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" config: "DataManipulationAgent.ConfigSchema" - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" - # TODO: Could be worth moving this to a "AbstractTAPAgent" - starting_node_name: str starting_application_name: str - next_execution_timestep: int - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setup_agent() @property - def next_execution_timestep(self): + def next_execution_timestep(self) -> int: """Returns the agents next execution timestep.""" return self.config.next_execution_timestep @property - def starting_node_name(self): + def starting_node_name(self) -> str: """Returns the agents starting node name.""" return self.config.starting_node_name - def _set_next_execution_timestep(self, timestep: int) -> None: - """Set the next execution timestep with a configured random variance. - - :param timestep: The timestep to add variance to. - """ - random_timestep_increment = random.randint( - -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance - ) - self.next_execution_timestep = timestep + random_timestep_increment - def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Waits until a specific timestep, then attempts to execute its data manipulation application. @@ -68,7 +51,7 @@ class DataManipulationAgent(AbstractScriptedAgent, identifier="Data_Manipulation def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" self._select_start_node() - self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) + self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) def _select_start_node(self) -> None: """Set the starting starting node of the agent to be a random node from this agent's action manager.""" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 99b8a1e9..a9082eda 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -1,14 +1,10 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import random -from typing import Dict, Optional, Tuple +from typing import Dict, Tuple from gymnasium.core import ObsType -from pydantic import BaseModel -from primaite.game.agent.actions import ActionManager from primaite.game.agent.interface import AbstractScriptedAgent -from primaite.game.agent.observations.observation_manager import ObservationManager -from primaite.game.agent.rewards import RewardFunction class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): @@ -30,8 +26,10 @@ class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): """Agent that does nothing most of the time, but executes application at regular intervals (with variance).""" - class Settings(BaseModel): - """Configuration values for when an agent starts performing actions.""" + config: "PeriodicAgent.ConfigSchema" + + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration Schema for Periodic Agent.""" start_step: int = 20 "The timestep at which an agent begins performing it's actions." @@ -43,25 +41,20 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): "The amount the frequency can randomly change to." max_executions: int = 999999 "Maximum number of times the agent can execute its action." + num_executions: int = 0 + """Number of times the agent has executed an action.""" + next_execution_timestep: int = 0 + """Timestep of the next action execution by the agent.""" - def __init__( - self, - agent_name: str, - action_space: ActionManager, - observation_space: ObservationManager, - reward_function: RewardFunction, - settings: Optional[Settings] = None, - ) -> None: - """Initialise PeriodicAgent.""" - super().__init__( - agent_name=agent_name, - action_space=action_space, - observation_space=observation_space, - reward_function=reward_function, - ) - self.settings = settings or PeriodicAgent.Settings() - self._set_next_execution_timestep(timestep=self.settings.start_step, variance=self.settings.start_variance) - self.num_executions = 0 + @property + def num_executions(self) -> int: + """Convenience method for accessing num_executions from config.""" + return self.config.num_executions + + @property + def next_execution_timestep(self) -> int: + """Convenience method for accessing next_execution_timestep from config.""" + return self.config.next_execution_timestep def _set_next_execution_timestep(self, timestep: int, variance: int) -> None: """Set the next execution timestep with a configured random variance. diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index 7365fd88..d3a82bbe 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -4,51 +4,41 @@ from __future__ import annotations import random from typing import Dict, Tuple -from gymnasium.core import ObsType - -from primaite.game.agent.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent -class TAP001(AbstractScriptedAgent, identifier="TAP001"): +class TAP001(AbstractTAPAgent, identifier="TAP001"): """ TAP001 | Mobile Malware -- Ransomware Variant. Scripted Red Agent. Capable of one action; launching the kill-chain (Ransomware Application) """ - # TODO: Link with DataManipulationAgent class via a parent "TAP" agent class. - config: "TAP001.ConfigSchema" - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for TAP001 Agent.""" - starting_node_name: str - next_execution_timestep: int = 0 installed: bool = False - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - # self.setup_agent() + @property + def starting_node_name(self) -> str: + """Node that TAP001 starts from.""" + return self.config.starting_node_name - def _set_next_execution_timestep(self, timestep: int) -> None: - """Set the next execution timestep with a configured random variance. + @classmethod + def from_config(cls, config: Dict) -> TAP001: + """Override the base from_config method to ensure successful agent setup.""" + obj: TAP001 = cls(config=cls.ConfigSchema(**config)) + obj.setup_agent() + return obj - :param timestep: The timestep to add variance to. - """ - random_timestep_increment = random.randint( - -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance - ) - self.config.next_execution_timestep = timestep + random_timestep_increment - - def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: + def get_action(self, timestep: int) -> Tuple[str, Dict]: """Waits until a specific timestep, then attempts to execute the ransomware application. This application acts a wrapper around the kill-chain, similar to green-analyst and the previous UC2 data manipulation bot. - :param obs: Current observation for this agent. - :type obs: ObsType :param timestep: The current simulation timestep, used for scheduling actions :type timestep: int :return: Action formatted in CAOS format @@ -62,11 +52,14 @@ class TAP001(AbstractScriptedAgent, identifier="TAP001"): if not self.config.installed: self.config.installed = True return "node_application_install", { - "node_name": self.config.starting_node_name, + "node_name": self.starting_node_name, "application_name": "RansomwareScript", } - return "node_application_execute", {"node_name": self.config.starting_node_name, "application_id": 0} + return "node_application_execute", { + "node_name": self.starting_node_name, + "application_name": "RansomwareScript", + } def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" @@ -75,7 +68,7 @@ class TAP001(AbstractScriptedAgent, identifier="TAP001"): for n, act in self.config.action_manager.action_map.items(): if not act[0] == "node_application_install": continue - if act[1]["node_name"] == self.config.starting_node_name: + if act[1]["node_name"] == self.starting_node_name: self.ip_address = act[1]["ip_address"] return raise RuntimeError("TAP001 agent could not find database server ip address in action map") @@ -84,6 +77,6 @@ class TAP001(AbstractScriptedAgent, identifier="TAP001"): """Set the starting starting node of the agent to be a random node from this agent's action manager.""" # we are assuming that every node in the node manager has a data manipulation application at idx 0 num_nodes = len(self.config.action_manager.node_names) - # TODO: Change this to something? - self.starting_node_idx = random.randint(0, num_nodes - 1) - self.logger.debug(f"Selected Starting node ID: {self.starting_node_idx}") + starting_node_idx = random.randint(0, num_nodes - 1) + self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] + self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 03f3feec..79587e47 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict from primaite import DEFAULT_BANDWIDTH, getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent +from primaite.game.agent.interface import AbstractAgent, ProxyAgent from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction, SharedReward from primaite.game.science import graph_has_cycle, topological_sort diff --git a/tests/conftest.py b/tests/conftest.py index efdb515e..b24c4c76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -267,20 +267,12 @@ def example_network() -> Network: class ControlledAgent(AbstractAgent, identifier="Controlled_Agent"): """Agent that can be controlled by the tests.""" - def __init__( - self, - agent_name: str, - action_space: ActionManager, - observation_space: ObservationManager, - reward_function: RewardFunction, - ) -> None: - super().__init__( - agent_name=agent_name, - action_space=action_space, - observation_space=observation_space, - reward_function=reward_function, - ) - self.most_recent_action: Tuple[str, Dict] + config: "ControlledAgent.ConfigSchema" + + class ConfigSchema(AbstractAgent.ConfigSchema): + """Configuration Schema for Abstract Agent used in tests.""" + + most_recent_action: Tuple[str, Dict] def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: """Return the agent's most recent action, formatted in CAOS format.""" From 1798ec6fe0168807d2ed1b2f84d8ccaf0198de89 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 5 Dec 2024 14:00:44 +0000 Subject: [PATCH 071/224] #2869 - Commit before switching branches. Changes to make pydantic happy with AgentLog --- src/primaite/game/agent/agent_log.py | 18 +++++++++++----- src/primaite/game/agent/interface.py | 11 ++++++++-- .../agent/scripted_agents/abstract_tap.py | 9 ++++++++ .../scripted_agents/data_manipulation_bot.py | 12 +++++------ .../scripted_agents/probabilistic_agent.py | 1 + .../agent/scripted_agents/random_agent.py | 13 +++++++++--- .../game/agent/scripted_agents/tap001.py | 21 ++++++------------- src/primaite/game/game.py | 1 - tests/conftest.py | 1 + 9 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 62ef4884..c292ba4f 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -1,8 +1,11 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import logging +from abc import ABC from pathlib import Path +from typing import Optional from prettytable import MARKDOWN, PrettyTable +from pydantic import BaseModel from primaite.simulator import LogLevel, SIM_OUTPUT @@ -18,22 +21,27 @@ class _NotJSONFilter(logging.Filter): return not record.getMessage().startswith("{") and not record.getMessage().endswith("}") -class AgentLog: +class AgentLog(BaseModel): """ A Agent Log class is a simple logger dedicated to managing and writing logging updates and information for an agent. Each log message is written to a file located at: /agent_name/agent_name.log """ - def __init__(self, agent_name: str): + agent_name: str = "unnamed_agent" + current_episode: int = 1 + current_timestep: int = 0 + + def __init__(self, agent_name: Optional[str]): """ Constructs a Agent Log instance for a given hostname. :param hostname: The hostname associated with the system logs being recorded. """ - self.agent_name = agent_name - self.current_episode: int = 1 - self.current_timestep: int = 0 + super().__init__() + self.agent_name = agent_name or "unnamed_agent" + # self.current_episode: int = 1 + # self.current_timestep: int = 0 self.setup_logger() @property diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 402c7ce2..1b9dbcd6 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -97,6 +97,7 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} config: "AbstractAgent.ConfigSchema" + agent_name = "Abstract_Agent" class ConfigSchema(BaseModel): """ @@ -115,13 +116,13 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): """ type: str - agent_name: ClassVar[str] + agent_name: str = "Abstact_Agent" logger: AgentLog = AgentLog(agent_name) history: List[AgentHistoryItem] = [] action_manager: Optional[ActionManager] = None observation_manager: Optional[ObservationManager] = None reward_function: Optional[RewardFunction] = None - agent_settings = Optional[AgentSettings] = None + agent_settings: Optional[AgentSettings] = None def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -213,6 +214,11 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): """Base class for actors which generate their own behaviour.""" + class ConfigSchema(AbstractAgent.ConfigSchema): + """Configuration Schema for AbstractScriptedAgents.""" + + agent_name: str = "Abstract_Scripted_Agent" + @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """Return an action to be taken in the environment.""" @@ -227,6 +233,7 @@ class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Proxy Agent.""" + agent_name: str = "Proxy_Agent" agent_settings = Union[AgentSettings | None] = None most_reason_action: ActType flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index 2523f9f7..19eeac1a 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -15,6 +15,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" + agent_name: str = "Abstract_TAP" starting_node_name: str next_execution_timestep: int @@ -32,3 +33,11 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance ) self.config.next_execution_timestep = timestep + random_timestep_increment + + def _select_start_node(self) -> None: + """Set the starting starting node of the agent to be a random node from this agent's action manager.""" + # we are assuming that every node in the node manager has a data manipulation application at idx 0 + num_nodes = len(self.config.action_manager.node_names) + starting_node_idx = random.randint(0, num_nodes - 1) + self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] + self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index b375da66..3a2dbdd2 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -16,6 +16,11 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen """Configuration Schema for DataManipulationAgent.""" starting_application_name: str + agent_name: str = "Data_Manipulation_Agent" + + def __init__(self) -> None: + """Meh.""" + self.setup_agent() @property def next_execution_timestep(self) -> int: @@ -52,10 +57,3 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen """Set the next execution timestep when the episode resets.""" self._select_start_node() self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) - - def _select_start_node(self) -> None: - """Set the starting starting node of the agent to be a random node from this agent's action manager.""" - # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.action_manager.node_names) - self.starting_node_idx = random.randint(0, num_nodes - 1) - self.config.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}") diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 02ac5931..c29719ac 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -18,6 +18,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent" class ConfigSchema(pydantic.BaseModel): """Config schema for Probabilistic agent settings.""" + agent_name: str = "Probabilistic_Agent" model_config = pydantic.ConfigDict(extra="forbid") """Strict validation.""" action_probabilities: Dict[int, float] diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index a9082eda..e11e3352 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -10,7 +10,12 @@ from primaite.game.agent.interface import AbstractScriptedAgent class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): """Agent that ignores its observation and acts completely at random.""" - def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration Schema for Random Agents.""" + + agent_name = "Random_Agent" + + def get_action(self) -> Tuple[str, Dict]: """Sample the action space randomly. :param obs: Current observation for this agent, not used in RandomAgent @@ -31,6 +36,8 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" + agent_name = "Periodic_Agent" + """Name of the agent.""" start_step: int = 20 "The timestep at which an agent begins performing it's actions." start_variance: int = 5 @@ -69,9 +76,9 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" - if timestep == self.next_execution_timestep and self.num_executions < self.settings.max_executions: + if timestep == self.next_execution_timestep and self.num_executions < self.config.max_executions: self.num_executions += 1 - self._set_next_execution_timestep(timestep + self.settings.frequency, self.settings.variance) + self._set_next_execution_timestep(timestep + self.config.frequency, self.config.variance) return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} return "DONOTHING", {} diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index d3a82bbe..3b7abe50 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -19,20 +19,19 @@ class TAP001(AbstractTAPAgent, identifier="TAP001"): class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for TAP001 Agent.""" + agent_name: str = "TAP001" installed: bool = False + def __init__(self) -> None: + """___init___ bruv. Restecpa.""" + super().__init__() + self.setup_agent() + @property def starting_node_name(self) -> str: """Node that TAP001 starts from.""" return self.config.starting_node_name - @classmethod - def from_config(cls, config: Dict) -> TAP001: - """Override the base from_config method to ensure successful agent setup.""" - obj: TAP001 = cls(config=cls.ConfigSchema(**config)) - obj.setup_agent() - return obj - def get_action(self, timestep: int) -> Tuple[str, Dict]: """Waits until a specific timestep, then attempts to execute the ransomware application. @@ -72,11 +71,3 @@ class TAP001(AbstractTAPAgent, identifier="TAP001"): self.ip_address = act[1]["ip_address"] return raise RuntimeError("TAP001 agent could not find database server ip address in action map") - - def _select_start_node(self) -> None: - """Set the starting starting node of the agent to be a random node from this agent's action manager.""" - # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.config.action_manager.node_names) - starting_node_idx = random.randint(0, num_nodes - 1) - self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] - self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 79587e47..9ef75fb9 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -558,7 +558,6 @@ class PrimaiteGame: msg = f"Configuration error: {agent_type} is not a valid agent type." _LOGGER.error(msg) raise ValueError(msg) - game.agents[agent_cfg["ref"]] = new_agent # Validate that if any agents are sharing rewards, they aren't forming an infinite loop. diff --git a/tests/conftest.py b/tests/conftest.py index b24c4c76..27032540 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -272,6 +272,7 @@ class ControlledAgent(AbstractAgent, identifier="Controlled_Agent"): class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Abstract Agent used in tests.""" + agent_name: str = "Controlled_Agent" most_recent_action: Tuple[str, Dict] def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: From be174b64774e10196bcc539536939f76fb78aa8f Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 6 Dec 2024 15:12:31 +0000 Subject: [PATCH 072/224] #2912 - Actioning review comments --- src/primaite/game/agent/actions/__init__.py | 1 + src/primaite/game/agent/actions/abstract.py | 2 - src/primaite/game/agent/actions/acl.py | 77 ++++++++++--------- .../game/agent/actions/application.py | 6 -- src/primaite/game/agent/actions/config.py | 17 +--- src/primaite/game/agent/actions/manager.py | 51 ++---------- 6 files changed, 53 insertions(+), 101 deletions(-) diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 7f054591..016a09ba 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -29,4 +29,5 @@ __all__ = ( "node", "service", "session", + "ActionManager", ) diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index cd14ef6d..ef22ec54 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -36,6 +36,4 @@ class AbstractAction(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAction": """Create an action component from a config dictionary.""" - if not config.get("type"): - config.update({"type": cls.__name__}) return cls(config=cls.ConfigSchema(**config)) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index e8ad59f5..fb18d025 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,5 +1,7 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import List, Literal, Union +from __future__ import annotations + +from typing import List from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -12,34 +14,48 @@ __all__ = ( ) -class ACLAbstractAction(AbstractAction, identifier="acl_abstract_action"): - """Base class for ACL actions.""" +class ACLAddRuleAbstractAction(AbstractAction, identifier="acl_add_rule_abstract_action"): + """Base abstract class for ACL add rule actions.""" + + config: ConfigSchema = "ACLAddRuleAbstractAction.ConfigSchema" class ConfigSchema(AbstractAction.ConfigSchema): - """Configuration Schema base for ACL abstract actions.""" + """Configuration Schema base for ACL add rule abstract actions.""" src_ip: str protocol_name: str + permission: str + position: int + src_ip: str + dst_ip: str + src_port: str + dst_port: str + src_wildcard: int + dst_wildcard: int -class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): +class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_abstract_action"): + """Base abstract class for ACL remove rule actions.""" + + config: ConfigSchema = "ACLRemoveRuleAbstractAction.ConfigSchema" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration Schema base for ACL remove rule abstract actions.""" + + src_ip: str + protocol_name: str + position: int + + +class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" config: "RouterACLAddRuleAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(ACLAddRuleAbstractAction.ConfigSchema): """Configuration Schema for RouterACLAddRuleAction.""" target_router: str - permission: str - protocol_name: str - position: int - src_ip: str - src_wildcard: int - source_port: str - dst_ip: str - dst_wildcard: int - dst_port: str @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: @@ -62,16 +78,15 @@ class RouterACLAddRuleAction(AbstractAction, identifier="router_acl_add_rule"): ] -class RouterACLRemoveRuleAction(AbstractAction, identifier="router_acl_remove_rule"): +class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, identifier="router_acl_remove_rule"): """Action which removes a rule from a router's ACL.""" config: "RouterACLRemoveRuleAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(ACLRemoveRuleAbstractAction.ConfigSchema): """Configuration schema for RouterACLRemoveRuleAction.""" target_router: str - position: int @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -79,31 +94,22 @@ class RouterACLRemoveRuleAction(AbstractAction, identifier="router_acl_remove_ru return ["network", "node", config.target_router, "acl", "remove_rule", config.position] -class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_rule"): +class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_acl_add_rule"): """Action which adds a rule to a firewall port's ACL.""" config: "FirewallACLAddRuleAction.ConfigSchema" - class ConfigSchema(ACLAbstractAction.ConfigSchema): + class ConfigSchema(ACLAddRuleAbstractAction.ConfigSchema): """Configuration schema for FirewallACLAddRuleAction.""" target_firewall_nodename: str firewall_port_name: str firewall_port_direction: str - position: int - permission: str - src_ip: str - dest_ip: str - src_port: str - dst_port: str - protocol_name: str - source_wildcard_id: int - dest_wildcard_id: int @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.protocol_name == None: + if config.protocol_name is None: return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS if config.src_ip == 0: return ["do_nothing"] # invalid formulation @@ -121,27 +127,26 @@ class FirewallACLAddRuleAction(ACLAbstractAction, identifier="firewall_acl_add_r config.permission, config.protocol_name, config.src_ip, - config.source_wildcard_id, + config.src_wildcard, config.src_port, - config.dest_ip, - config.dest_wildcard_id, + config.dst_ip, + config.dst_wildcard, config.dst_port, config.position, ] -class FirewallACLRemoveRuleAction(AbstractAction, identifier="firewall_acl_remove_rule"): +class FirewallACLRemoveRuleAction(ACLRemoveRuleAbstractAction, identifier="firewall_acl_remove_rule"): """Action which removes a rule from a firewall port's ACL.""" config: "FirewallACLRemoveRuleAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(ACLRemoveRuleAbstractAction.ConfigSchema): """Configuration schema for FirewallACLRemoveRuleAction.""" target_firewall_nodename: str firewall_port_name: str firewall_port_direction: str - position: int @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index f515a8ec..91e34eae 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -34,8 +34,6 @@ class NodeApplicationAbstractAction(AbstractAction, identifier="node_application @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.node_name is None or config.application_name is None: - return ["do_nothing"] return [ "network", "node", @@ -103,8 +101,6 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="no @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.node_name is None: - return ["do_nothing"] return [ "network", "node", @@ -129,8 +125,6 @@ class NodeApplicationRemoveAction(NodeApplicationAbstractAction, identifier="nod @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.node_name is None: - return ["do_nothing"] return [ "network", "node", diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 7c72e57d..319cd212 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -1,8 +1,8 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, List, Optional, Union +from typing import List, Optional, Union -from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo +from pydantic import ConfigDict, Field, field_validator, ValidationInfo from primaite.game.agent.actions.manager import AbstractAction, ActionManager from primaite.interface.request import RequestFormat @@ -27,7 +27,6 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_rans class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for ConfigureRansomwareScriptAction.""" - model_config = ConfigDict(extra="forbid") node_name: str server_ip_address: Optional[str] server_password: Optional[str] @@ -109,17 +108,7 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): @classmethod def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - if config.node_name is None: - return ["do_nothing"] - configuration = ConfigureC2BeaconAction.ConfigSchema( - c2_server_ip_address=config.c2_server_ip_address, - keep_alive_frequency=config.keep_alive_frequency, - masquerade_port=config.masquerade_port, - masquerade_protocol=config.masquerade_protocol, - ) - - ConfigureC2BeaconAction.ConfigSchema.model_validate(configuration) # check that options adhere to schema - + configuration = [] return ["network", "node", config.node_name, "application", "C2Beacon", "configure", configuration] diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index a413f6dc..b89704f4 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -13,8 +13,7 @@ agents: from __future__ import annotations -import itertools -from typing import Dict, List, Literal, Optional, Tuple +from typing import Dict, List, Optional, Tuple from gymnasium import spaces @@ -45,7 +44,9 @@ class ActionManager: def __init__( self, actions: List[Dict], # stores list of actions available to agent - act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions + act_map: Optional[ + Dict[int, Dict] + ] = None, # allows restricting set of possible actions - TODO: Refactor to be a list? *args, **kwargs, ) -> None: @@ -79,43 +80,6 @@ class ActionManager: # make sure all numbers between 0 and N are represented as dict keys in action map assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) - def _enumerate_actions( - self, - ) -> Dict[int, Tuple[str, Dict]]: - """Generate a list of all the possible actions that could be taken. - - This enumerates all actions all combinations of parameters you could choose for those actions. The output - of this function is intended to populate the self.action_map parameter in the situation where the user provides - a list of action types, but doesn't specify any subset of actions that should be made available to the agent. - - The enumeration relies on the Actions' `shape` attribute. - - :return: An action map maps consecutive integers to a combination of Action type and parameter choices. - An example output could be: - {0: ("do_nothing", {'dummy': 0}), - 1: ("node_os_scan", {'node_name': computer}), - 2: ("node_os_scan", {'node_name': server}), - 3: ("node_folder_scan", {'node_name:computer, folder_name:downloads}), - ... #etc... - } - :rtype: Dict[int, Tuple[AbstractAction, Dict]] - """ - all_action_possibilities = [] - for act_name, action in self.actions.items(): - param_names = list(action.shape.keys()) - num_possibilities = list(action.shape.values()) - possibilities = [range(n) for n in num_possibilities] - - param_combinations = list(itertools.product(*possibilities)) - all_action_possibilities.extend( - [ - (act_name, {param_names[i]: param_combinations[j][i] for i in range(len(param_names))}) - for j in range(len(param_combinations)) - ] - ) - - return {i: p for i, p in enumerate(all_action_possibilities)} - def get_action(self, action: int) -> Tuple[str, Dict]: """Produce action in CAOS format.""" """the agent chooses an action (as an integer), this is converted into an action in CAOS format""" @@ -125,8 +89,9 @@ class ActionManager: def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" - act_obj = self.actions[action_identifier].from_config(config=action_options) - return act_obj.form_request(config=act_obj.config) + act_class = AbstractAction._registry[action_identifier] + config = act_class.ConfigSchema(**action_options) + return act_class.form_request(config=config) @property def space(self) -> spaces.Space: @@ -134,7 +99,7 @@ class ActionManager: return spaces.Discrete(len(self.action_map)) @classmethod - def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": + def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": # noqa: F821 """ Construct an ActionManager from a config definition. From a8fbb002e4299d6a34f3801a56b55b61f7fc674c Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 9 Dec 2024 09:54:35 +0000 Subject: [PATCH 073/224] #2912 - Updates following review, ACL rules now have validation for ConfigSchema fields --- src/primaite/game/agent/actions/acl.py | 42 ++++++++++++++++++----- src/primaite/game/agent/actions/config.py | 3 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index fb18d025..f129a82f 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -3,8 +3,12 @@ from __future__ import annotations from typing import List +from pydantic import field_validator + from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +from primaite.utils.validation.ip_protocol import is_valid_protocol, protocol_validator +from primaite.utils.validation.port import is_valid_port __all__ = ( "RouterACLAddRuleAction", @@ -33,6 +37,35 @@ class ACLAddRuleAbstractAction(AbstractAction, identifier="acl_add_rule_abstract src_wildcard: int dst_wildcard: int + @field_validator( + src_port, + dst_port, + mode="before", + ) + @classmethod + def valid_port(cls, v: str) -> int: + """Check that inputs are valid.""" + return is_valid_port(v) + + @field_validator( + src_ip, + dst_ip, + mode="before", + ) + @classmethod + def valid_ip(cls, v: str) -> str: + """Check that a valid IP has been provided for src and dst.""" + return is_valid_protocol(v) + + @field_validator( + protocol_name, + mode="before", + ) + @classmethod + def is_valid_protocol(cls, v: str) -> bool: + """Check that we are using a valid protocol.""" + return protocol_validator(v) + class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_abstract_action"): """Base abstract class for ACL remove rule actions.""" @@ -70,7 +103,7 @@ class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_ad config.protocol_name, config.src_ip, config.src_wildcard, - config.source_port, + config.src_port, config.dst_ip, config.dst_wildcard, config.dst_port, @@ -109,13 +142,6 @@ class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_ac @classmethod def form_request(cls, config: ConfigSchema) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.protocol_name is None: - return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - if config.src_ip == 0: - return ["do_nothing"] # invalid formulation - if config.src_port == 0: - return ["do_nothing"] # invalid configuration. - return [ "network", "node", diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 319cd212..050e9b94 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -108,8 +108,7 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): @classmethod def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - configuration = [] - return ["network", "node", config.node_name, "application", "C2Beacon", "configure", configuration] + return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config] class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_command"): From 386717fa4166d935e09524681905b285d56e3223 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 9 Dec 2024 09:59:47 +0000 Subject: [PATCH 074/224] #2912 - removal of the rom_config method as this shouldn't be needed for the actions refactor --- src/primaite/game/agent/actions/abstract.py | 5 ----- src/primaite/game/agent/actions/acl.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index ef22ec54..8c332d5e 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -32,8 +32,3 @@ class AbstractAction(BaseModel): def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" pass - - @classmethod - def from_config(cls, config: Dict) -> "AbstractAction": - """Create an action component from a config dictionary.""" - return cls(config=cls.ConfigSchema(**config)) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index f129a82f..5cd7a355 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -79,6 +79,24 @@ class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_ab protocol_name: str position: int + @field_validator( + src_ip, + mode="before", + ) + @classmethod + def valid_ip(cls, v: str) -> str: + """Check that a valid IP has been provided for src and dst.""" + return is_valid_protocol(v) + + @field_validator( + protocol_name, + mode="before", + ) + @classmethod + def is_valid_protocol(cls, v: str) -> bool: + """Check that we are using a valid protocol.""" + return protocol_validator(v) + class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" From 068ad2f1fa17cac80856bd71fa4d00f5b673b89c Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 9 Dec 2024 13:56:40 +0000 Subject: [PATCH 075/224] #2912 - Updates to get tests to pass. Some ACL rules still misbehaving --- src/primaite/game/agent/actions/acl.py | 35 +++++------ .../configs/firewall_actions_network.yaml | 17 +++--- .../game_layer/test_actions.py | 58 +++++++++++++++---- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 5cd7a355..37dde757 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,14 +1,16 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations +from ipaddress import IPv4Address from typing import List from pydantic import field_validator from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat -from primaite.utils.validation.ip_protocol import is_valid_protocol, protocol_validator -from primaite.utils.validation.port import is_valid_port +from primaite.utils.validation.ip_protocol import protocol_validator +from primaite.utils.validation.ipv4_address import ipv4_validator +from primaite.utils.validation.port import port_validator __all__ = ( "RouterACLAddRuleAction", @@ -26,39 +28,38 @@ class ACLAddRuleAbstractAction(AbstractAction, identifier="acl_add_rule_abstract class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema base for ACL add rule abstract actions.""" - src_ip: str + src_ip: IPv4Address protocol_name: str permission: str position: int - src_ip: str - dst_ip: str - src_port: str - dst_port: str + dst_ip: IPv4Address + src_port: int + dst_port: int src_wildcard: int dst_wildcard: int @field_validator( - src_port, - dst_port, + "src_port", + "dst_port", mode="before", ) @classmethod def valid_port(cls, v: str) -> int: """Check that inputs are valid.""" - return is_valid_port(v) + return port_validator(v) @field_validator( - src_ip, - dst_ip, + "src_ip", + "dst_ip", mode="before", ) @classmethod def valid_ip(cls, v: str) -> str: """Check that a valid IP has been provided for src and dst.""" - return is_valid_protocol(v) + return ipv4_validator(v) @field_validator( - protocol_name, + "protocol_name", mode="before", ) @classmethod @@ -80,16 +81,16 @@ class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_ab position: int @field_validator( - src_ip, + "src_ip", mode="before", ) @classmethod def valid_ip(cls, v: str) -> str: """Check that a valid IP has been provided for src and dst.""" - return is_valid_protocol(v) + return ipv4_validator(v) @field_validator( - protocol_name, + "protocol_name", mode="before", ) @classmethod diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 4c3b5000..29af3b55 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -108,18 +108,19 @@ agents: 1: action: firewall_acl_add_rule options: + type: firewall_acl_add_rule target_firewall_nodename: firewall firewall_port_name: internal firewall_port_direction: inbound position: 1 permission: PERMIT src_ip: 192.168.0.10 - dest_ip: ALL - src_port: ALL - dst_port: ALL - protocol_name: ALL - source_wildcard_id: 0 - dest_wildcard_id: 0 + dst_ip: 0.0.0.0 + src_port: 80 + dst_port: HTTP + protocol_name: TCP + src_wildcard: 0 + dst_wildcard: 0 2: action: firewall_acl_remove_rule options: @@ -136,7 +137,7 @@ agents: position: 1 permission: DENY src_ip: 192.168.0.10 # client 1 - dest_ip: ALL # ALL + dest_ip: ALL src_port: ARP dst_port: DNS protocol_name: ICMP @@ -240,11 +241,13 @@ agents: 13: action: network_port_disable options: + type: network_port_disable target_nodename: firewall port_id: 3 14: action: network_port_enable options: + type: network_port_enable target_nodename: firewall port_id: 3 options: diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index a21ad34f..9fdf029b 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -88,7 +88,7 @@ def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA svc.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a patch action - action = ("node_service_fix", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_fix", {"type": "node_service_fix", "node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() @@ -123,16 +123,17 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox action = ( "router_acl_add_rule", { + "type": "router_acl_add_rule", "target_router": "router", "position": 4, "permission": "DENY", "src_ip": "10.0.1.2", "src_wildcard": 0, - "source_port": "ALL", + "src_port": "HTTP", "dst_ip": "10.0.2.3", "dst_wildcard": 0, - "dst_port": "ALL", - "protocol_name": "ALL", + "dst_port": "HTTP", + "protocol_name": "udp", }, ) agent.store_action(action) @@ -150,6 +151,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox action = ( "router_acl_add_rule", { + "type": "router_acl_add_rule", "target_router": "router", "position": 5, # 5th rule "permission": "DENY", # DENY @@ -190,6 +192,7 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P action = ( "router_acl_remove_rule", { + "type": "router_acl_remove_rule", "target_router": "router", "position": 3, # 4th rule }, @@ -223,6 +226,7 @@ def test_host_nic_disable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA action = ( "host_nic_disable", { + "type": "host_nic_disable", "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) }, @@ -254,6 +258,7 @@ def test_host_nic_enable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAg action = ( "host_nic_enable", { + "type": "host_nic_enable", "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) }, @@ -281,6 +286,7 @@ def test_node_file_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAge action = ( "node_file_scan", { + "type": "node_file_scan", "node_name": "client_1", # client_1, "folder_name": "downloads", # downloads, "file_name": "cat.png", # cat.png @@ -318,6 +324,7 @@ def test_node_file_delete_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA action = ( "node_file_delete", { + "type": "node_file_delete", "node_name": "client_1", # client_1 "folder_name": "downloads", # downloads "file_name": "cat.png", # cat.png @@ -340,7 +347,13 @@ def test_node_file_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_create", - {"node_name": "client_1", "folder_name": "test", "file_name": "file.txt", "force": "False"}, + { + "type": "node_file_create", + "node_name": "client_1", + "folder_name": "test", + "file_name": "file.txt", + "force": "False", + }, ) agent.store_action(action) game.step() @@ -357,6 +370,7 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_create", { + "type": "node_file_create", "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", @@ -370,6 +384,7 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_access", { + "type": "node_file_access", "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", @@ -390,6 +405,7 @@ def test_node_folder_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_folder_create", { + "type": "node_folder_create", "node_name": "client_1", "folder_name": "test", }, @@ -418,6 +434,7 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG action = ( "network_port_disable", { + "type": "network_port_disable", "target_nodename": "router", # router "port_id": 1, # port 1 }, @@ -450,6 +467,7 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa action = ( "network_port_enable", { + "type": "network_port_enable", "target_nodename": "router", # router "port_id": 1, # port 1 }, @@ -478,7 +496,10 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P assert browser.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("node_application_scan", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ( + "node_application_scan", + {"type": "node_application_scan", "node_name": "client_1", "application_name": "WebBrowser"}, + ) agent.store_action(action) game.step() assert browser.health_state_actual == SoftwareHealthState.GOOD @@ -489,7 +510,10 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P assert browser.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("node_application_scan", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ( + "node_application_scan", + {"type": "node_application_scan", "node_name": "client_1", "application_name": "WebBrowser"}, + ) agent.store_action(action) game.step() assert browser.health_state_actual == SoftwareHealthState.COMPROMISED @@ -510,7 +534,10 @@ def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, Pr browser.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a fix action - action = ("node_application_fix", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ( + "node_application_fix", + {"type": "node_application_fix", "node_name": "client_1", "application_name": "WebBrowser"}, + ) agent.store_action(action) game.step() @@ -536,7 +563,10 @@ def test_node_application_close_integration(game_and_agent: Tuple[PrimaiteGame, assert browser.operating_state == ApplicationOperatingState.RUNNING # 2: Apply a close action - action = ("node_application_close", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ( + "node_application_close", + {"type": "node_application_close", "node_name": "client_1", "application_name": "WebBrowser"}, + ) agent.store_action(action) game.step() @@ -555,13 +585,19 @@ def test_node_application_install_and_uninstall_integration(game_and_agent: Tupl assert client_1.software_manager.software.get("DoSBot") is None - action = ("node_application_install", {"node_name": "client_1", "application_name": "DoSBot"}) + action = ( + "node_application_install", + {"type": "node_application_install", "node_name": "client_1", "application_name": "DoSBot"}, + ) agent.store_action(action) game.step() assert client_1.software_manager.software.get("DoSBot") is not None - action = ("node_application_remove", {"node_name": "client_1", "application_name": "DoSBot"}) + action = ( + "node_application_remove", + {"type": "node_application_remove", "node_name": "client_1", "application_name": "DoSBot"}, + ) agent.store_action(action) game.step() From ed128fc53539dc883575d5e1738ccf91d57a6337 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 9 Dec 2024 16:38:42 +0000 Subject: [PATCH 076/224] #2888: Add ConfigSchema to Application class. --- .../system/applications/application.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index a7871315..43ffa37a 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -1,10 +1,12 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations -from abc import abstractmethod +from abc import ABC, abstractmethod from enum import Enum from typing import Any, ClassVar, Dict, Optional, Set, Type +from pydantic import BaseModel + from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType from primaite.simulator.system.software import IOSoftware, SoftwareHealthState @@ -27,6 +29,7 @@ class Application(IOSoftware): Applications are user-facing programs that may perform input/output operations. """ + config: "Application.ConfigSchema" operating_state: ApplicationOperatingState = ApplicationOperatingState.CLOSED "The current operating state of the Application." @@ -44,6 +47,11 @@ class Application(IOSoftware): _registry: ClassVar[Dict[str, Type["Application"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" + class ConfigSchema(BaseModel, ABC): + """Config Schema for Application class.""" + + type: str + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register an application type. @@ -59,6 +67,21 @@ class Application(IOSoftware): raise ValueError(f"Tried to define new application {identifier}, but this name is already reserved.") cls._registry[identifier] = cls + @classmethod + def from_config(cls, config: Dict) -> "Application": + """Create an application from a config dictionary. + + :param config: dict of options for application components constructor + :type config: dict + :return: The application component. + :rtype: Application + """ + if config["type"] not in cls._registry: + raise ValueError(f"Invalid Application type {config['type']}") + application_class = cls._registry[config["type"]] + application_object = application_class(config=application_class.ConfigSchema(**config)) + return application_object + def __init__(self, **kwargs): super().__init__(**kwargs) From 7dd25f18f666a319528d34df5916893f3d7c37f3 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 10 Dec 2024 12:27:50 +0000 Subject: [PATCH 077/224] #2888: Update with ConfigSchema --- .../simulator/system/applications/application.py | 1 + .../simulator/system/applications/database_client.py | 9 ++++++++- src/primaite/simulator/system/applications/nmap.py | 7 +++++++ .../applications/red_applications/c2/abstract_c2.py | 11 +++++++++-- .../applications/red_applications/c2/c2_beacon.py | 10 +++++++++- .../applications/red_applications/c2/c2_server.py | 8 ++++++++ .../system/applications/red_applications/dos_bot.py | 8 ++++++++ .../red_applications/ransomware_script.py | 7 +++++++ .../simulator/system/applications/web_browser.py | 7 +++++++ 9 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 43ffa37a..402c64f2 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -29,6 +29,7 @@ class Application(IOSoftware): Applications are user-facing programs that may perform input/output operations. """ + config: "Application.ConfigSchema" operating_state: ApplicationOperatingState = ApplicationOperatingState.CLOSED diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index cd4b2a03..cc593a30 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -68,10 +68,12 @@ class DatabaseClient(Application, identifier="DatabaseClient"): Extends the Application class to provide functionality for connecting, querying, and disconnecting from a Database Service. It mainly operates over TCP protocol. - :ivar server_ip_address: The IPv4 address of the Database Service server, defaults to None. """ + config: "DatabaseClient.ConfigSchema" + server_ip_address: Optional[IPv4Address] = None + """The IPv4 address of the Database Service server, defaults to None.""" server_password: Optional[str] = None _query_success_tracker: Dict[str, bool] = {} """Keep track of connections that were established or verified during this step. Used for rewards.""" @@ -88,6 +90,11 @@ class DatabaseClient(Application, identifier="DatabaseClient"): native_connection: Optional[DatabaseClientConnection] = None """Native Client Connection for using the client directly (similar to psql in a terminal).""" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for DatabaseClient.""" + + type: str = "DATABASE_CLIENT" + def __init__(self, **kwargs): kwargs["name"] = "DatabaseClient" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index e2b9117d..3f9724ca 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -52,6 +52,8 @@ class NMAP(Application, identifier="NMAP"): as ping scans to discover active hosts and port scans to detect open ports on those hosts. """ + config: "NMAP.ConfigSchema" + _active_port_scans: Dict[str, PortScanPayload] = {} _port_scan_responses: Dict[str, PortScanPayload] = {} @@ -62,6 +64,11 @@ class NMAP(Application, identifier="NMAP"): (False, False): "Port", } + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for NMAP.""" + + type: str = "NMAP" + def __init__(self, **kwargs): kwargs["name"] = "NMAP" kwargs["port"] = PORT_LOOKUP["NONE"] diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index f77bc33a..9961e790 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -45,7 +45,7 @@ class C2Payload(Enum): """C2 Input Command payload. Used by the C2 Server to send a command to the c2 beacon.""" OUTPUT = "output_command" - """C2 Output Command. Used by the C2 Beacon to send the results of a Input command to the c2 server.""" + """C2 Output Command. Used by the C2 Beacon to send the results of an Input command to the c2 server.""" class AbstractC2(Application, identifier="AbstractC2"): @@ -63,6 +63,8 @@ class AbstractC2(Application, identifier="AbstractC2"): Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. """ + config: "AbstractC2" + c2_connection_active: bool = False """Indicates if the c2 server and c2 beacon are currently connected.""" @@ -75,6 +77,11 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_inactivity: int = 0 """Indicates how many timesteps since the last time the c2 application received a keep alive.""" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for AbstractC2.""" + + type: str = "ABSTRACTC2" + class _C2Opts(BaseModel): """A Pydantic Schema for the different C2 configuration options.""" @@ -118,7 +125,7 @@ class AbstractC2(Application, identifier="AbstractC2"): :type c2_command: C2Command. :param command_options: The relevant C2 Beacon parameters.F :type command_options: Dict - :return: Returns the construct C2Packet + :return: Returns the constructed C2Packet :rtype: C2Packet """ constructed_packet = C2Packet( diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index c0c3d872..98cb85ba 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -8,6 +8,7 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts, RansomwareOpts, TerminalOpts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript @@ -32,15 +33,22 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): 2. Leveraging the terminal application to execute requests (dependent on the command given) 3. Sending the RequestResponse back to the C2 Server (Command output) - Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. + Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ + config: "C2Beacon.ConfigSchema" + keep_alive_attempted: bool = False """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" terminal_session: TerminalClientConnection = None "The currently in use terminal session." + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for C2Beacon.""" + + type: str = "C2BEACON" + @property def _host_terminal(self) -> Optional[Terminal]: """Return the Terminal that is installed on the same machine as the C2 Beacon.""" diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index f948d696..b5ea9e08 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -7,6 +7,7 @@ from pydantic import validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.red_applications.c2 import ( CommandOpts, ExfilOpts, @@ -34,9 +35,16 @@ class C2Server(AbstractC2, identifier="C2Server"): Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. """ + config: "C2Server.ConfigSchema" + current_command_output: RequestResponse = None """The Request Response by the last command send. This attribute is updated by the method _handle_command_output.""" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for C2Server.""" + + type: str = "C2SERVER" + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index fb2c8847..a02b04c5 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -7,6 +7,7 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.utils.validation.port import Port, PORT_LOOKUP @@ -32,6 +33,8 @@ class DoSAttackStage(IntEnum): class DoSBot(DatabaseClient, identifier="DoSBot"): """A bot that simulates a Denial of Service attack.""" + config: "DoSBot.ConfigSchema" + target_ip_address: Optional[IPv4Address] = None """IP address of the target service.""" @@ -53,6 +56,11 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): dos_intensity: float = 1.0 """How much of the max sessions will be used by the DoS when attacking.""" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for DoSBot.""" + + type: str = "DOSBOT" + def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "DoSBot" diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 93b4c50d..236cde79 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -18,6 +18,8 @@ class RansomwareScript(Application, identifier="RansomwareScript"): :ivar payload: The attack stage query payload. (Default ENCRYPT) """ + config: "RansomwareScript.ConfigSchema" + server_ip_address: Optional[IPv4Address] = None """IP address of node which hosts the database.""" server_password: Optional[str] = None @@ -25,6 +27,11 @@ class RansomwareScript(Application, identifier="RansomwareScript"): payload: Optional[str] = "ENCRYPT" "Payload String for the payload stage" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for RansomwareScript.""" + + type: str = "RANSOMWARE_SCRIPT" + def __init__(self, **kwargs): kwargs["name"] = "RansomwareScript" kwargs["port"] = PORT_LOOKUP["NONE"] diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index c57a9bd3..35f35fea 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -30,6 +30,8 @@ class WebBrowser(Application, identifier="WebBrowser"): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ + config: "WebBrowser.ConfigSchema" + target_url: Optional[str] = None domain_name_ip_address: Optional[IPv4Address] = None @@ -41,6 +43,11 @@ class WebBrowser(Application, identifier="WebBrowser"): history: List["BrowserHistoryItem"] = [] """Keep a log of visited websites and information about the visit, such as response code.""" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for WebBrowser.""" + + type: str = "WEB_BROWSER" + def __init__(self, **kwargs): kwargs["name"] = "WebBrowser" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] From 66f775da4d2ec04576837b0874b5d42786ba17a2 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 10 Dec 2024 16:58:28 +0000 Subject: [PATCH 078/224] #2888: Add ConfigSchema to Services. --- .../simulator/system/services/arp/arp.py | 7 +++++ .../services/database/database_service.py | 7 +++++ .../system/services/dns/dns_server.py | 7 +++++ .../system/services/ftp/ftp_client.py | 10 ++++++- .../system/services/ftp/ftp_server.py | 10 ++++++- .../simulator/system/services/icmp/icmp.py | 7 +++++ .../system/services/ntp/ntp_client.py | 7 +++++ .../system/services/ntp/ntp_server.py | 7 +++++ .../simulator/system/services/service.py | 30 +++++++++++++++++-- .../system/services/terminal/terminal.py | 7 +++++ .../system/services/web_server/web_server.py | 7 +++++ 11 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 816eb99e..2e13aa41 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -22,8 +22,15 @@ class ARP(Service): sends ARP requests and replies, and processes incoming ARP packets. """ + config: "ARP.ConfigSchema" + arp: Dict[IPV4Address, ARPEntry] = {} + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for ARP.""" + + type: str = "ARP" + def __init__(self, **kwargs): kwargs["name"] = "ARP" kwargs["port"] = PORT_LOOKUP["ARP"] diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index b7cd8886..a5aa4c44 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -24,6 +24,8 @@ class DatabaseService(Service): This class inherits from the `Service` class and provides methods to simulate a SQL database. """ + config: "DatabaseService.ConfigSchema" + password: Optional[str] = None """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" @@ -36,6 +38,11 @@ class DatabaseService(Service): latest_backup_file_name: str = None """File name of latest backup.""" + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for DatabaseService.""" + + type: str = "DATABASESERVICE" + def __init__(self, **kwargs): kwargs["name"] = "DatabaseService" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 5b380320..8a202aea 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -16,9 +16,16 @@ _LOGGER = getLogger(__name__) class DNSServer(Service): """Represents a DNS Server as a Service.""" + config: "DNSServer.ConfigSchema" + dns_table: Dict[str, IPv4Address] = {} "A dict of mappings between domain names and IPv4 addresses." + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for DNSServer.""" + + type: str = "DNSSERVER" + def __init__(self, **kwargs): kwargs["name"] = "DNSServer" kwargs["port"] = PORT_LOOKUP["DNS"] diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 00b70332..604c7f30 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -9,6 +9,7 @@ from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.simulator.system.services.service import Service from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import Port, PORT_LOOKUP @@ -19,10 +20,17 @@ class FTPClient(FTPServiceABC): """ A class for simulating an FTP client service. - This class inherits from the `Service` class and provides methods to emulate FTP + This class inherits from the `FTPServiceABC` class and provides methods to emulate FTP RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ + config: "FTPClient.ConfigSchema" + + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for FTPClient.""" + + type: str = "FTPCLIENT" + def __init__(self, **kwargs): kwargs["name"] = "FTPClient" kwargs["port"] = PORT_LOOKUP["FTP"] diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 671200f5..596f9e77 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -4,6 +4,7 @@ from typing import Any, Optional from primaite import getLogger from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.simulator.system.services.service import Service from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP @@ -14,13 +15,20 @@ class FTPServer(FTPServiceABC): """ A class for simulating an FTP server service. - This class inherits from the `Service` class and provides methods to emulate FTP + This class inherits from the `FTPServiceABC` class and provides methods to emulate FTP RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ + config: "FTPServer.ConfigSchema" + server_password: Optional[str] = None """Password needed to connect to FTP server. Default is None.""" + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for FTPServer.""" + + type: str = "FTPServer" + def __init__(self, **kwargs): kwargs["name"] = "FTPServer" kwargs["port"] = PORT_LOOKUP["FTP"] diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 84ad995d..8349fff4 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -22,8 +22,15 @@ class ICMP(Service): network diagnostics, notably the ping command. """ + config: "ICMP.ConfigSchema" + request_replies: Dict = {} + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for ICMP.""" + + type: str = "ICMP" + def __init__(self, **kwargs): kwargs["name"] = "ICMP" kwargs["port"] = PORT_LOOKUP["NONE"] diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index ed89971f..a08ae795 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -15,10 +15,17 @@ _LOGGER = getLogger(__name__) class NTPClient(Service): """Represents a NTP client as a service.""" + config: "NTPClient.ConfigSchema" + ntp_server: Optional[IPv4Address] = None "The NTP server the client sends requests to." time: Optional[datetime] = None + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for NTPClient.""" + + type: str = "NTPCLIENT" + def __init__(self, **kwargs): kwargs["name"] = "NTPClient" kwargs["port"] = PORT_LOOKUP["NTP"] diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index b674a296..c253e322 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -14,6 +14,13 @@ _LOGGER = getLogger(__name__) class NTPServer(Service): """Represents a NTP server as a service.""" + config: "NTPServer.ConfigSchema" + + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for NTPServer.""" + + type: str = "NTPSERVER" + def __init__(self, **kwargs): kwargs["name"] = "NTPServer" kwargs["port"] = PORT_LOOKUP["NTP"] diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 4f0b879c..9b30e5e2 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,10 +1,12 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations -from abc import abstractmethod +from abc import ABC, abstractmethod from enum import Enum from typing import Any, ClassVar, Dict, Optional, Type +from pydantic import BaseModel + from primaite import getLogger from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType @@ -37,6 +39,8 @@ class Service(IOSoftware): Services are programs that run in the background and may perform input/output operations. """ + config: "Service.ConfigSchema" + operating_state: ServiceOperatingState = ServiceOperatingState.STOPPED "The current operating state of the Service." @@ -49,6 +53,11 @@ class Service(IOSoftware): _registry: ClassVar[Dict[str, Type["Service"]]] = {} """Registry of service types. Automatically populated when subclasses are defined.""" + class ConfigSchema(BaseModel, ABC): + """Config Schema for Service class.""" + + type: str + def __init__(self, **kwargs): super().__init__(**kwargs) @@ -69,6 +78,21 @@ class Service(IOSoftware): raise ValueError(f"Tried to define new hostnode {identifier}, but this name is already reserved.") cls._registry[identifier] = cls + @classmethod + def from_config(cls, config: Dict) -> "Service": + """Create a service from a config dictionary. + + :param config: dict of options for service components constructor + :type config: dict + :return: The service component. + :rtype: Service + """ + if config["type"] not in cls._registry: + raise ValueError(f"Invalid service type {config['type']}") + service_class = cls._registry[config["type"]] + service_object = service_class(config=service_class.ConfigSchema(**config)) + return service_object + def _can_perform_action(self) -> bool: """ Checks if the service can perform actions. @@ -232,14 +256,14 @@ class Service(IOSoftware): def disable(self) -> bool: """Disable the service.""" - self.sys_log.info(f"Disabling Application {self.name}") + self.sys_log.info(f"Disabling Service {self.name}") self.operating_state = ServiceOperatingState.DISABLED return True def enable(self) -> bool: """Enable the disabled service.""" if self.operating_state == ServiceOperatingState.DISABLED: - self.sys_log.info(f"Enabling Application {self.name}") + self.sys_log.info(f"Enabling Service {self.name}") self.operating_state = ServiceOperatingState.STOPPED return True return False diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index ae3557f7..1e820689 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -132,9 +132,16 @@ class RemoteTerminalConnection(TerminalClientConnection): class Terminal(Service): """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" + config: "Terminal.ConfigSchema" + _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} """Dictionary of connect requests made to remote nodes.""" + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for Terminal.""" + + type: str = "TERMINAL" + def __init__(self, **kwargs): kwargs["name"] = "Terminal" kwargs["port"] = PORT_LOOKUP["SSH"] diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 75d9c472..f8ca1a69 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -22,8 +22,15 @@ _LOGGER = getLogger(__name__) class WebServer(Service): """Class used to represent a Web Server Service in simulation.""" + config: "WebServer.ConfigSchema" + response_codes_this_timestep: List[HttpStatusCode] = [] + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for WebServer.""" + + type: str = "WEBSERVER" + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. From 4050bd9e85c431c2b2d2ee09a86b1abab2c07776 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 11 Dec 2024 10:12:50 +0000 Subject: [PATCH 079/224] #2888: Add identifier keyword to services. --- src/primaite/simulator/system/applications/web_browser.py | 2 +- src/primaite/simulator/system/services/arp/arp.py | 2 +- .../simulator/system/services/database/database_service.py | 2 +- src/primaite/simulator/system/services/dns/dns_server.py | 2 +- src/primaite/simulator/system/services/ftp/ftp_client.py | 2 +- src/primaite/simulator/system/services/ftp/ftp_server.py | 2 +- src/primaite/simulator/system/services/icmp/icmp.py | 2 +- src/primaite/simulator/system/services/ntp/ntp_client.py | 2 +- src/primaite/simulator/system/services/ntp/ntp_server.py | 2 +- src/primaite/simulator/system/services/terminal/terminal.py | 2 +- src/primaite/simulator/system/services/web_server/web_server.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 35f35fea..271aec71 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -46,7 +46,7 @@ class WebBrowser(Application, identifier="WebBrowser"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for WebBrowser.""" - type: str = "WEB_BROWSER" + type: str = "WEBBROWSER" def __init__(self, **kwargs): kwargs["name"] = "WebBrowser" diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 2e13aa41..91b58bc4 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -14,7 +14,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP -class ARP(Service): +class ARP(Service, identifier="ARP"): """ The ARP (Address Resolution Protocol) Service. diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index a5aa4c44..ccf566bf 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -17,7 +17,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class DatabaseService(Service): +class DatabaseService(Service, identifier="DatabaseService"): """ A class for simulating a generic SQL Server service. diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 8a202aea..05a6b373 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -13,7 +13,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class DNSServer(Service): +class DNSServer(Service, identifier="DNSServer"): """Represents a DNS Server as a Service.""" config: "DNSServer.ConfigSchema" diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 604c7f30..e8e79d85 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -16,7 +16,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class FTPClient(FTPServiceABC): +class FTPClient(FTPServiceABC, identifier="FTPClient"): """ A class for simulating an FTP client service. diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 596f9e77..cbac2030 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -11,7 +11,7 @@ from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class FTPServer(FTPServiceABC): +class FTPServer(FTPServiceABC, identifier="FTPServer"): """ A class for simulating an FTP server service. diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 8349fff4..f5225f71 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -14,7 +14,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ICMP(Service): +class ICMP(Service, identifier="ICMP"): """ The Internet Control Message Protocol (ICMP) service. diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index a08ae795..8c36b55f 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -12,7 +12,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class NTPClient(Service): +class NTPClient(Service, identifier="NTPClient"): """Represents a NTP client as a service.""" config: "NTPClient.ConfigSchema" diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index c253e322..538a1ec3 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -11,7 +11,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class NTPServer(Service): +class NTPServer(Service, identifier="NTPServer"): """Represents a NTP server as a service.""" config: "NTPServer.ConfigSchema" diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 1e820689..7ecd425d 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -129,7 +129,7 @@ class RemoteTerminalConnection(TerminalClientConnection): return self.parent_terminal.send(payload=payload, session_id=self.ssh_session_id) -class Terminal(Service): +class Terminal(Service, identifier="Terminal"): """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" config: "Terminal.ConfigSchema" diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index f8ca1a69..0c47961d 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class WebServer(Service): +class WebServer(Service, identifier="WebServer"): """Class used to represent a Web Server Service in simulation.""" config: "WebServer.ConfigSchema" From e40fd053f73ccd78c66ec55ceb43a5c309e98075 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 11 Dec 2024 10:32:15 +0000 Subject: [PATCH 080/224] #2912 - Removing print statements left in from debugging --- .../simulator/system/core/software_manager.py | 4 ++++ tests/assets/configs/data_manipulation.yaml | 14 +++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 04a3e3fb..2f19a8b0 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -207,6 +207,10 @@ class SoftwareManager: :param session_id: The Session ID from which the payload originates. Optional. :return: True if the payload was successfully sent, False otherwise. """ + print(payload) + print(dest_ip_address) + print(src_port) + print(session_id) return self.session_manager.receive_payload_from_software_manager( payload=payload, dst_ip_address=dest_ip_address, diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 97442903..96751cad 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -33,7 +33,7 @@ agents: observation_space: null action_space: action_list: - - type: DONOTHING + - type: do_nothing - type: NODE_APPLICATION_EXECUTE options: nodes: @@ -47,7 +47,7 @@ agents: max_applications_per_node: 2 action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: action: NODE_APPLICATION_EXECUTE @@ -82,7 +82,7 @@ agents: observation_space: null action_space: action_list: - - type: DONOTHING + - type: do_nothing - type: NODE_APPLICATION_EXECUTE options: nodes: @@ -96,7 +96,7 @@ agents: max_applications_per_node: 2 action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: action: NODE_APPLICATION_EXECUTE @@ -132,7 +132,7 @@ agents: action_space: action_list: - - type: DONOTHING + - type: do_nothing - type: NODE_APPLICATION_EXECUTE options: nodes: @@ -236,7 +236,7 @@ agents: action_space: action_list: - - type: DONOTHING + - type: do_nothing - type: NODE_SERVICE_SCAN - type: NODE_SERVICE_STOP - type: NODE_SERVICE_START @@ -266,7 +266,7 @@ agents: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: From feee0284855fe972df2fc9795238f652de512509 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 11 Dec 2024 11:58:42 +0000 Subject: [PATCH 081/224] #2869 - Updates to Probabilistic Agent to follow the defined extensibility schema. --- src/primaite/game/agent/agent_log.py | 1 - .../agent/scripted_agents/abstract_tap.py | 43 ----------- .../scripted_agents/data_manipulation_bot.py | 1 - .../scripted_agents/probabilistic_agent.py | 43 +++-------- .../game/agent/scripted_agents/tap001.py | 73 ------------------- 5 files changed, 12 insertions(+), 149 deletions(-) delete mode 100644 src/primaite/game/agent/scripted_agents/abstract_tap.py delete mode 100644 src/primaite/game/agent/scripted_agents/tap001.py diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index c292ba4f..7f7b6ffd 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -1,6 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import logging -from abc import ABC from pathlib import Path from typing import Optional diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py deleted file mode 100644 index 19eeac1a..00000000 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ /dev/null @@ -1,43 +0,0 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -import random -from abc import abstractmethod - -from primaite.game.agent.interface import AbstractScriptedAgent - - -class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): - """Base class for TAP agents to inherit from.""" - - config: "AbstractTAPAgent.ConfigSchema" - - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): - """Configuration schema for Abstract TAP agents.""" - - agent_name: str = "Abstract_TAP" - starting_node_name: str - next_execution_timestep: int - - @abstractmethod - def setup_agent(self) -> None: - """Set up agent.""" - pass - - def _set_next_execution_timestep(self, timestep: int) -> None: - """Set the next execution timestep with a configured random variance. - - :param timestep: The timestep to add variance to. - """ - random_timestep_increment = random.randint( - -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance - ) - self.config.next_execution_timestep = timestep + random_timestep_increment - - def _select_start_node(self) -> None: - """Set the starting starting node of the agent to be a random node from this agent's action manager.""" - # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.config.action_manager.node_names) - starting_node_idx = random.randint(0, num_nodes - 1) - self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] - self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 3a2dbdd2..dbb51b74 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,5 +1,4 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -import random from typing import Dict, Tuple from gymnasium.core import ObsType diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index c29719ac..1522096e 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Agents with predefined behaviours.""" -from typing import Dict, Optional, Tuple +from typing import Dict, Tuple import numpy as np import pydantic @@ -8,23 +8,20 @@ from gymnasium.core import ObsType from primaite.game.agent.actions import ActionManager from primaite.game.agent.interface import AbstractScriptedAgent -from primaite.game.agent.observations.observation_manager import ObservationManager -from primaite.game.agent.rewards import RewardFunction class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" - class ConfigSchema(pydantic.BaseModel): - """Config schema for Probabilistic agent settings.""" + config: "ProbabilisticAgent.ConfigSchema" + + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration schema for Probabilistic Agent.""" agent_name: str = "Probabilistic_Agent" - model_config = pydantic.ConfigDict(extra="forbid") - """Strict validation.""" + action_space: ActionManager action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" - # TODO: give the option to still set a random seed, but have it vary each episode in a predictable way - # for example if the user sets seed 123, have it be 123 + episode_num, so that each ep it's the next seed. @pydantic.field_validator("action_probabilities", mode="after") @classmethod @@ -45,32 +42,16 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent" ) return v - def __init__( - self, - agent_name: str, - action_space: Optional[ActionManager], - observation_space: Optional[ObservationManager], - reward_function: Optional[RewardFunction], - settings: Dict = {}, - ) -> None: - # If the action probabilities are not specified, create equal probabilities for all actions - if "action_probabilities" not in settings: - num_actions = len(action_space.action_map) - settings = {"action_probabilities": {i: 1 / num_actions for i in range(num_actions)}} - - # The random number seed for np.random is dependent on whether a random number seed is set - # in the config file. If there is one it is processed by set_random_seed() in environment.py - # and as a consequence the the sequence of rng_seed's used here will be repeatable. - self.settings = ProbabilisticAgent.ConfigSchema(**settings) + def __init__(self) -> None: rng_seed = np.random.randint(0, 65535) self.rng = np.random.default_rng(rng_seed) - - # convert probabilities from - self.probabilities = np.asarray(list(self.settings.action_probabilities.values())) - - super().__init__(agent_name, action_space, observation_space, reward_function) self.logger.debug(f"ProbabilisticAgent RNG seed: {rng_seed}") + @property + def probabilities(self) -> Dict[str, int]: + """Convenience method to view the probabilities of the Agent.""" + return np.asarray(list(self.config.action_probabilities.values())) + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ Sample the action space randomly. diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py deleted file mode 100644 index 3b7abe50..00000000 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ /dev/null @@ -1,73 +0,0 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -import random -from typing import Dict, Tuple - -from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent - - -class TAP001(AbstractTAPAgent, identifier="TAP001"): - """ - TAP001 | Mobile Malware -- Ransomware Variant. - - Scripted Red Agent. Capable of one action; launching the kill-chain (Ransomware Application) - """ - - config: "TAP001.ConfigSchema" - - class ConfigSchema(AbstractTAPAgent.ConfigSchema): - """Configuration Schema for TAP001 Agent.""" - - agent_name: str = "TAP001" - installed: bool = False - - def __init__(self) -> None: - """___init___ bruv. Restecpa.""" - super().__init__() - self.setup_agent() - - @property - def starting_node_name(self) -> str: - """Node that TAP001 starts from.""" - return self.config.starting_node_name - - def get_action(self, timestep: int) -> Tuple[str, Dict]: - """Waits until a specific timestep, then attempts to execute the ransomware application. - - This application acts a wrapper around the kill-chain, similar to green-analyst and - the previous UC2 data manipulation bot. - - :param timestep: The current simulation timestep, used for scheduling actions - :type timestep: int - :return: Action formatted in CAOS format - :rtype: Tuple[str, Dict] - """ - if timestep < self.config.next_execution_timestep: - return "do_nothing", {} - - self._set_next_execution_timestep(timestep + self.config.agent_settings.start_settings.frequency) - - if not self.config.installed: - self.config.installed = True - return "node_application_install", { - "node_name": self.starting_node_name, - "application_name": "RansomwareScript", - } - - return "node_application_execute", { - "node_name": self.starting_node_name, - "application_name": "RansomwareScript", - } - - def setup_agent(self) -> None: - """Set the next execution timestep when the episode resets.""" - self._select_start_node() - self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) - for n, act in self.config.action_manager.action_map.items(): - if not act[0] == "node_application_install": - continue - if act[1]["node_name"] == self.starting_node_name: - self.ip_address = act[1]["ip_address"] - return - raise RuntimeError("TAP001 agent could not find database server ip address in action map") From fe65cef9aa8459aefb6f5bdb249b91d778135929 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 11 Dec 2024 12:01:07 +0000 Subject: [PATCH 082/224] '2869 - Revert deletion of abstract_tap.py as needed for DataManipulationBot --- .../agent/scripted_agents/abstract_tap.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/primaite/game/agent/scripted_agents/abstract_tap.py diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py new file mode 100644 index 00000000..a1d1eebc --- /dev/null +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -0,0 +1,43 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from __future__ import annotations + +import random +from abc import abstractmethod + +from primaite.game.agent.interface import AbstractScriptedAgent + + +class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): + """Base class for TAP agents to inherit from.""" + + config: "AbstractTAPAgent.ConfigSchema" + + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration schema for Abstract TAP agents.""" + + agent_name: str = "Abstract_TAP" + starting_node_name: str + next_execution_timestep: int + + @abstractmethod + def setup_agent(self) -> None: + """Set up agent.""" + pass + + def _set_next_execution_timestep(self, timestep: int) -> None: + """Set the next execution timestep with a configured random variance. + + :param timestep: The timestep to add variance to. + """ + random_timestep_increment = random.randint( + -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance + ) + self.config.next_execution_timestep = timestep + random_timestep_increment + + def _select_start_node(self) -> None: + """Set the starting starting node of the agent to be a random node from this agent's action manager.""" + # we are assuming that every node in the node manager has a data manipulation application at idx 0 + num_nodes = len(self.config.action_manager.node_names) + starting_node_idx = random.randint(0, num_nodes - 1) + self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] + self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") \ No newline at end of file From 2ecc142c289dac9093445531cbd7cbf147e0c1a1 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 11 Dec 2024 16:50:43 +0000 Subject: [PATCH 083/224] #2888: Changes to Applications and Services previously missed. --- .../simulator/network/hardware/base.py | 18 ++++++++++++++++-- tests/conftest.py | 16 +++++++++++++++- .../applications/extended_application.py | 7 +++++++ .../network/test_broadcast.py | 9 ++++++++- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 51e200e7..02270e38 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -824,7 +824,7 @@ class User(SimComponent): return self.model_dump() -class UserManager(Service): +class UserManager(Service, identifier="UserManager"): """ Manages users within the PrimAITE system, handling creation, authentication, and administration. @@ -833,8 +833,15 @@ class UserManager(Service): :param disabled_admins: A dictionary of currently disabled admin users by their usernames """ + config: "UserManager.ConfigSchema" + users: Dict[str, User] = {} + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for UserManager.""" + + type: str = "USERMANAGER" + def __init__(self, **kwargs): """ Initializes a UserManager instanc. @@ -1130,13 +1137,15 @@ class RemoteUserSession(UserSession): return state -class UserSessionManager(Service): +class UserSessionManager(Service, identifier="UserSessionManager"): """ Manages user sessions on a Node, including local and remote sessions. This class handles authentication, session management, and session timeouts for users interacting with the Node. """ + config: "UserSessionManager.ConfigSchema" + local_session: Optional[UserSession] = None """The current local user session, if any.""" @@ -1158,6 +1167,11 @@ class UserSessionManager(Service): current_timestep: int = 0 """The current timestep in the simulation.""" + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for UserSessionManager.""" + + type: str = "USERSESSIONMANAGER" + def __init__(self, **kwargs): """ Initializes a UserSessionManager instance. diff --git a/tests/conftest.py b/tests/conftest.py index 64fe0699..071d7d99 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,9 +37,16 @@ ACTION_SPACE_NODE_ACTION_VALUES = 1 _LOGGER = getLogger(__name__) -class DummyService(Service): +class DummyService(Service, identifier="DummyService"): """Test Service class""" + config: "DummyService.ConfigSchema" + + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for DummyService.""" + + type: str = "DUMMYSERVICE" + def describe_state(self) -> Dict: return super().describe_state() @@ -56,6 +63,13 @@ class DummyService(Service): class DummyApplication(Application, identifier="DummyApplication"): """Test Application class""" + config: "DummyApplication.ConfigSchema" + + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for DummyApplication.""" + + type: str = "DUMMYAPPLICATION" + def __init__(self, **kwargs): kwargs["name"] = "DummyApplication" kwargs["port"] = PORT_LOOKUP["HTTP"] diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 70dc7cba..189d7975 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -31,6 +31,8 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ + config: "ExtendedApplication.ConfigSchema" + target_url: Optional[str] = None domain_name_ip_address: Optional[IPv4Address] = None @@ -42,6 +44,11 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): history: List["BrowserHistoryItem"] = [] """Keep a log of visited websites and information about the visit, such as response code.""" + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for ExtendedApplication.""" + + type: str = "EXTENDEDAPPLICATION" + def __init__(self, **kwargs): kwargs["name"] = "ExtendedApplication" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index f07f02e7..675e0f53 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -14,9 +14,16 @@ from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP -class BroadcastTestService(Service): +class BroadcastTestService(Service, identifier="BroadcastTestService"): """A service for sending broadcast and unicast messages over a network.""" + config: "BroadcastTestService.ConfigSchema" + + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for BroadcastTestService.""" + + type: str = "BROADCASTTESTSERVICE" + def __init__(self, **kwargs): # Set default service properties for broadcasting kwargs["name"] = "BroadcastService" From 86ad872cba2f071a52d2eb178e32f4b3c8d10c14 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 12 Dec 2024 11:32:59 +0000 Subject: [PATCH 084/224] #2869 - Committing minor changes to base AbstractAgent class before changing branches --- src/primaite/game/agent/interface.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 1b9dbcd6..56404e13 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -96,8 +96,8 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): """Base class for scripted and RL agents.""" _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} + config: "AbstractAgent.ConfigSchema" - agent_name = "Abstract_Agent" class ConfigSchema(BaseModel): """ @@ -130,6 +130,11 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): raise ValueError(f"Cannot create a new agent under reserved name {identifier}") cls._registry[identifier] = cls + @property + def logger(self) -> AgentLog: + """Return the AgentLog.""" + return self.config.logger + @property def observation_manager(self) -> ObservationManager: """Returns the agents observation manager.""" From 4a52054ed6dd85d576349448f7f9131d998ca2ad Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 12 Dec 2024 14:58:48 +0000 Subject: [PATCH 085/224] #2888: Initialise ConfigSchema's and fix type names. --- src/primaite/simulator/network/hardware/base.py | 10 +++++----- .../simulator/system/applications/application.py | 2 +- .../simulator/system/applications/database_client.py | 2 +- src/primaite/simulator/system/applications/nmap.py | 2 +- .../applications/red_applications/c2/abstract_c2.py | 6 +++--- .../applications/red_applications/c2/c2_beacon.py | 4 ++-- .../applications/red_applications/c2/c2_server.py | 6 +++--- .../system/applications/red_applications/dos_bot.py | 4 ++-- .../applications/red_applications/ransomware_script.py | 2 +- .../simulator/system/applications/web_browser.py | 4 ++-- src/primaite/simulator/system/services/arp/arp.py | 2 +- .../system/services/database/database_service.py | 4 ++-- .../simulator/system/services/dns/dns_client.py | 6 ++++++ .../simulator/system/services/dns/dns_server.py | 4 ++-- .../simulator/system/services/ftp/ftp_client.py | 4 ++-- .../simulator/system/services/ftp/ftp_server.py | 4 ++-- src/primaite/simulator/system/services/icmp/icmp.py | 2 +- .../simulator/system/services/ntp/ntp_client.py | 4 ++-- .../simulator/system/services/ntp/ntp_server.py | 4 ++-- .../simulator/system/services/terminal/terminal.py | 2 +- .../simulator/system/services/web_server/web_server.py | 4 ++-- tests/conftest.py | 8 ++++---- .../extensions/applications/extended_application.py | 4 ++-- tests/integration_tests/network/test_broadcast.py | 4 ++-- .../system/test_service_listening_on_ports.py | 8 +++++++- 25 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 02270e38..7a58e6be 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -833,18 +833,18 @@ class UserManager(Service, identifier="UserManager"): :param disabled_admins: A dictionary of currently disabled admin users by their usernames """ - config: "UserManager.ConfigSchema" + config: "UserManager.ConfigSchema" = None users: Dict[str, User] = {} class ConfigSchema(Service.ConfigSchema): """ConfigSchema for UserManager.""" - type: str = "USERMANAGER" + type: str = "USER_MANAGER" def __init__(self, **kwargs): """ - Initializes a UserManager instanc. + Initializes a UserManager instance. :param username: The username for the default admin user :param password: The password for the default admin user @@ -1144,7 +1144,7 @@ class UserSessionManager(Service, identifier="UserSessionManager"): This class handles authentication, session management, and session timeouts for users interacting with the Node. """ - config: "UserSessionManager.ConfigSchema" + config: "UserSessionManager.ConfigSchema" = None local_session: Optional[UserSession] = None """The current local user session, if any.""" @@ -1170,7 +1170,7 @@ class UserSessionManager(Service, identifier="UserSessionManager"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for UserSessionManager.""" - type: str = "USERSESSIONMANAGER" + type: str = "USER_SESSION_MANAGER" def __init__(self, **kwargs): """ diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 402c64f2..ffe53baa 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -30,7 +30,7 @@ class Application(IOSoftware): Applications are user-facing programs that may perform input/output operations. """ - config: "Application.ConfigSchema" + config: "Application.ConfigSchema" = None operating_state: ApplicationOperatingState = ApplicationOperatingState.CLOSED "The current operating state of the Application." diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index cc593a30..62bcbcaf 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -70,7 +70,7 @@ class DatabaseClient(Application, identifier="DatabaseClient"): """ - config: "DatabaseClient.ConfigSchema" + config: "DatabaseClient.ConfigSchema" = None server_ip_address: Optional[IPv4Address] = None """The IPv4 address of the Database Service server, defaults to None.""" diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 3f9724ca..d2dc84be 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -52,7 +52,7 @@ class NMAP(Application, identifier="NMAP"): as ping scans to discover active hosts and port scans to detect open ports on those hosts. """ - config: "NMAP.ConfigSchema" + config: "NMAP.ConfigSchema" = None _active_port_scans: Dict[str, PortScanPayload] = {} _port_scan_responses: Dict[str, PortScanPayload] = {} diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 9961e790..056c93bc 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -60,10 +60,10 @@ class AbstractC2(Application, identifier="AbstractC2"): Defaults to masquerading as HTTP (Port 80) via TCP. - Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. + Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ - config: "AbstractC2" + config: "AbstractC2.ConfigSchema" = None c2_connection_active: bool = False """Indicates if the c2 server and c2 beacon are currently connected.""" @@ -80,7 +80,7 @@ class AbstractC2(Application, identifier="AbstractC2"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for AbstractC2.""" - type: str = "ABSTRACTC2" + type: str = "ABSTRACT_C2" class _C2Opts(BaseModel): """A Pydantic Schema for the different C2 configuration options.""" diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 98cb85ba..b6e730e2 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -36,7 +36,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ - config: "C2Beacon.ConfigSchema" + config: "C2Beacon.ConfigSchema" = None keep_alive_attempted: bool = False """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" @@ -47,7 +47,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for C2Beacon.""" - type: str = "C2BEACON" + type: str = "C2_BEACON" @property def _host_terminal(self) -> Optional[Terminal]: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index b5ea9e08..4a887783 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -32,10 +32,10 @@ class C2Server(AbstractC2, identifier="C2Server"): 1. Sending commands to the C2 Beacon. (Command input) 2. Parsing terminal RequestResponses back to the Agent. - Please refer to the Command-&-Control notebook for an in-depth example of the C2 Suite. + Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ - config: "C2Server.ConfigSchema" + config: "C2Server.ConfigSchema" = None current_command_output: RequestResponse = None """The Request Response by the last command send. This attribute is updated by the method _handle_command_output.""" @@ -43,7 +43,7 @@ class C2Server(AbstractC2, identifier="C2Server"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for C2Server.""" - type: str = "C2SERVER" + type: str = "C2_SERVER" def _init_request_manager(self) -> RequestManager: """ diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index a02b04c5..36decaab 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -33,7 +33,7 @@ class DoSAttackStage(IntEnum): class DoSBot(DatabaseClient, identifier="DoSBot"): """A bot that simulates a Denial of Service attack.""" - config: "DoSBot.ConfigSchema" + config: "DoSBot.ConfigSchema" = None target_ip_address: Optional[IPv4Address] = None """IP address of the target service.""" @@ -59,7 +59,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for DoSBot.""" - type: str = "DOSBOT" + type: str = "DOS_BOT" def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 236cde79..6bb27d69 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -18,7 +18,7 @@ class RansomwareScript(Application, identifier="RansomwareScript"): :ivar payload: The attack stage query payload. (Default ENCRYPT) """ - config: "RansomwareScript.ConfigSchema" + config: "RansomwareScript.ConfigSchema" = None server_ip_address: Optional[IPv4Address] = None """IP address of node which hosts the database.""" diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 271aec71..16cf1975 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -30,7 +30,7 @@ class WebBrowser(Application, identifier="WebBrowser"): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ - config: "WebBrowser.ConfigSchema" + config: "WebBrowser.ConfigSchema" = None target_url: Optional[str] = None @@ -46,7 +46,7 @@ class WebBrowser(Application, identifier="WebBrowser"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for WebBrowser.""" - type: str = "WEBBROWSER" + type: str = "WEB_BROWSER" def __init__(self, **kwargs): kwargs["name"] = "WebBrowser" diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 91b58bc4..d78e9aba 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -22,7 +22,7 @@ class ARP(Service, identifier="ARP"): sends ARP requests and replies, and processes incoming ARP packets. """ - config: "ARP.ConfigSchema" + config: "ARP.ConfigSchema" = None arp: Dict[IPV4Address, ARPEntry] = {} diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index ccf566bf..0f16a731 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -24,7 +24,7 @@ class DatabaseService(Service, identifier="DatabaseService"): This class inherits from the `Service` class and provides methods to simulate a SQL database. """ - config: "DatabaseService.ConfigSchema" + config: "DatabaseService.ConfigSchema" = None password: Optional[str] = None """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" @@ -41,7 +41,7 @@ class DatabaseService(Service, identifier="DatabaseService"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DatabaseService.""" - type: str = "DATABASESERVICE" + type: str = "DATABASE_SERVICE" def __init__(self, **kwargs): kwargs["name"] = "DatabaseService" diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 78642fa6..d8a3cc4d 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -15,11 +15,17 @@ _LOGGER = getLogger(__name__) class DNSClient(Service): """Represents a DNS Client as a Service.""" + config: "DNSClient.ConfigSchema" = None dns_cache: Dict[str, IPv4Address] = {} "A dict of known mappings between domain/URLs names and IPv4 addresses." dns_server: Optional[IPv4Address] = None "The DNS Server the client sends requests to." + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for DNSClient.""" + + type: str = "DNS_CLIENT" + def __init__(self, **kwargs): kwargs["name"] = "DNSClient" kwargs["port"] = PORT_LOOKUP["DNS"] diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 05a6b373..c094a5f6 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -16,7 +16,7 @@ _LOGGER = getLogger(__name__) class DNSServer(Service, identifier="DNSServer"): """Represents a DNS Server as a Service.""" - config: "DNSServer.ConfigSchema" + config: "DNSServer.ConfigSchema" = None dns_table: Dict[str, IPv4Address] = {} "A dict of mappings between domain names and IPv4 addresses." @@ -24,7 +24,7 @@ class DNSServer(Service, identifier="DNSServer"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DNSServer.""" - type: str = "DNSSERVER" + type: str = "DNS_SERVER" def __init__(self, **kwargs): kwargs["name"] = "DNSServer" diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index e8e79d85..6fe8ac7e 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -24,12 +24,12 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPClient.ConfigSchema" + config: "FTPClient.ConfigSchema" = None class ConfigSchema(Service.ConfigSchema): """ConfigSchema for FTPClient.""" - type: str = "FTPCLIENT" + type: str = "FTP_CLIENT" def __init__(self, **kwargs): kwargs["name"] = "FTPClient" diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index cbac2030..e37a3faa 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -19,7 +19,7 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPServer.ConfigSchema" + config: "FTPServer.ConfigSchema" = None server_password: Optional[str] = None """Password needed to connect to FTP server. Default is None.""" @@ -27,7 +27,7 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for FTPServer.""" - type: str = "FTPServer" + type: str = "FTP_Server" def __init__(self, **kwargs): kwargs["name"] = "FTPServer" diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index f5225f71..686da97a 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -22,7 +22,7 @@ class ICMP(Service, identifier="ICMP"): network diagnostics, notably the ping command. """ - config: "ICMP.ConfigSchema" + config: "ICMP.ConfigSchema" = None request_replies: Dict = {} diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 8c36b55f..e30a6d05 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -15,7 +15,7 @@ _LOGGER = getLogger(__name__) class NTPClient(Service, identifier="NTPClient"): """Represents a NTP client as a service.""" - config: "NTPClient.ConfigSchema" + config: "NTPClient.ConfigSchema" = None ntp_server: Optional[IPv4Address] = None "The NTP server the client sends requests to." @@ -24,7 +24,7 @@ class NTPClient(Service, identifier="NTPClient"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for NTPClient.""" - type: str = "NTPCLIENT" + type: str = "NTP_CLIENT" def __init__(self, **kwargs): kwargs["name"] = "NTPClient" diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index 538a1ec3..8855de47 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -14,12 +14,12 @@ _LOGGER = getLogger(__name__) class NTPServer(Service, identifier="NTPServer"): """Represents a NTP server as a service.""" - config: "NTPServer.ConfigSchema" + config: "NTPServer.ConfigSchema" = None class ConfigSchema(Service.ConfigSchema): """ConfigSchema for NTPServer.""" - type: str = "NTPSERVER" + type: str = "NTP_SERVER" def __init__(self, **kwargs): kwargs["name"] = "NTPServer" diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 7ecd425d..725711f0 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -132,7 +132,7 @@ class RemoteTerminalConnection(TerminalClientConnection): class Terminal(Service, identifier="Terminal"): """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" - config: "Terminal.ConfigSchema" + config: "Terminal.ConfigSchema" = None _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} """Dictionary of connect requests made to remote nodes.""" diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 0c47961d..e1f735d3 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -22,14 +22,14 @@ _LOGGER = getLogger(__name__) class WebServer(Service, identifier="WebServer"): """Class used to represent a Web Server Service in simulation.""" - config: "WebServer.ConfigSchema" + config: "WebServer.ConfigSchema" = None response_codes_this_timestep: List[HttpStatusCode] = [] class ConfigSchema(Service.ConfigSchema): """ConfigSchema for WebServer.""" - type: str = "WEBSERVER" + type: str = "WEB_SERVER" def describe_state(self) -> Dict: """ diff --git a/tests/conftest.py b/tests/conftest.py index 071d7d99..a4cc77d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,12 +40,12 @@ _LOGGER = getLogger(__name__) class DummyService(Service, identifier="DummyService"): """Test Service class""" - config: "DummyService.ConfigSchema" + config: "DummyService.ConfigSchema" = None class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DummyService.""" - type: str = "DUMMYSERVICE" + type: str = "DUMMY_SERVICE" def describe_state(self) -> Dict: return super().describe_state() @@ -63,12 +63,12 @@ class DummyService(Service, identifier="DummyService"): class DummyApplication(Application, identifier="DummyApplication"): """Test Application class""" - config: "DummyApplication.ConfigSchema" + config: "DummyApplication.ConfigSchema" = None class ConfigSchema(Application.ConfigSchema): """ConfigSchema for DummyApplication.""" - type: str = "DUMMYAPPLICATION" + type: str = "DUMMY_APPLICATION" def __init__(self, **kwargs): kwargs["name"] = "DummyApplication" diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 189d7975..f2afad0d 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -31,7 +31,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ - config: "ExtendedApplication.ConfigSchema" + config: "ExtendedApplication.ConfigSchema" = None target_url: Optional[str] = None @@ -47,7 +47,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for ExtendedApplication.""" - type: str = "EXTENDEDAPPLICATION" + type: str = "EXTENDED_APPLICATION" def __init__(self, **kwargs): kwargs["name"] = "ExtendedApplication" diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index 675e0f53..2304769f 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -17,12 +17,12 @@ from primaite.utils.validation.port import PORT_LOOKUP class BroadcastTestService(Service, identifier="BroadcastTestService"): """A service for sending broadcast and unicast messages over a network.""" - config: "BroadcastTestService.ConfigSchema" + config: "BroadcastTestService.ConfigSchema" = None class ConfigSchema(Service.ConfigSchema): """ConfigSchema for BroadcastTestService.""" - type: str = "BROADCASTTESTSERVICE" + type: str = "BROADCAST_TEST_SERVICE" def __init__(self, **kwargs): # Set default service properties for broadcasting 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 7a085ee1..6673c2c9 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -14,13 +14,19 @@ from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT -class _DatabaseListener(Service): +class _DatabaseListener(Service, identifier="_DatabaseListener"): + config: "_DatabaseListener.ConfigSchema" = None name: str = "DatabaseListener" protocol: str = PROTOCOL_LOOKUP["TCP"] port: int = PORT_LOOKUP["NONE"] listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} payloads_received: List[Any] = Field(default_factory=list) + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for _DatabaseListener.""" + + type: str = "_DATABASE_LISTENER" + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: self.payloads_received.append(payload) self.sys_log.info(f"{self.name}: received payload {payload}") From 47ed585ee2e4cbc2acbb962bf57fca8f9a31257b Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 12 Dec 2024 16:08:11 +0000 Subject: [PATCH 086/224] #2912 - Replace DONOTHING reference with do_nothing, tweaks following milpac actions --- src/primaite/_legacy/actions.py | 20 +++++++++++-------- src/primaite/game/agent/actions/manager.py | 5 ++++- src/primaite/game/agent/actions/node.py | 3 ++- src/primaite/game/agent/rewards.py | 8 ++++---- .../scripted_agents/data_manipulation_bot.py | 2 +- .../agent/scripted_agents/random_agent.py | 2 +- .../game/agent/scripted_agents/tap001.py | 2 +- .../Data-Manipulation-E2E-Demonstration.ipynb | 14 ++++++------- .../actions/test_c2_suite_actions.py | 2 +- .../actions/test_node_request_permission.py | 6 +++--- .../game_layer/test_RNG_seed.py | 8 ++++---- .../game_layer/test_action_mask.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_rewards.py | 10 +++++----- .../_primaite/_game/_agent/test_actions.py | 6 +++--- .../_game/_agent/test_sticky_rewards.py | 16 +++++++-------- 16 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/primaite/_legacy/actions.py b/src/primaite/_legacy/actions.py index 64cbe0cf..0eda7d86 100644 --- a/src/primaite/_legacy/actions.py +++ b/src/primaite/_legacy/actions.py @@ -455,11 +455,12 @@ class NodeAbstractAction(AbstractAction): Any action which applies to a node and uses node_id as its only parameter can inherit from this base class. """ - @abstractmethod - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes} - self.verb: str # define but don't initialise: defends against children classes not defining this + config: "NodeAbstractAction.ConfigSchema" + + class ConfigSchema(AbstractAction.ConfigSchema): + """Configuration schema for NodeAbstractAction.""" + + verb: str = "Node_Abstract_Action" def form_request(self, node_id: int) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -478,9 +479,12 @@ class NodeOSScanAction(NodeAbstractAction): class NodeShutdownAction(NodeAbstractAction): """Action which shuts down a node.""" - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes) - self.verb: str = "shutdown" + config: "NodeShutdownAction.ConfigSchema" + + class ConfigSchema(NodeAbstractAction.ConfigSchema): + """Configuration Schema for NodeShutdownAction.""" + + verb: str = "shutdown" class NodeStartupAction(NodeAbstractAction): diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index b89704f4..a6a4f5a6 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -28,7 +28,7 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): """Do Nothing Action.""" class ConfigSchema(AbstractAction.ConfigSchema): - """Configuration Schema for DoNothingAction.""" + """Configuration Schema for do_nothingAction.""" type: str = "do_nothing" @@ -44,6 +44,7 @@ class ActionManager: def __init__( self, actions: List[Dict], # stores list of actions available to agent + nodes: List[Dict], # extra configuration for each node act_map: Optional[ Dict[int, Dict] ] = None, # allows restricting set of possible actions - TODO: Refactor to be a list? @@ -79,6 +80,8 @@ class ActionManager: self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} # make sure all numbers between 0 and N are represented as dict keys in action map assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) + self.node_names: List[str] = [n["node_name"] for n in nodes] + """List of node names in this action space. The list order is the mapping between node index and node name.""" def get_action(self, action: int) -> Tuple[str, Dict]: """Produce action in CAOS format.""" diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 4ecc1393..480cb8da 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -34,7 +34,8 @@ class NodeAbstractAction(AbstractAction, identifier="node_abstract"): @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["network", "node", config.node_name, cls.config.verb] + print(config) + return ["network", "node", config.node_name, config.verb] class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 1de34b40..f528c851 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -447,7 +447,7 @@ class SharedReward(AbstractReward): class ActionPenalty(AbstractReward): - """Apply a negative reward when taking any action except DONOTHING.""" + """Apply a negative reward when taking any action except do_nothing.""" def __init__(self, action_penalty: float, do_nothing_penalty: float) -> None: """ @@ -455,9 +455,9 @@ class ActionPenalty(AbstractReward): Reward or penalise agents for doing nothing or taking actions. - :param action_penalty: Reward to give agents for taking any action except DONOTHING + :param action_penalty: Reward to give agents for taking any action except do_nothing :type action_penalty: float - :param do_nothing_penalty: Reward to give agent for taking the DONOTHING action + :param do_nothing_penalty: Reward to give agent for taking the do_nothing action :type do_nothing_penalty: float """ self.action_penalty = action_penalty @@ -473,7 +473,7 @@ class ActionPenalty(AbstractReward): :return: Reward value :rtype: float """ - if last_action_response.action == "DONOTHING": + if last_action_response.action == "do_nothing": return self.do_nothing_penalty else: return self.action_penalty diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 129fac1a..c245d687 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -39,7 +39,7 @@ class DataManipulationAgent(AbstractScriptedAgent): """ if timestep < self.next_execution_timestep: self.logger.debug(msg="Performing do NOTHING") - return "DONOTHING", {} + return "do_nothing", {} self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) self.logger.info(msg="Performing a data manipulation attack!") diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index df9273f7..eade3a0c 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -81,4 +81,4 @@ class PeriodicAgent(AbstractScriptedAgent): self._set_next_execution_timestep(timestep + self.settings.frequency, self.settings.variance) return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} - return "DONOTHING", {} + return "do_nothing", {} diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index c4f6062a..6d370654 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -46,7 +46,7 @@ class TAP001(AbstractScriptedAgent): :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: - return "DONOTHING", {} + return "do_nothing", {} self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 0460f771..89620215 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -165,13 +165,13 @@ "\n", "| node_id | node name |\n", "|---------|------------------|\n", - "| 1 | domain_controller|\n", - "| 2 | web_server |\n", - "| 3 | database_server |\n", - "| 4 | backup_server |\n", - "| 5 | security_suite |\n", - "| 6 | client_1 |\n", - "| 7 | client_2 |\n", + "| 0 | domain_controller|\n", + "| 1 | web_server |\n", + "| 2 | database_server |\n", + "| 3 | backup_server |\n", + "| 4 | security_suite |\n", + "| 5 | client_1 |\n", + "| 6 | client_2 |\n", "\n", "Service 1 on node 2 (web_server) corresponds to the Web Server service. Other services are only there for padding to ensure that each node's observation space has the same shape. They are filled with zeroes.\n", "\n", diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 187fb1fe..d73c9834 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -134,7 +134,7 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA # Stepping a few timesteps to allow for the RansowmareScript to finish installing. - action = ("DONOTHING", {}) + action = ("do_nothing", {}) agent.store_action(action) game.step() game.step() diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index fdf04ad5..c34103bc 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -36,7 +36,7 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.SHUTTING_DOWN for i in range(client_1.shut_down_duration + 1): - action = ("DONOTHING", {"node_id": 0}) + action = ("do_nothing", {"node_id": 0}) agent.store_action(action) game.step() @@ -50,7 +50,7 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.BOOTING for i in range(client_1.start_up_duration + 1): - action = ("DONOTHING", {"node_id": 0}) + action = ("do_nothing", {"node_id": 0}) agent.store_action(action) game.step() @@ -80,7 +80,7 @@ def test_node_cannot_be_shut_down_if_node_is_already_off(game_and_agent_fixture: client_1.power_off() for i in range(client_1.shut_down_duration + 1): - action = ("DONOTHING", {"node_id": 0}) + action = ("do_nothing", {"node_id": 0}) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index 0c6d567d..e772af32 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -24,12 +24,12 @@ def test_rng_seed_set(create_env): env.reset(seed=3) for i in range(100): env.step(0) - a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] env.reset(seed=3) for i in range(100): env.step(0) - b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] assert a == b @@ -40,11 +40,11 @@ def test_rng_seed_unset(create_env): env.reset() for i in range(100): env.step(0) - a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] env.reset() for i in range(100): env.step(0) - b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "DONOTHING"] + b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] assert a != b diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 64464724..7a1475c2 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -91,7 +91,7 @@ def test_mask_contents_correct(): assert mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "DONOTHING": + if act_type == "do_nothing": assert mask[action_num] if act_type == "NODE_SERVICE_DISABLE": diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 9fdf029b..859c056c 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -32,7 +32,7 @@ FIREWALL_ACTIONS_NETWORK = TEST_ASSETS_ROOT / "configs/firewall_actions_network. def test_do_nothing_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): - """Test that the DoNothingAction can form a request and that it is accepted by the simulation.""" + """Test that the do_nothingAction can form a request and that it is accepted by the simulation.""" game, agent = game_and_agent action = ("do_nothing", {}) diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 0005b508..882c0923 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -31,7 +31,7 @@ def test_WebpageUnavailablePenalty(game_and_agent): agent.reward_function.register_component(comp, 0.7) # Check that before trying to fetch the webpage, the reward is 0.0 - agent.store_action(("DONOTHING", {})) + agent.store_action(("do_nothing", {})) game.step() assert agent.reward_function.current_reward == 0.0 @@ -149,7 +149,7 @@ def test_action_penalty(): # Create an ActionPenalty Reward Penalty = ActionPenalty(action_penalty=-0.75, do_nothing_penalty=0.125) - # Assert that penalty is applied if action isn't DONOTHING + # Assert that penalty is applied if action isn't do_nothing reward_value = Penalty.calculate( state={}, last_action_response=AgentHistoryItem( @@ -163,12 +163,12 @@ def test_action_penalty(): assert reward_value == -0.75 - # Assert that no penalty applied for a DONOTHING action + # Assert that no penalty applied for a do_nothing action reward_value = Penalty.calculate( state={}, last_action_response=AgentHistoryItem( timestep=0, - action="DONOTHING", + action="do_nothing", parameters={}, request=["do_nothing"], response=RequestResponse.from_bool(True), @@ -186,7 +186,7 @@ def test_action_penalty_e2e(game_and_agent): agent.reward_function.register_component(comp, 1.0) - action = ("DONOTHING", {}) + action = ("do_nothing", {}) agent.store_action(action) game.step() assert agent.reward_function.current_reward == 0.125 diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index c2d31ee1..46963015 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -5,7 +5,7 @@ import pytest from primaite.game.agent.actions import ( ActionManager, - DoNothingAction, + do_nothingAction, NodeServiceDisableAction, NodeServiceEnableAction, NodeServicePauseAction, @@ -18,10 +18,10 @@ from primaite.game.agent.actions import ( def test_do_nothing_action_form_request(): - """Test that the DoNothingAction can form a request and that it is correct.""" + """Test that the do_nothingAction can form a request and that it is correct.""" manager = Mock() - action = DoNothingAction(manager=manager) + action = do_nothingAction(manager=manager) request = action.form_request() diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 58f0fcc1..78113f5f 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -70,7 +70,7 @@ class TestWebpageUnavailabilitySticky: reward = WebpageUnavailablePenalty("computer", sticky=False) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} @@ -93,7 +93,7 @@ class TestWebpageUnavailabilitySticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} @@ -130,7 +130,7 @@ class TestWebpageUnavailabilitySticky: reward = WebpageUnavailablePenalty("computer", sticky=True) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} @@ -153,7 +153,7 @@ class TestWebpageUnavailabilitySticky: # THE IMPORTANT BIT # agent did nothing, because reward is sticky, it stays at 1.0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( @@ -191,7 +191,7 @@ class TestGreenAdminDatabaseUnreachableSticky: reward = GreenAdminDatabaseUnreachablePenalty("computer", sticky=False) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( @@ -212,7 +212,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} @@ -247,7 +247,7 @@ class TestGreenAdminDatabaseUnreachableSticky: reward = GreenAdminDatabaseUnreachablePenalty("computer", sticky=True) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( @@ -268,7 +268,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "DO_NOTHING", {}, ["DONOTHING"] + action, params, request = "DO_NOTHING", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( From 6380e01122efeb7851e14de3302f8ae866c8d45e Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 12 Dec 2024 17:01:40 +0000 Subject: [PATCH 087/224] #2888: Update some additional services. --- .../extensions/services/extended_service.py | 9 ++++++++- .../_primaite/_simulator/_system/test_software.py | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index ddaf4a1e..ac58091c 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -17,13 +17,15 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ExtendedService(Service, identifier="extendedservice"): +class ExtendedService(Service, identifier="ExtendedService"): """ A copy of DatabaseService that uses the extension framework instead of being part of PrimAITE. This class inherits from the `Service` class and provides methods to simulate a SQL database. """ + config: "ExtendedService.ConfigSchema" = None + password: Optional[str] = None """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" @@ -36,6 +38,11 @@ class ExtendedService(Service, identifier="extendedservice"): latest_backup_file_name: str = None """File name of latest backup.""" + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for ExtendedService.""" + + type: str = "EXTENDED_SERVICE" + def __init__(self, **kwargs): kwargs["name"] = "ExtendedService" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 300f8d9d..d9da7d73 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -10,7 +10,15 @@ from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP -class TestSoftware(Service): +class TestSoftware(Service, identifier="TestSoftware"): + + config: "TestSoftware.ConfigSchema" = None + + class ConfigSchema(Service.ConfigSchema): + """ConfigSChema for TestSoftware.""" + + type: str = "TEST_SOFTWARE" + def describe_state(self) -> Dict: pass From 3c0a70be717e7f20b08fbd65e25d98820b89d81c Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 13 Dec 2024 09:49:21 +0000 Subject: [PATCH 088/224] #2912 - Changes for extensible actions --- src/primaite/game/agent/actions/acl.py | 20 ------------------- .../scripted_agents/data_manipulation_bot.py | 11 ++++++++-- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 37dde757..d2846ddb 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -76,28 +76,8 @@ class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_ab class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema base for ACL remove rule abstract actions.""" - src_ip: str - protocol_name: str position: int - @field_validator( - "src_ip", - mode="before", - ) - @classmethod - def valid_ip(cls, v: str) -> str: - """Check that a valid IP has been provided for src and dst.""" - return ipv4_validator(v) - - @field_validator( - "protocol_name", - mode="before", - ) - @classmethod - def is_valid_protocol(cls, v: str) -> bool: - """Check that we are using a valid protocol.""" - return protocol_validator(v) - class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index c245d687..eb0ce957 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -38,12 +38,18 @@ class DataManipulationAgent(AbstractScriptedAgent): :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: - self.logger.debug(msg="Performing do NOTHING") + self.logger.debug(msg="Performing do nothing") return "do_nothing", {} self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) self.logger.info(msg="Performing a data manipulation attack!") - return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} + self.logger.info(msg=f"Chosen to attack {self.starting_node}") + # TODO: Why is the application_id hardcoded to target application 0? + return "node_application_execute", { + "type": "node_application_execute", + "node_name": self.starting_node, + "application_name": "test", + } def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" @@ -55,4 +61,5 @@ class DataManipulationAgent(AbstractScriptedAgent): # we are assuming that every node in the node manager has a data manipulation application at idx 0 num_nodes = len(self.action_manager.node_names) self.starting_node_idx = random.randint(0, num_nodes - 1) + self.starting_node = self.action_manager.node_names[self.starting_node_idx] self.logger.debug(msg=f"Select Start Node ID: {self.starting_node_idx}") From 4ac90c3c103b4d7f6205025beea33523dac7e465 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 13 Dec 2024 11:05:24 +0000 Subject: [PATCH 089/224] #2869 - Changes to agent refactor config schema, removal of state variables that aren't necessary to be in config --- src/primaite/game/agent/interface.py | 5 ++- .../agent/scripted_agents/abstract_tap.py | 4 +-- .../scripted_agents/data_manipulation_bot.py | 4 +-- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 34 +++++++++---------- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 56404e13..5bef1076 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -98,6 +98,8 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} config: "AbstractAgent.ConfigSchema" + agent_name: str = "Abstact_Agent" + logger: AgentLog = AgentLog(agent_name) class ConfigSchema(BaseModel): """ @@ -115,9 +117,6 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): :type agent_settings: Optional[AgentSettings] """ - type: str - agent_name: str = "Abstact_Agent" - logger: AgentLog = AgentLog(agent_name) history: List[AgentHistoryItem] = [] action_manager: Optional[ActionManager] = None observation_manager: Optional[ObservationManager] = None diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index a1d1eebc..bda54c58 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -11,11 +11,11 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): """Base class for TAP agents to inherit from.""" config: "AbstractTAPAgent.ConfigSchema" + agent_name: str = "Abstract_TAP" class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" - agent_name: str = "Abstract_TAP" starting_node_name: str next_execution_timestep: int @@ -40,4 +40,4 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): num_nodes = len(self.config.action_manager.node_names) starting_node_idx = random.randint(0, num_nodes - 1) self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] - self.config.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") \ No newline at end of file + self.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index dbb51b74..247e815a 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -10,15 +10,15 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" config: "DataManipulationAgent.ConfigSchema" + agent_name: str = "Data_Manipulation_Agent" class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" starting_application_name: str - agent_name: str = "Data_Manipulation_Agent" def __init__(self) -> None: - """Meh.""" + """Initialise DataManipulationAgent.""" self.setup_agent() @property diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 1522096e..6300b1d9 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -14,11 +14,11 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent" """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" config: "ProbabilisticAgent.ConfigSchema" + agent_name: str = "Probabilistic_Agent" class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Probabilistic Agent.""" - agent_name: str = "Probabilistic_Agent" action_space: ActionManager action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index e11e3352..8b0a3591 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -31,27 +31,27 @@ class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): """Agent that does nothing most of the time, but executes application at regular intervals (with variance).""" - config: "PeriodicAgent.ConfigSchema" + config: "PeriodicAgent.ConfigSchema" = {} class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" - agent_name = "Periodic_Agent" - """Name of the agent.""" - start_step: int = 20 - "The timestep at which an agent begins performing it's actions." - start_variance: int = 5 - "Deviation around the start step." - frequency: int = 5 - "The number of timesteps to wait between performing actions." - variance: int = 0 - "The amount the frequency can randomly change to." - max_executions: int = 999999 - "Maximum number of times the agent can execute its action." - num_executions: int = 0 - """Number of times the agent has executed an action.""" - next_execution_timestep: int = 0 - """Timestep of the next action execution by the agent.""" + agent_name = "Periodic_Agent" + """Name of the agent.""" + start_step: int = 20 + "The timestep at which an agent begins performing it's actions." + start_variance: int = 5 + "Deviation around the start step." + frequency: int = 5 + "The number of timesteps to wait between performing actions." + variance: int = 0 + "The amount the frequency can randomly change to." + max_executions: int = 999999 + "Maximum number of times the agent can execute its action." + num_executions: int = 0 + """Number of times the agent has executed an action.""" + next_execution_timestep: int = 0 + """Timestep of the next action execution by the agent.""" @property def num_executions(self) -> int: From 4c20cd4ac61045f7f954cdb47643888e3b94f384 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 13 Dec 2024 15:28:01 +0000 Subject: [PATCH 090/224] #2869 - initial creation of how to section for the new extendable agents. --- CHANGELOG.md | 7 ++ .../how_to_guides/extensible_agents.rst | 73 +++++++++++++++++++ src/primaite/game/agent/agent_log.py | 1 + .../agent/scripted_agents/random_agent.py | 15 +--- 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 docs/source/how_to_guides/extensible_agents.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index e9147947..3aec3ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.0.0] = TBC + +### Added + +### Changed +- Agents now follow a common configuration format, simplifying the configuration of agents and their extensibilty. + ## [3.3.0] - 2024-09-04 ### Added diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst new file mode 100644 index 00000000..718ea09a --- /dev/null +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -0,0 +1,73 @@ +.. only:: comment + + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +.. _about: + +Extensible Agents +***************** + +Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents. + + +Developing Agents for PrimAITE +============================== + +Agents within PrimAITE, follow the shown inheritance structure, and + +# TODO: Turn this into an inheritance diagram + +AbstractAgent + | + | - AbstractScriptedAgent + | | + | | - AbstractTAPAgent + | | | + | | | - DataManipulationAgent + | | + | | + | | - RandomAgent + | | + | | - PeriodicAgent + | | + | | - RandomAgent + | + | + | - ProxyAgent + | + | - ControlledAgent + + +#. **ConfigSchema**: + + Configurable items within a new agent within PrimAITE should contain a ``ConfigSchema`` which holds all configurable variables of the agent. This should not include parameters related to its *state*. + + + .. code-block:: python + + class ExampleAgent(AbstractAgent, identifier = "example_agent"): + """An example agent for demonstration purposes.""" + + config: "ExampleAgent.ConfigSchema" + """Agent configuration""" + num_executions: int + """Number of action executions by agent""" + + class ConfigSchema(AbstractAgent.ConfigSchema): + """ExampleAgent configuration schema""" + + agent_name: str + """Name of agent""" + action_interval: int + """Number of steps between agent actions""" + +#. **identifier**: + + All agent classes should have a unique ``identifier`` attribute, for when they are added to the base ``AbstractAgent`` registry. PrimAITE notation is for these to be written in snake_case + +Changes to YAML file +==================== + +Agent configurations specified within YAML files used for earlier versions of PrimAITE will need updating to be compatible with PrimAITE v4.0.0+. + +# TODO: Show changes to YAML config needed here diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 7f7b6ffd..6eaf9e73 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -30,6 +30,7 @@ class AgentLog(BaseModel): agent_name: str = "unnamed_agent" current_episode: int = 1 current_timestep: int = 0 + logger: logging def __init__(self, agent_name: Optional[str]): """ diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 8b0a3591..0e9d2763 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -53,16 +53,6 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" - @property - def num_executions(self) -> int: - """Convenience method for accessing num_executions from config.""" - return self.config.num_executions - - @property - def next_execution_timestep(self) -> int: - """Convenience method for accessing next_execution_timestep from config.""" - return self.config.next_execution_timestep - def _set_next_execution_timestep(self, timestep: int, variance: int) -> None: """Set the next execution timestep with a configured random variance. @@ -78,7 +68,8 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): """Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" if timestep == self.next_execution_timestep and self.num_executions < self.config.max_executions: self.num_executions += 1 - self._set_next_execution_timestep(timestep + self.config.frequency, self.config.variance) - return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} + self._set_next_execution_timestep(timestep + self.frequency, self.variance) + self.target_node = self.action_manager.node_names[0] + return "node_application_execute", {"node_name": self.target_node, "application_name": 0} return "DONOTHING", {} From c3a70be8d14ddc64c7a31a58b8d2354dfe5f529f Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 13 Dec 2024 16:37:39 +0000 Subject: [PATCH 091/224] #2869 - Changes to AbstractAgent to address some pydantic issues --- .../how_to_guides/extensible_agents.rst | 3 +-- src/primaite/game/agent/agent_log.py | 16 ++++--------- src/primaite/game/agent/interface.py | 24 +++++++++---------- .../scripted_agents/data_manipulation_bot.py | 8 +++---- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 718ea09a..b694f882 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -42,7 +42,6 @@ AbstractAgent Configurable items within a new agent within PrimAITE should contain a ``ConfigSchema`` which holds all configurable variables of the agent. This should not include parameters related to its *state*. - .. code-block:: python class ExampleAgent(AbstractAgent, identifier = "example_agent"): @@ -63,7 +62,7 @@ AbstractAgent #. **identifier**: - All agent classes should have a unique ``identifier`` attribute, for when they are added to the base ``AbstractAgent`` registry. PrimAITE notation is for these to be written in snake_case + All agent classes should have a ``identifier`` attribute, a unique snake_case string, for when they are added to the base ``AbstractAgent`` registry. Changes to YAML file ==================== diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 6eaf9e73..f12c49f7 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -4,7 +4,6 @@ from pathlib import Path from typing import Optional from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel from primaite.simulator import LogLevel, SIM_OUTPUT @@ -20,28 +19,23 @@ class _NotJSONFilter(logging.Filter): return not record.getMessage().startswith("{") and not record.getMessage().endswith("}") -class AgentLog(BaseModel): +class AgentLog: """ A Agent Log class is a simple logger dedicated to managing and writing logging updates and information for an agent. Each log message is written to a file located at: /agent_name/agent_name.log """ - agent_name: str = "unnamed_agent" - current_episode: int = 1 - current_timestep: int = 0 - logger: logging - def __init__(self, agent_name: Optional[str]): """ Constructs a Agent Log instance for a given hostname. - :param hostname: The hostname associated with the system logs being recorded. + :param agent_name: The agent_name associated with the system logs being recorded. """ super().__init__() - self.agent_name = agent_name or "unnamed_agent" - # self.current_episode: int = 1 - # self.current_timestep: int = 0 + self.agent_name = agent_name if agent_name else "unnamed_agent" + self.current_timestep: int = 0 + self.current_episode: int = 0 self.setup_logger() @property diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 5bef1076..0c208f71 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -2,7 +2,7 @@ """Interface for agents.""" from __future__ import annotations -from abc import ABC, abstractmethod +from abc import abstractmethod from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union from gymnasium.core import ActType, ObsType @@ -92,14 +92,12 @@ class AgentSettings(BaseModel): return cls(**config) -class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): +class AbstractAgent(BaseModel, identifier="Abstract_Agent"): """Base class for scripted and RL agents.""" _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} config: "AbstractAgent.ConfigSchema" - agent_name: str = "Abstact_Agent" - logger: AgentLog = AgentLog(agent_name) class ConfigSchema(BaseModel): """ @@ -117,11 +115,13 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): :type agent_settings: Optional[AgentSettings] """ + agent_name: str = "Abstract_Agent" history: List[AgentHistoryItem] = [] - action_manager: Optional[ActionManager] = None - observation_manager: Optional[ObservationManager] = None - reward_function: Optional[RewardFunction] = None - agent_settings: Optional[AgentSettings] = None + _logger: AgentLog = AgentLog(agent_name=agent_name) + _action_manager: Optional[ActionManager] = None + _observation_manager: Optional[ObservationManager] = None + _reward_function: Optional[RewardFunction] = None + _agent_settings: Optional[AgentSettings] = None def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -132,22 +132,22 @@ class AbstractAgent(BaseModel, ABC, identifier="Abstract_Agent"): @property def logger(self) -> AgentLog: """Return the AgentLog.""" - return self.config.logger + return self.config._logger @property def observation_manager(self) -> ObservationManager: """Returns the agents observation manager.""" - return self.config.observation_manager + return self.config._observation_manager @property def action_manager(self) -> ActionManager: """Returns the agents action manager.""" - return self.config.action_manager + return self.config._action_manager @property def reward_function(self) -> RewardFunction: """Returns the agents reward function.""" - return self.config.reward_function + return self.config._reward_function @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 247e815a..5927cd09 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -42,11 +42,11 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: - self.config.logger.debug(msg="Performing do nothing action") + self.logger.debug(msg="Performing do nothing action") return "do_nothing", {} - self._set_next_execution_timestep(timestep + self.config.agent_settings.start_settings.frequency) - self.config.logger.info(msg="Performing a data manipulation attack!") + self._set_next_execution_timestep(timestep + self.config._agent_settings.start_settings.frequency) + self.logger.info(msg="Performing a data manipulation attack!") return "node_application_execute", { "node_name": self.config.starting_node_name, "application_name": self.config.starting_application_name, @@ -55,4 +55,4 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" self._select_start_node() - self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) + self._set_next_execution_timestep(self.config._agent_settings.start_settings.start_step) From d9a1a0e26f949a65bca01a9b49543226a9b31b69 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 16 Dec 2024 11:27:14 +0000 Subject: [PATCH 092/224] 2869 - Addressing some typos in agent declaration, and neatening up the agent structure within PrimAITE. --- docs/source/how_to_guides/extensible_agents.rst | 3 +++ src/primaite/game/agent/rewards.py | 2 +- .../game/agent/scripted_agents/__init__.py | 10 ++++++++++ .../game/agent/scripted_agents/abstract_tap.py | 14 +++++++++++--- .../scripted_agents/data_manipulation_bot.py | 2 +- .../agent/{ => scripted_agents}/interface.py | 17 ++++++++++------- .../scripted_agents/probabilistic_agent.py | 15 ++++++++------- .../game/agent/scripted_agents/random_agent.py | 2 +- src/primaite/game/game.py | 4 +++- src/primaite/session/environment.py | 2 +- src/primaite/session/ray_envs.py | 2 +- tests/conftest.py | 2 +- .../software_installation_and_configuration.py | 2 +- .../test_application_request_permission.py | 2 +- .../game_layer/actions/test_c2_suite_actions.py | 2 +- .../actions/test_file_request_permission.py | 2 +- .../actions/test_folder_request_permission.py | 2 +- .../actions/test_nic_request_permission.py | 2 +- .../actions/test_node_request_permission.py | 2 +- .../actions/test_service_request_permission.py | 2 +- .../game_layer/actions/test_terminal_actions.py | 2 +- .../observations/test_nic_observations.py | 2 +- .../game_layer/test_RNG_seed.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_rewards.py | 2 +- .../test_c2_suite_integration.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 2 +- .../_system/_services/test_terminal.py | 2 +- 28 files changed, 67 insertions(+), 40 deletions(-) rename src/primaite/game/agent/{ => scripted_agents}/interface.py (96%) diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index b694f882..6ccb80cd 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -69,4 +69,7 @@ Changes to YAML file Agent configurations specified within YAML files used for earlier versions of PrimAITE will need updating to be compatible with PrimAITE v4.0.0+. +Agents now follow a more standardised settings definition, so should be more consistent across YAML. + + # TODO: Show changes to YAML config needed here diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 1de34b40..3c83731f 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -36,7 +36,7 @@ from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE if TYPE_CHECKING: - from primaite.game.agent.interface import AgentHistoryItem + from primaite.game.agent.scripted_agents.interface import AgentHistoryItem _LOGGER = getLogger(__name__) WhereType = Optional[Iterable[Union[str, int]]] diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py index be6c00e7..6237d430 100644 --- a/src/primaite/game/agent/scripted_agents/__init__.py +++ b/src/primaite/game/agent/scripted_agents/__init__.py @@ -1 +1,11 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + +from primaite.game.agent.scripted_agents import ( + abstract_tap, + data_manipulation_bot, + interface, + probabilistic_agent, + random_agent, +) + +__all__ = ("abstract_tap", "data_manipulation_bot", "interface", "probabilistic_agent", "random_agent") diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index bda54c58..fb3f1688 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -3,8 +3,11 @@ from __future__ import annotations import random from abc import abstractmethod +from typing import Dict, Tuple -from primaite.game.agent.interface import AbstractScriptedAgent +from gymnasium.core import ObsType + +from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): @@ -12,12 +15,17 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): config: "AbstractTAPAgent.ConfigSchema" agent_name: str = "Abstract_TAP" + _next_execution_timestep: int class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" starting_node_name: str - next_execution_timestep: int + + @abstractmethod + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: + """Return an action to be taken in the environment.""" + return super().get_action(obs=obs, timestep=timestep) @abstractmethod def setup_agent(self) -> None: @@ -32,7 +40,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): random_timestep_increment = random.randint( -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance ) - self.config.next_execution_timestep = timestep + random_timestep_increment + self._next_execution_timestep = timestep + random_timestep_increment def _select_start_node(self) -> None: """Set the starting starting node of the agent to be a random node from this agent's action manager.""" diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 5927cd09..a8b8d292 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -24,7 +24,7 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen @property def next_execution_timestep(self) -> int: """Returns the agents next execution timestep.""" - return self.config.next_execution_timestep + return self._next_execution_timestep @property def starting_node_name(self) -> str: diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/scripted_agents/interface.py similarity index 96% rename from src/primaite/game/agent/interface.py rename to src/primaite/game/agent/scripted_agents/interface.py index 0c208f71..bc083ecf 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/scripted_agents/interface.py @@ -3,10 +3,10 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING, Union +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING from gymnasium.core import ActType, ObsType -from pydantic import BaseModel, model_validator +from pydantic import BaseModel, ConfigDict, model_validator from primaite.game.agent.actions import ActionManager from primaite.game.agent.agent_log import AgentLog @@ -92,7 +92,7 @@ class AgentSettings(BaseModel): return cls(**config) -class AbstractAgent(BaseModel, identifier="Abstract_Agent"): +class AbstractAgent(BaseModel): """Base class for scripted and RL agents.""" _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} @@ -116,6 +116,7 @@ class AbstractAgent(BaseModel, identifier="Abstract_Agent"): """ agent_name: str = "Abstract_Agent" + model_config = ConfigDict(extra="forbid") history: List[AgentHistoryItem] = [] _logger: AgentLog = AgentLog(agent_name=agent_name) _action_manager: Optional[ActionManager] = None @@ -124,10 +125,10 @@ class AbstractAgent(BaseModel, identifier="Abstract_Agent"): _agent_settings: Optional[AgentSettings] = None def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: - super().__init_subclass__(**kwargs) if identifier in cls._registry: raise ValueError(f"Cannot create a new agent under reserved name {identifier}") cls._registry[identifier] = cls + super().__init_subclass__(**kwargs) @property def logger(self) -> AgentLog: @@ -218,6 +219,8 @@ class AbstractAgent(BaseModel, identifier="Abstract_Agent"): class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): """Base class for actors which generate their own behaviour.""" + config: "AbstractScriptedAgent.ConfigSchema" + class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for AbstractScriptedAgents.""" @@ -233,20 +236,20 @@ class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): """Agent that sends observations to an RL model and receives actions from that model.""" config: "ProxyAgent.ConfigSchema" + _most_recent_action: ActType class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Proxy Agent.""" agent_name: str = "Proxy_Agent" - agent_settings = Union[AgentSettings | None] = None - most_reason_action: ActType + agent_settings: AgentSettings = None flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False action_masking: bool = agent_settings.action_masking if agent_settings else False @property def most_recent_action(self) -> ActType: """Convenience method to access the agents most recent action.""" - return self.config.most_recent_action + return self._most_recent_action def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 6300b1d9..c2d7d580 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -7,14 +7,14 @@ import pydantic from gymnasium.core import ObsType from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent -class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent"): +class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" config: "ProbabilisticAgent.ConfigSchema" - agent_name: str = "Probabilistic_Agent" + agent_name: str = "ProbabilisticAgent" class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Probabilistic Agent.""" @@ -42,10 +42,11 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="Probabilistic_Agent" ) return v - def __init__(self) -> None: - rng_seed = np.random.randint(0, 65535) - self.rng = np.random.default_rng(rng_seed) - self.logger.debug(f"ProbabilisticAgent RNG seed: {rng_seed}") + # def __init__(self, **kwargs) -> None: + # rng_seed = np.random.randint(0, 65535) + # self.rng = np.random.default_rng(rng_seed) + # self.logger.debug(f"ProbabilisticAgent RNG seed: {rng_seed}") + # super().__init_subclass__(**kwargs) @property def probabilities(self) -> Dict[str, int]: diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 0e9d2763..b0c0f7ce 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -4,7 +4,7 @@ from typing import Dict, Tuple from gymnasium.core import ObsType -from primaite.game.agent.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 9ef75fb9..f307bba5 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -8,9 +8,9 @@ from pydantic import BaseModel, ConfigDict from primaite import DEFAULT_BANDWIDTH, getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, ProxyAgent from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction, SharedReward +from primaite.game.agent.scripted_agents.interface import AbstractAgent, ProxyAgent from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.creation import NetworkNodeAdder @@ -549,6 +549,8 @@ class PrimaiteGame: {"action_manager": action_space, "observation_manager": obs_space, "reward_function": reward_function} ) # new_agent_cfg.update{} + print(AbstractAgent._registry) + if agent_type in AbstractAgent._registry: new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) # If blue agent is created, add to game.rl_agents diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index c66663e3..ab7b68f0 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -10,7 +10,7 @@ import numpy as np from gymnasium.core import ActType, ObsType from primaite import getLogger -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler from primaite.session.io import PrimaiteIO diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 33c74b0e..2d540237 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -7,7 +7,7 @@ from gymnasium import spaces from gymnasium.core import ActType, ObsType from ray.rllib.env.multi_agent_env import MultiAgentEnv -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import _LOGGER, PrimaiteGymEnv from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler diff --git a/tests/conftest.py b/tests/conftest.py index 27032540..b693a5e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,9 @@ from ray import init as rayinit from primaite import getLogger, PRIMAITE_PATHS from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction +from primaite.game.agent.scripted_agents.interface import AbstractAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index a642564c..bea18fb0 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -6,8 +6,8 @@ from typing import Union import yaml from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.game.game import PrimaiteGame, SERVICE_TYPES_MAPPING from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index 36a7ae57..24e0d67e 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 187fb1fe..9d77536e 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.base import UserManager diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 1c143aed..39b3fc8f 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index e5e0806a..19d549f5 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index d796b75e..53629332 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index fdf04ad5..baf79007 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index 3054c73b..d0099f6d 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index a70cea72..fa103805 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.base import UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 8254dad2..eb5aca3a 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -6,8 +6,8 @@ import pytest import yaml from gymnasium import spaces -from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.observations.nic_observations import NICObservation +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index 0c6d567d..a7a2b6c3 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -5,7 +5,7 @@ import pytest import yaml from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.interface import AgentHistoryItem +from primaite.game.agent.scripted_agents.interface import AgentHistoryItem from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index e03a7d26..53d8edd9 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -17,7 +17,7 @@ from typing import Tuple import pytest import yaml -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 0005b508..66c2d5a0 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -2,8 +2,8 @@ import pytest import yaml -from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import ActionPenalty, GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty +from primaite.game.agent.scripted_agents.interface import AgentHistoryItem from primaite.game.game import PrimaiteGame from primaite.interface.request import RequestResponse from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 2cbd4d11..3352b975 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -5,7 +5,7 @@ from typing import Tuple import pytest import yaml -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 58f0fcc1..bfcc544d 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -1,11 +1,11 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import ( GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty, WebServer404Penalty, ) +from primaite.game.agent.scripted_agents.interface import AgentHistoryItem from primaite.interface.request import RequestResponse diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 9b6a4bf3..fb081f12 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -4,7 +4,7 @@ from uuid import uuid4 import pytest -from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer From a4fbd29bb4de8762423add2ed7581eb002e7aa63 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 16 Dec 2024 15:57:00 +0000 Subject: [PATCH 093/224] #2869 - Updates to agents to make sure they can be generated from a given config. Updates to test suite to reflect code changes --- .../agent/scripted_agents/abstract_tap.py | 6 ++-- .../scripted_agents/data_manipulation_bot.py | 22 +++++++++------ .../game/agent/scripted_agents/interface.py | 20 ++++++------- .../scripted_agents/probabilistic_agent.py | 26 ++++++++--------- .../agent/scripted_agents/random_agent.py | 4 +-- src/primaite/game/game.py | 20 ++++++++----- tests/conftest.py | 20 +++++++------ .../_game/_agent/test_probabilistic_agent.py | 28 +++++++++++++------ 8 files changed, 83 insertions(+), 63 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index fb3f1688..95769624 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -3,11 +3,11 @@ from __future__ import annotations import random from abc import abstractmethod -from typing import Dict, Tuple +from typing import Dict, Optional, Tuple from gymnasium.core import ObsType -from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.interface import AbstractAgent, AbstractScriptedAgent class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): @@ -20,7 +20,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" - starting_node_name: str + starting_node_name: Optional[str] = None @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index a8b8d292..0f687367 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,12 +1,12 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Dict, Tuple +from typing import Any, Dict, Optional, Tuple from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent -class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agent"): +class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" config: "DataManipulationAgent.ConfigSchema" @@ -14,12 +14,12 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" + starting_application_name: Optional[str] = None - starting_application_name: str - - def __init__(self) -> None: - """Initialise DataManipulationAgent.""" - self.setup_agent() + # def __init__(self, **kwargs: Any) -> None: + # """Initialise DataManipulationAgent.""" + # # self.setup_agent() + # super().__init_subclass__(**kwargs) @property def next_execution_timestep(self) -> int: @@ -41,11 +41,15 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen :return: Action formatted in CAOS format :rtype: Tuple[str, Dict] """ + if self.starting_node_name or self.config is None: + self.setup_agent() + self.get_action(obs=obs, timestep=timestep) + if timestep < self.next_execution_timestep: self.logger.debug(msg="Performing do nothing action") return "do_nothing", {} - self._set_next_execution_timestep(timestep + self.config._agent_settings.start_settings.frequency) + self._set_next_execution_timestep(timestep + self.config.agent_settings.start_settings.frequency) self.logger.info(msg="Performing a data manipulation attack!") return "node_application_execute", { "node_name": self.config.starting_node_name, @@ -55,4 +59,4 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="Data_Manipulation_Agen def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" self._select_start_node() - self._set_next_execution_timestep(self.config._agent_settings.start_settings.start_step) + self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) diff --git a/src/primaite/game/agent/scripted_agents/interface.py b/src/primaite/game/agent/scripted_agents/interface.py index bc083ecf..5e9167f5 100644 --- a/src/primaite/game/agent/scripted_agents/interface.py +++ b/src/primaite/game/agent/scripted_agents/interface.py @@ -115,14 +115,14 @@ class AbstractAgent(BaseModel): :type agent_settings: Optional[AgentSettings] """ - agent_name: str = "Abstract_Agent" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + agent_name: Optional[str] = "Abstract_Agent" history: List[AgentHistoryItem] = [] _logger: AgentLog = AgentLog(agent_name=agent_name) - _action_manager: Optional[ActionManager] = None - _observation_manager: Optional[ObservationManager] = None - _reward_function: Optional[RewardFunction] = None - _agent_settings: Optional[AgentSettings] = None + action_manager: ActionManager + observation_manager: ObservationManager + reward_function: RewardFunction + agent_settings: Optional[AgentSettings] = None def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: if identifier in cls._registry: @@ -138,17 +138,17 @@ class AbstractAgent(BaseModel): @property def observation_manager(self) -> ObservationManager: """Returns the agents observation manager.""" - return self.config._observation_manager + return self.config.observation_manager @property def action_manager(self) -> ActionManager: """Returns the agents action manager.""" - return self.config._action_manager + return self.config.action_manager @property def reward_function(self) -> RewardFunction: """Returns the agents reward function.""" - return self.config._reward_function + return self.config.reward_function @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": @@ -232,7 +232,7 @@ class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent") return super().get_action(obs=obs, timestep=timestep) -class ProxyAgent(AbstractAgent, identifier="Proxy_Agent"): +class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): """Agent that sends observations to an RL model and receives actions from that model.""" config: "ProxyAgent.ConfigSchema" diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index c2d7d580..750a120f 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -1,28 +1,23 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Agents with predefined behaviours.""" -from typing import Dict, Tuple +from typing import Any, Dict, Tuple import numpy as np import pydantic from gymnasium.core import ObsType -from primaite.game.agent.actions import ActionManager -from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent, AgentSettings class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" config: "ProbabilisticAgent.ConfigSchema" - agent_name: str = "ProbabilisticAgent" + rng: Any = np.random.default_rng(np.random.randint(0, 65535)) - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): - """Configuration schema for Probabilistic Agent.""" - - action_space: ActionManager + class AgentSettings(AgentSettings): action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" - @pydantic.field_validator("action_probabilities", mode="after") @classmethod def probabilities_sum_to_one(cls, v: Dict[int, float]) -> Dict[int, float]: @@ -42,16 +37,17 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") ) return v - # def __init__(self, **kwargs) -> None: - # rng_seed = np.random.randint(0, 65535) - # self.rng = np.random.default_rng(rng_seed) - # self.logger.debug(f"ProbabilisticAgent RNG seed: {rng_seed}") - # super().__init_subclass__(**kwargs) + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration schema for Probabilistic Agent.""" + + agent_name: str = "ProbabilisticAgent" + agent_settings: "ProbabilisticAgent.AgentSettings" + @property def probabilities(self) -> Dict[str, int]: """Convenience method to view the probabilities of the Agent.""" - return np.asarray(list(self.config.action_probabilities.values())) + return np.asarray(list(self.config.agent_settings.action_probabilities.values())) def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index b0c0f7ce..d28069e6 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -13,7 +13,7 @@ class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Random Agents.""" - agent_name = "Random_Agent" + agent_name: str = "Random_Agent" def get_action(self) -> Tuple[str, Dict]: """Sample the action space randomly. @@ -36,7 +36,7 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" - agent_name = "Periodic_Agent" + agent_name: str = "Periodic_Agent" """Name of the agent.""" start_step: int = 20 "The timestep at which an agent begins performing it's actions." diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f307bba5..6cf4a75a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -174,7 +174,7 @@ class PrimaiteGame: obs = agent.observation_manager.current_observation action_choice, parameters = agent.get_action(obs, timestep=self.step_counter) if SIM_OUTPUT.save_agent_logs: - agent.config.logger.debug(f"Chosen Action: {action_choice}") + agent.logger.debug(f"Chosen Action: {action_choice}") request = agent.format_request(action_choice, parameters) response = self.simulation.apply_request(request) agent.process_action_response( @@ -544,14 +544,20 @@ class PrimaiteGame: # CREATE AGENT - agent_config = agent_cfg.get("agent_settings", {}) - agent_config.update( - {"action_manager": action_space, "observation_manager": obs_space, "reward_function": reward_function} - ) - # new_agent_cfg.update{} - print(AbstractAgent._registry) + agent_settings = agent_cfg["agent_settings"] + agent_config = { + "agent_name": agent_ref, + "action_manager": action_space, + "observation_manager": obs_space, + "reward_function": reward_function, + "agent_settings": agent_settings, + } + # new_agent_cfg.update{} if agent_type in AbstractAgent._registry: + print(agent_type) + print(agent_config) + print(AbstractAgent._registry) new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) # If blue agent is created, add to game.rl_agents if agent_type == "ProxyAgent": diff --git a/tests/conftest.py b/tests/conftest.py index b693a5e6..68097830 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Any, Dict, Tuple +from typing import Any, Dict, Optional, Tuple import pytest import yaml @@ -10,6 +10,7 @@ from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.game.agent.scripted_agents.interface import AbstractAgent +from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.container import Network @@ -268,12 +269,12 @@ class ControlledAgent(AbstractAgent, identifier="Controlled_Agent"): """Agent that can be controlled by the tests.""" config: "ControlledAgent.ConfigSchema" + most_recent_action: Optional[Tuple[str, Dict]] = None class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Abstract Agent used in tests.""" agent_name: str = "Controlled_Agent" - most_recent_action: Tuple[str, Dict] def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: """Return the agent's most recent action, formatted in CAOS format.""" @@ -496,12 +497,15 @@ def game_and_agent(): observation_space = ObservationManager(NestedObservation(components={})) reward_function = RewardFunction() - test_agent = ControlledAgent( - agent_name="test_agent", - action_space=action_space, - observation_space=observation_space, - reward_function=reward_function, - ) + + config = { + "agent_name":"test_agent", + "action_manager":action_space, + "observation_manager":observation_space, + "reward_function":reward_function, + } + + test_agent = ControlledAgent.from_config(config=config) game.agents["test_agent"] = test_agent diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index ec18f1fb..6e8c9c79 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -55,15 +55,25 @@ def test_probabilistic_agent(): observation_space = ObservationManager(NestedObservation(components={})) reward_function = RewardFunction() - pa = ProbabilisticAgent( - agent_name="test_agent", - action_space=action_space, - observation_space=observation_space, - reward_function=reward_function, - settings={ - "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, - }, - ) + # pa = ProbabilisticAgent( + # agent_name="test_agent", + # action_space=action_space, + # observation_space=observation_space, + # reward_function=reward_function, + # settings={ + # "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, + # }, + # ) + + pa_config = {"agent_name":"test_agent", + "action_manager": action_space, + "observation_manager": observation_space, + "reward_function": reward_function, + "agent_settings": { + "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, + }} + + pa = ProbabilisticAgent.from_config(config=pa_config) do_nothing_count = 0 node_application_execute_count = 0 From 436a986458ccaa930b877d64ded7c9031c01f525 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 17 Dec 2024 10:51:57 +0000 Subject: [PATCH 094/224] #2869 - Fixed failing tests from agent refactor. Some tests still fail but this is due to updating some action names in anticipation of merging in the extensible actions refactor --- .../agent/scripted_agents/abstract_tap.py | 9 +++++++-- .../scripted_agents/data_manipulation_bot.py | 8 ++------ .../game/agent/scripted_agents/interface.py | 19 ++++++++++++++----- .../assets/configs/test_primaite_session.yaml | 2 ++ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index 95769624..add29b03 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -15,13 +15,18 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): config: "AbstractTAPAgent.ConfigSchema" agent_name: str = "Abstract_TAP" - _next_execution_timestep: int + next_execution_timestep: int = 0 class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" starting_node_name: Optional[str] = None + # @property + # def next_execution_timestep(self) -> int: + # """Returns the agents next execution timestep.""" + # return self.next_execution_timestep + @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """Return an action to be taken in the environment.""" @@ -40,7 +45,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): random_timestep_increment = random.randint( -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance ) - self._next_execution_timestep = timestep + random_timestep_increment + self.next_execution_timestep = timestep + random_timestep_increment def _select_start_node(self) -> None: """Set the starting starting node of the agent to be a random node from this agent's action manager.""" diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 0f687367..84cad9f6 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -14,18 +14,14 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingA class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" + starting_application_name: Optional[str] = None # def __init__(self, **kwargs: Any) -> None: # """Initialise DataManipulationAgent.""" - # # self.setup_agent() + # self.setup_agent() # super().__init_subclass__(**kwargs) - @property - def next_execution_timestep(self) -> int: - """Returns the agents next execution timestep.""" - return self._next_execution_timestep - @property def starting_node_name(self) -> str: """Returns the agents starting node name.""" diff --git a/src/primaite/game/agent/scripted_agents/interface.py b/src/primaite/game/agent/scripted_agents/interface.py index 5e9167f5..045d6d12 100644 --- a/src/primaite/game/agent/scripted_agents/interface.py +++ b/src/primaite/game/agent/scripted_agents/interface.py @@ -135,6 +135,15 @@ class AbstractAgent(BaseModel): """Return the AgentLog.""" return self.config._logger + @property + def flatten_obs(self) -> bool: + return self.config.agent_settings.flatten_obs + + @property + def history(self) -> List[AgentHistoryItem]: + """Return the agent history""" + return self.config.history + @property def observation_manager(self) -> ObservationManager: """Returns the agents observation manager.""" @@ -236,7 +245,7 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): """Agent that sends observations to an RL model and receives actions from that model.""" config: "ProxyAgent.ConfigSchema" - _most_recent_action: ActType + most_recent_action: ActType = None class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Proxy Agent.""" @@ -246,10 +255,10 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False action_masking: bool = agent_settings.action_masking if agent_settings else False - @property - def most_recent_action(self) -> ActType: - """Convenience method to access the agents most recent action.""" - return self._most_recent_action + # @property + # def most_recent_action(self) -> ActType: + # """Convenience method to access the agents most recent action.""" + # return self._most_recent_action def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 27cfa240..cf241f3c 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -47,6 +47,8 @@ agents: start_step: 25 frequency: 20 variance: 5 + action_probabilities: + 0: 1.0 - ref: data_manipulation_attacker team: RED From 3b1b74fb3a21cd37b9331ea0b5c1de79f0d90d22 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 17 Dec 2024 12:21:28 +0000 Subject: [PATCH 095/224] #2869 - Some additional test updates to amend failures. Pre-commit tbd and some cleanup --- src/primaite/game/agent/scripted_agents/abstract_tap.py | 6 +----- .../game/agent/scripted_agents/data_manipulation_bot.py | 6 +----- src/primaite/game/agent/scripted_agents/interface.py | 7 ++++++- .../game/agent/scripted_agents/probabilistic_agent.py | 1 + src/primaite/game/agent/scripted_agents/random_agent.py | 8 ++++++++ tests/assets/configs/basic_switched_network.yaml | 4 ++++ tests/assets/configs/fix_duration_one_item.yaml | 4 +++- tests/assets/configs/software_fix_duration.yaml | 4 +++- 8 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index add29b03..d30ba9b1 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -9,6 +9,7 @@ from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.interface import AbstractAgent, AbstractScriptedAgent +__all__ = ("AbstractTAPAgent") class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): """Base class for TAP agents to inherit from.""" @@ -22,11 +23,6 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): starting_node_name: Optional[str] = None - # @property - # def next_execution_timestep(self) -> int: - # """Returns the agents next execution timestep.""" - # return self.next_execution_timestep - @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """Return an action to be taken in the environment.""" diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 84cad9f6..594f1b41 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -5,6 +5,7 @@ from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent +__all__ = ("DataManipulationAgent") class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" @@ -17,11 +18,6 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingA starting_application_name: Optional[str] = None - # def __init__(self, **kwargs: Any) -> None: - # """Initialise DataManipulationAgent.""" - # self.setup_agent() - # super().__init_subclass__(**kwargs) - @property def starting_node_name(self) -> str: """Returns the agents starting node name.""" diff --git a/src/primaite/game/agent/scripted_agents/interface.py b/src/primaite/game/agent/scripted_agents/interface.py index 045d6d12..ab78eee0 100644 --- a/src/primaite/game/agent/scripted_agents/interface.py +++ b/src/primaite/game/agent/scripted_agents/interface.py @@ -17,6 +17,11 @@ from primaite.interface.request import RequestFormat, RequestResponse if TYPE_CHECKING: pass +__all__ = ("AgentHistoryItem", + "AgentStartSettings", + "AbstractAgent", + "AbstractScriptedAgent", + "ProxyAgent") class AgentHistoryItem(BaseModel): """One entry of an agent's action log - what the agent did and how the simulator responded in 1 step.""" @@ -116,7 +121,7 @@ class AbstractAgent(BaseModel): """ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - agent_name: Optional[str] = "Abstract_Agent" + agent_name: ClassVar[str] = "Abstract_Agent" # TODO: Make this a ClassVar[str] like verb in actions? history: List[AgentHistoryItem] = [] _logger: AgentLog = AgentLog(agent_name=agent_name) action_manager: ActionManager diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 750a120f..ba6ba850 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -8,6 +8,7 @@ from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent, AgentSettings +__all__ = ("ProbabilisticAgent") class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index d28069e6..fecc235f 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -6,6 +6,7 @@ from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent +__all__ = ("RandomAgent", "PeriodicAgent") class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): """Agent that ignores its observation and acts completely at random.""" @@ -38,18 +39,25 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): agent_name: str = "Periodic_Agent" """Name of the agent.""" + + # TODO: This is available in config.agent_settings.start_settings.start_step start_step: int = 20 "The timestep at which an agent begins performing it's actions." start_variance: int = 5 "Deviation around the start step." + + # TODO: This is available in config.agent_settings.start_settings.frequency frequency: int = 5 "The number of timesteps to wait between performing actions." + + # TODO: This is available in config.agent_settings.start_settings.variance variance: int = 0 "The amount the frequency can randomly change to." max_executions: int = 999999 "Maximum number of times the agent can execute its action." num_executions: int = 0 """Number of times the agent has executed an action.""" + #TODO: Also in abstract_tap - move up and inherit? Add to AgentStartSettings? next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index fed0f52d..00ba381b 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -63,6 +63,10 @@ agents: start_step: 5 frequency: 4 variance: 3 + action_probabilities: + 0: 0.6 + 1: 0.4 + diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index bd0fb61f..62579e35 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -60,7 +60,9 @@ agents: start_step: 5 frequency: 4 variance: 3 - + action_probabilities: + 0: 0.4 + 1: 0.6 - ref: defender diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 1a28258b..3e3d6e22 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -60,7 +60,9 @@ agents: start_step: 5 frequency: 4 variance: 3 - + action_probabilities: + 0: 0.4 + 1: 0.6 - ref: defender From 770896200b4dba0c6ae4a91f43d476465d5005bf Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 17 Dec 2024 12:47:54 +0000 Subject: [PATCH 096/224] #2869 - More YAML/test fixes to address failures --- src/primaite/game/agent/scripted_agents/interface.py | 5 ----- src/primaite/session/ray_envs.py | 10 +++++----- tests/assets/configs/basic_firewall.yaml | 3 +++ tests/assets/configs/dmz_network.yaml | 3 +++ tests/assets/configs/install_and_configure_apps.yaml | 3 +++ .../test_uc2_data_manipulation_scenario.py | 2 +- .../game_layer/observations/test_user_observations.py | 2 +- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents/interface.py b/src/primaite/game/agent/scripted_agents/interface.py index ab78eee0..e0dc61f2 100644 --- a/src/primaite/game/agent/scripted_agents/interface.py +++ b/src/primaite/game/agent/scripted_agents/interface.py @@ -260,11 +260,6 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False action_masking: bool = agent_settings.action_masking if agent_settings else False - # @property - # def most_recent_action(self) -> ActType: - # """Convenience method to access the agents most recent action.""" - # return self._most_recent_action - def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ Return the agent's most recent action, formatted in CAOS format. diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 2d540237..5d15ffa2 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -44,7 +44,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): ) for agent_name in self._agent_ids: agent = self.game.rl_agents[agent_name] - if agent.action_masking: + if agent.config.action_masking: self.observation_space[agent_name] = spaces.Dict( { "action_mask": spaces.MultiBinary(agent.action_manager.space.n), @@ -143,7 +143,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): unflat_space = agent.observation_manager.space unflat_obs = agent.observation_manager.current_observation obs = gymnasium.spaces.flatten(unflat_space, unflat_obs) - if agent.action_masking: + if agent.config.action_masking: all_obs[agent_name] = {"action_mask": self.game.action_mask(agent_name), "observations": obs} else: all_obs[agent_name] = obs @@ -168,7 +168,7 @@ class PrimaiteRayEnv(gymnasium.Env): self.env = PrimaiteGymEnv(env_config=env_config) # self.env.episode_counter -= 1 self.action_space = self.env.action_space - if self.env.agent.action_masking: + if self.env.agent.config.agent_settings.action_masking: self.observation_space = spaces.Dict( {"action_mask": spaces.MultiBinary(self.env.action_space.n), "observations": self.env.observation_space} ) @@ -178,7 +178,7 @@ class PrimaiteRayEnv(gymnasium.Env): def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" super().reset() # Ensure PRNG seed is set everywhere - if self.env.agent.action_masking: + if self.env.agent.config.action_masking: obs, *_ = self.env.reset(seed=seed) new_obs = {"action_mask": self.env.action_masks(), "observations": obs} return new_obs, *_ @@ -187,7 +187,7 @@ class PrimaiteRayEnv(gymnasium.Env): def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict]: """Perform a step in the environment.""" # if action masking is enabled, intercept the step method and add action mask to observation - if self.env.agent.action_masking: + if self.env.agent.config.action_masking: obs, *_ = self.env.step(action) new_obs = {"action_mask": self.game.action_mask(self.env._agent_name), "observations": obs} return new_obs, *_ diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 0253a4d2..e37a67da 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -60,6 +60,9 @@ agents: start_step: 5 frequency: 4 variance: 3 + action_probabilities: + 0: 0.4 + 1: 0.6 simulation: network: diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 52316260..d560efa3 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -85,6 +85,9 @@ agents: start_step: 5 frequency: 4 variance: 3 + action_probabilities: + 0: 0.4 + 1: 0.6 simulation: diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index 6b548f7e..18a9724b 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -92,6 +92,9 @@ agents: reward_function: reward_components: - type: DUMMY + agent_settings: + flatten_obs: True + action_masking: False simulation: network: diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 7ec38d72..1cf2ceea 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -49,7 +49,7 @@ def test_application_install_uninstall_on_uc2(): cfg = yaml.safe_load(f) env = PrimaiteGymEnv(env_config=cfg) - env.agent.flatten_obs = False + env.agent.config.flatten_obs = False env.reset() _, _, _, _, _ = env.step(0) diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index e7287eee..b7af3ec8 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -13,7 +13,7 @@ DATA_MANIPULATION_CONFIG = TEST_ASSETS_ROOT / "configs" / "data_manipulation.yam def env_with_ssh() -> PrimaiteGymEnv: """Build data manipulation environment with SSH port open on router.""" env = PrimaiteGymEnv(DATA_MANIPULATION_CONFIG) - env.agent.flatten_obs = False + env.agent.config.agent_settings.flatten_obs = False router: Router = env.game.simulation.network.get_node_by_hostname("router_1") router.acl.add_rule(ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=3) return env From dc6f2be20932f716fd70b64c40c97d2963be8f4d Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 17 Dec 2024 12:50:14 +0000 Subject: [PATCH 097/224] #2869 - pre-commit changes --- .../game/agent/scripted_agents/abstract_tap.py | 5 +++-- .../scripted_agents/data_manipulation_bot.py | 5 +++-- .../game/agent/scripted_agents/interface.py | 12 +++++------- .../agent/scripted_agents/probabilistic_agent.py | 7 +++++-- .../game/agent/scripted_agents/random_agent.py | 7 ++++--- tests/conftest.py | 9 ++++----- .../_game/_agent/test_probabilistic_agent.py | 16 +++++++++------- 7 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index d30ba9b1..725d3525 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -7,9 +7,10 @@ from typing import Dict, Optional, Tuple from gymnasium.core import ObsType -from primaite.game.agent.scripted_agents.interface import AbstractAgent, AbstractScriptedAgent +from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent + +__all__ = "AbstractTAPAgent" -__all__ = ("AbstractTAPAgent") class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): """Base class for TAP agents to inherit from.""" diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 594f1b41..d6213f67 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,11 +1,12 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Any, Dict, Optional, Tuple +from typing import Dict, Optional, Tuple from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent -__all__ = ("DataManipulationAgent") +__all__ = "DataManipulationAgent" + class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" diff --git a/src/primaite/game/agent/scripted_agents/interface.py b/src/primaite/game/agent/scripted_agents/interface.py index e0dc61f2..e6c2d6b3 100644 --- a/src/primaite/game/agent/scripted_agents/interface.py +++ b/src/primaite/game/agent/scripted_agents/interface.py @@ -17,11 +17,8 @@ from primaite.interface.request import RequestFormat, RequestResponse if TYPE_CHECKING: pass -__all__ = ("AgentHistoryItem", - "AgentStartSettings", - "AbstractAgent", - "AbstractScriptedAgent", - "ProxyAgent") +__all__ = ("AgentHistoryItem", "AgentStartSettings", "AbstractAgent", "AbstractScriptedAgent", "ProxyAgent") + class AgentHistoryItem(BaseModel): """One entry of an agent's action log - what the agent did and how the simulator responded in 1 step.""" @@ -121,7 +118,7 @@ class AbstractAgent(BaseModel): """ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - agent_name: ClassVar[str] = "Abstract_Agent" # TODO: Make this a ClassVar[str] like verb in actions? + agent_name: ClassVar[str] = "Abstract_Agent" # TODO: Make this a ClassVar[str] like verb in actions? history: List[AgentHistoryItem] = [] _logger: AgentLog = AgentLog(agent_name=agent_name) action_manager: ActionManager @@ -142,11 +139,12 @@ class AbstractAgent(BaseModel): @property def flatten_obs(self) -> bool: + """Return agent flatten_obs param.""" return self.config.agent_settings.flatten_obs @property def history(self) -> List[AgentHistoryItem]: - """Return the agent history""" + """Return the agent history.""" return self.config.history @property diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index ba6ba850..533f0628 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -8,7 +8,8 @@ from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent, AgentSettings -__all__ = ("ProbabilisticAgent") +__all__ = "ProbabilisticAgent" + class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" @@ -17,8 +18,11 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") rng: Any = np.random.default_rng(np.random.randint(0, 65535)) class AgentSettings(AgentSettings): + """ProbabilisticAgent settings.""" + action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" + @pydantic.field_validator("action_probabilities", mode="after") @classmethod def probabilities_sum_to_one(cls, v: Dict[int, float]) -> Dict[int, float]: @@ -44,7 +48,6 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") agent_name: str = "ProbabilisticAgent" agent_settings: "ProbabilisticAgent.AgentSettings" - @property def probabilities(self) -> Dict[str, int]: """Convenience method to view the probabilities of the Agent.""" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index fecc235f..fadaa66c 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -8,6 +8,7 @@ from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent __all__ = ("RandomAgent", "PeriodicAgent") + class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): """Agent that ignores its observation and acts completely at random.""" @@ -45,11 +46,11 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): "The timestep at which an agent begins performing it's actions." start_variance: int = 5 "Deviation around the start step." - + # TODO: This is available in config.agent_settings.start_settings.frequency frequency: int = 5 "The number of timesteps to wait between performing actions." - + # TODO: This is available in config.agent_settings.start_settings.variance variance: int = 0 "The amount the frequency can randomly change to." @@ -57,7 +58,7 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): "Maximum number of times the agent can execute its action." num_executions: int = 0 """Number of times the agent has executed an action.""" - #TODO: Also in abstract_tap - move up and inherit? Add to AgentStartSettings? + # TODO: Also in abstract_tap - move up and inherit? Add to AgentStartSettings? next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" diff --git a/tests/conftest.py b/tests/conftest.py index 68097830..319e306d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -497,12 +497,11 @@ def game_and_agent(): observation_space = ObservationManager(NestedObservation(components={})) reward_function = RewardFunction() - config = { - "agent_name":"test_agent", - "action_manager":action_space, - "observation_manager":observation_space, - "reward_function":reward_function, + "agent_name": "test_agent", + "action_manager": action_space, + "observation_manager": observation_space, + "reward_function": reward_function, } test_agent = ControlledAgent.from_config(config=config) diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 6e8c9c79..b6a49170 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -65,13 +65,15 @@ def test_probabilistic_agent(): # }, # ) - pa_config = {"agent_name":"test_agent", - "action_manager": action_space, - "observation_manager": observation_space, - "reward_function": reward_function, - "agent_settings": { - "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, - }} + pa_config = { + "agent_name": "test_agent", + "action_manager": action_space, + "observation_manager": observation_space, + "reward_function": reward_function, + "agent_settings": { + "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, + }, + } pa = ProbabilisticAgent.from_config(config=pa_config) From bf32271ddd0a9841126a001c4e43d5d16f91b6a6 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 2 Jan 2025 15:05:06 +0000 Subject: [PATCH 098/224] Change copyright to 2025 --- benchmark/benchmark.py | 2 +- benchmark/primaite_benchmark.py | 2 +- benchmark/report.py | 2 +- benchmark/utils.py | 2 +- docs/_templates/custom-class-template.rst | 2 +- docs/_templates/custom-module-template.rst | 2 +- docs/api.rst | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/source/action_masking.rst | 2 +- docs/source/config.rst | 2 +- docs/source/configuration/agents.rst | 2 +- docs/source/configuration/game.rst | 2 +- docs/source/configuration/io_settings.rst | 2 +- docs/source/configuration/simulation.rst | 2 +- .../simulation/nodes/common/common.rst | 2 +- .../common/common_host_node_attributes.rst | 2 +- .../common/common_network_node_attributes.rst | 2 +- .../nodes/common/common_node_attributes.rst | 2 +- .../nodes/common/node_type_list.rst | 2 +- .../simulation/nodes/computer.rst | 2 +- .../simulation/nodes/firewall.rst | 2 +- .../simulation/nodes/network_examples.rst | 2 +- .../configuration/simulation/nodes/router.rst | 2 +- .../configuration/simulation/nodes/server.rst | 2 +- .../configuration/simulation/nodes/switch.rst | 2 +- .../simulation/software/applications.rst | 2 +- .../simulation/software/services.rst | 2 +- docs/source/customising_scenarios.rst | 2 +- docs/source/dependencies.rst | 2 +- docs/source/developer_tools.rst | 2 +- docs/source/environment.rst | 2 +- docs/source/example_notebooks.rst | 2 +- docs/source/game_layer.rst | 2 +- docs/source/getting_started.rst | 2 +- docs/source/glossary.rst | 2 +- docs/source/node_sets.rst | 2 +- docs/source/notebooks/executed_notebooks.rst | 2 +- docs/source/primaite-dependencies.rst | 2 +- docs/source/request_system.rst | 2 +- docs/source/rewards.rst | 2 +- docs/source/simulation.rst | 2 +- .../network/airspace.rst | 2 +- .../network/base_hardware.rst | 2 +- .../simulation_components/network/network.rst | 2 +- .../network/network_interfaces.rst | 2 +- .../network/nodes/firewall.rst | 2 +- .../network/nodes/host_node.rst | 2 +- .../network/nodes/network_node.rst | 2 +- .../network/nodes/router.rst | 2 +- .../network/nodes/switch.rst | 2 +- .../network/nodes/wireless_router.rst | 2 +- .../network/transport_to_data_link_layer.rst | 2 +- .../system/applications/c2_suite.rst | 2 +- .../applications/data_manipulation_bot.rst | 2 +- .../system/applications/database_client.rst | 2 +- .../system/applications/dos_bot.rst | 2 +- .../system/applications/nmap.rst | 2 +- .../system/applications/ransomware_script.rst | 2 +- .../system/applications/web_browser.rst | 2 +- .../system/common/common_configuration.rst | 2 +- .../system/common/db_payload_list.rst | 2 +- .../system/internal_frame_processing.rst | 2 +- .../system/list_of_applications.rst | 2 +- .../system/list_of_services.rst | 2 +- .../system/list_of_system_applications.rst | 2 +- .../system/list_of_system_services.rst | 2 +- .../simulation_components/system/pcap.rst | 2 +- .../system/services/database_service.rst | 2 +- .../system/services/dns_client.rst | 2 +- .../system/services/dns_server.rst | 2 +- .../system/services/ftp_client.rst | 2 +- .../system/services/ftp_server.rst | 2 +- .../system/services/ntp_client.rst | 2 +- .../system/services/ntp_server.rst | 2 +- .../system/services/terminal.rst | 2 +- .../system/services/web_server.rst | 2 +- .../system/session_and_software_manager.rst | 2 +- .../simulation_components/system/software.rst | 2 +- .../simulation_components/system/sys_log.rst | 2 +- docs/source/simulation_structure.rst | 2 +- docs/source/state_system.rst | 2 +- docs/source/varying_config_files.rst | 2 +- src/primaite/__init__.py | 2 +- src/primaite/cli.py | 2 +- src/primaite/config/__init__.py | 2 +- src/primaite/config/load.py | 2 +- src/primaite/exceptions.py | 2 +- src/primaite/game/__init__.py | 2 +- src/primaite/game/agent/__init__.py | 2 +- src/primaite/game/agent/actions.py | 2 +- src/primaite/game/agent/agent_log.py | 2 +- src/primaite/game/agent/interface.py | 2 +- .../game/agent/observations/__init__.py | 2 +- .../agent/observations/acl_observation.py | 2 +- .../observations/file_system_observations.py | 260 +- .../observations/firewall_observation.py | 2 +- .../agent/observations/host_observations.py | 2 +- .../agent/observations/link_observation.py | 154 +- .../agent/observations/nic_observations.py | 2 +- .../agent/observations/node_observations.py | 2 +- .../agent/observations/observation_manager.py | 2 +- .../game/agent/observations/observations.py | 2 +- .../agent/observations/router_observation.py | 2 +- .../observations/software_observation.py | 165 +- src/primaite/game/agent/rewards.py | 2 +- .../game/agent/scripted_agents/__init__.py | 2 +- .../scripted_agents/data_manipulation_bot.py | 2 +- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 2 +- .../game/agent/scripted_agents/tap001.py | 2 +- src/primaite/game/agent/utils.py | 2 +- src/primaite/game/game.py | 2 +- src/primaite/game/science.py | 2 +- src/primaite/interface/__init__.py | 2 +- src/primaite/interface/request.py | 2 +- src/primaite/session/__init__.py | 2 +- src/primaite/session/environment.py | 2 +- src/primaite/session/episode_schedule.py | 2 +- src/primaite/session/io.py | 2 +- src/primaite/session/ray_envs.py | 2 +- src/primaite/setup/__init__.py | 2 +- src/primaite/setup/reset_demo_notebooks.py | 2 +- src/primaite/setup/reset_example_configs.py | 2 +- src/primaite/simulator/__init__.py | 2 +- src/primaite/simulator/core.py | 2 +- src/primaite/simulator/domain/__init__.py | 2 +- src/primaite/simulator/domain/account.py | 2 +- src/primaite/simulator/domain/controller.py | 2 +- .../simulator/file_system/__init__.py | 2 +- src/primaite/simulator/file_system/file.py | 2 +- .../simulator/file_system/file_system.py | 2 +- .../file_system/file_system_item_abc.py | 2 +- .../simulator/file_system/file_type.py | 2 +- src/primaite/simulator/file_system/folder.py | 2 +- src/primaite/simulator/network/__init__.py | 2 +- src/primaite/simulator/network/airspace.py | 2 +- src/primaite/simulator/network/container.py | 2 +- src/primaite/simulator/network/creation.py | 2 +- .../simulator/network/hardware/__init__.py | 2 +- .../simulator/network/hardware/base.py | 2230 +---------------- .../hardware/network_interface/__init__.py | 2 +- .../network_interface/wireless/__init__.py | 2 +- .../wireless/wireless_access_point.py | 2 +- .../wireless/wireless_nic.py | 2 +- .../network/hardware/node_operating_state.py | 2 +- .../network/hardware/nodes/__init__.py | 2 +- .../network/hardware/nodes/host/__init__.py | 2 +- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../network/hardware/nodes/host/server.py | 2 +- .../hardware/nodes/network/__init__.py | 2 +- .../hardware/nodes/network/firewall.py | 2 +- .../hardware/nodes/network/network_node.py | 2 +- .../network/hardware/nodes/network/router.py | 2 +- .../network/hardware/nodes/network/switch.py | 2 +- .../hardware/nodes/network/wireless_router.py | 2 +- src/primaite/simulator/network/networks.py | 2 +- src/primaite/simulator/network/nmne.py | 2 +- .../simulator/network/protocols/__init__.py | 2 +- .../simulator/network/protocols/arp.py | 2 +- .../simulator/network/protocols/dns.py | 2 +- .../simulator/network/protocols/ftp.py | 2 +- .../simulator/network/protocols/http.py | 2 +- .../simulator/network/protocols/icmp.py | 2 +- .../simulator/network/protocols/masquerade.py | 2 +- .../simulator/network/protocols/ntp.py | 2 +- .../simulator/network/protocols/packet.py | 2 +- .../simulator/network/protocols/ssh.py | 2 +- .../network/transmission/__init__.py | 2 +- .../network/transmission/data_link_layer.py | 2 +- .../network/transmission/network_layer.py | 2 +- .../network/transmission/primaite_layer.py | 2 +- .../network/transmission/transport_layer.py | 2 +- src/primaite/simulator/network/utils.py | 2 +- src/primaite/simulator/sim_container.py | 2 +- src/primaite/simulator/system/__init__.py | 2 +- .../simulator/system/applications/__init__.py | 2 +- .../system/applications/application.py | 2 +- .../system/applications/database_client.py | 2 +- .../simulator/system/applications/nmap.py | 2 +- .../applications/red_applications/__init__.py | 2 +- .../red_applications/c2/__init__.py | 2 +- .../red_applications/c2/abstract_c2.py | 2 +- .../red_applications/c2/c2_beacon.py | 2 +- .../red_applications/c2/c2_server.py | 2 +- .../red_applications/data_manipulation_bot.py | 2 +- .../applications/red_applications/dos_bot.py | 2 +- .../red_applications/ransomware_script.py | 2 +- .../system/applications/web_browser.py | 2 +- .../simulator/system/core/__init__.py | 2 +- .../simulator/system/core/packet_capture.py | 2 +- .../simulator/system/core/session_manager.py | 2 +- .../simulator/system/core/software_manager.py | 2 +- src/primaite/simulator/system/core/sys_log.py | 2 +- .../simulator/system/processes/__init__.py | 2 +- .../simulator/system/processes/process.py | 2 +- .../simulator/system/services/__init__.py | 2 +- .../system/services/access/__init__.py | 2 +- .../system/services/access/user_manager.py | 2 +- .../services/access/user_session_manager.py | 2 +- .../simulator/system/services/arp/__init__.py | 2 +- .../simulator/system/services/arp/arp.py | 2 +- .../system/services/database/__init__.py | 2 +- .../services/database/database_service.py | 2 +- .../simulator/system/services/dns/__init__.py | 2 +- .../system/services/dns/dns_client.py | 2 +- .../system/services/dns/dns_server.py | 2 +- .../simulator/system/services/ftp/__init__.py | 2 +- .../system/services/ftp/ftp_client.py | 339 +-- .../system/services/ftp/ftp_server.py | 2 +- .../system/services/ftp/ftp_service.py | 2 +- .../system/services/icmp/__init__.py | 2 +- .../simulator/system/services/icmp/icmp.py | 2 +- .../system/services/icmp/router_icmp.py | 2 +- .../simulator/system/services/ntp/__init__.py | 2 +- .../system/services/ntp/ntp_client.py | 2 +- .../system/services/ntp/ntp_server.py | 2 +- .../simulator/system/services/service.py | 2 +- .../system/services/terminal/__init__.py | 2 +- .../system/services/terminal/terminal.py | 546 +--- .../system/services/web_server/__init__.py | 2 +- .../system/services/web_server/web_server.py | 2 +- src/primaite/simulator/system/software.py | 2 +- src/primaite/utils/__init__.py | 2 +- src/primaite/utils/cli/__init__.py | 2 +- src/primaite/utils/cli/dev_cli.py | 2 +- .../utils/cli/primaite_config_utils.py | 2 +- src/primaite/utils/converters.py | 2 +- src/primaite/utils/package_data.py | 2 +- src/primaite/utils/session_metadata_parser.py | 2 +- src/primaite/utils/session_output_reader.py | 2 +- src/primaite/utils/session_output_writer.py | 2 +- src/primaite/utils/validation/__init__.py | 2 +- src/primaite/utils/validation/ip_protocol.py | 2 +- src/primaite/utils/validation/ipv4_address.py | 2 +- src/primaite/utils/validation/port.py | 2 +- tests/__init__.py | 2 +- tests/conftest.py | 2 +- tests/e2e_integration_tests/__init__.py | 2 +- .../action_masking/__init__.py | 2 +- .../test_agents_use_action_masks.py | 2 +- .../environments/__init__.py | 2 +- .../test_rllib_multi_agent_environment.py | 2 +- .../test_rllib_single_agent_environment.py | 2 +- .../environments/test_sb3_environment.py | 2 +- .../e2e_integration_tests/test_environment.py | 2 +- .../test_uc2_data_manipulation_scenario.py | 2 +- tests/integration_tests/__init__.py | 2 +- tests/integration_tests/cli/__init__.py | 2 +- tests/integration_tests/cli/test_dev_cli.py | 2 +- .../component_creation/__init__.py | 2 +- .../test_action_integration.py | 2 +- .../test_permission_system.py | 2 +- .../configuration_file_parsing/__init__.py | 2 +- .../nodes/__init__.py | 2 +- .../nodes/network/__init__.py | 2 +- .../nodes/network/test_firewall_config.py | 2 +- .../nodes/network/test_router_config.py | 2 +- .../nodes/test_node_config.py | 2 +- ...software_installation_and_configuration.py | 2 +- .../test_episode_scheduler.py | 2 +- .../test_game_options_config.py | 2 +- .../test_io_settings.py | 2 +- .../test_no_nodes_links_agents_config.py | 2 +- .../test_software_fix_duration.py | 2 +- .../applications/extended_application.py | 2 +- .../extensions/nodes/giga_switch.py | 2 +- .../extensions/nodes/super_computer.py | 2 +- .../extensions/services/extended_service.py | 2 +- .../extensions/test_extendable_config.py | 2 +- .../game_layer/actions/__init__.py | 2 +- .../test_application_request_permission.py | 2 +- .../actions/test_c2_suite_actions.py | 2 +- .../actions/test_configure_actions.py | 2 +- .../actions/test_file_request_permission.py | 2 +- .../actions/test_folder_request_permission.py | 2 +- .../actions/test_nic_request_permission.py | 2 +- .../actions/test_node_request_permission.py | 2 +- .../test_service_request_permission.py | 2 +- .../actions/test_terminal_actions.py | 2 +- .../game_layer/observations/__init__.py | 2 +- .../observations/test_acl_observations.py | 2 +- .../test_file_system_observations.py | 2 +- .../observations/test_firewall_observation.py | 2 +- .../observations/test_link_observations.py | 2 +- .../observations/test_nic_observations.py | 2 +- .../observations/test_node_observations.py | 2 +- .../observations/test_router_observation.py | 2 +- .../test_software_observations.py | 2 +- .../observations/test_user_observations.py | 2 +- .../game_layer/test_RNG_seed.py | 2 +- .../game_layer/test_action_mask.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_observations.py | 2 +- .../game_layer/test_rewards.py | 2 +- tests/integration_tests/network/__init__.py | 2 +- .../network/test_airspace_config.py | 2 +- ...ndwidth_load_checks_before_transmission.py | 2 +- .../network/test_broadcast.py | 2 +- .../network/test_capture_nmne.py | 2 +- .../network/test_firewall.py | 2 +- .../network/test_frame_transmission.py | 2 +- ...test_multi_lan_internet_example_network.py | 2 +- .../network/test_network_creation.py | 2 +- .../network/test_nic_link_connection.py | 2 +- .../integration_tests/network/test_routing.py | 2 +- .../network/test_switched_network.py | 2 +- .../test_users_creation_from_config.py | 2 +- .../network/test_wireless_router.py | 2 +- tests/integration_tests/system/__init__.py | 2 +- .../test_c2_suite_integration.py | 2 +- .../test_data_manipulation_bot_and_server.py | 2 +- .../test_dos_bot_and_server.py | 2 +- .../test_ransomware_script.py | 2 +- .../system/test_application_on_node.py | 110 +- tests/integration_tests/system/test_arp.py | 2 +- .../system/test_database_on_node.py | 2 +- .../system/test_dns_client_server.py | 2 +- .../system/test_ftp_client_server.py | 2 +- tests/integration_tests/system/test_nmap.py | 2 +- .../system/test_ntp_client_server.py | 2 +- .../system/test_service_listening_on_ports.py | 2 +- .../system/test_service_on_node.py | 2 +- .../test_user_session_manager_logins.py | 2 +- .../system/test_web_client_server.py | 2 +- .../test_web_client_server_and_database.py | 2 +- .../test_simulation/__init__.py | 2 +- .../test_simulation/test_request_response.py | 2 +- tests/mock_and_patch/__init__.py | 2 +- tests/mock_and_patch/get_session_path_mock.py | 2 +- tests/unit_tests/__init__.py | 2 +- tests/unit_tests/_primaite/__init__.py | 2 +- tests/unit_tests/_primaite/_game/__init__.py | 2 +- .../_primaite/_game/_agent/__init__.py | 2 +- .../_primaite/_game/_agent/test_actions.py | 2 +- .../_primaite/_game/_agent/test_agent_log.py | 2 +- .../_game/_agent/test_observations.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 2 +- .../_primaite/_interface/__init__.py | 2 +- .../_primaite/_interface/test_request.py | 2 +- .../unit_tests/_primaite/_session/__init__.py | 2 +- .../_session/test_episode_schedule.py | 2 +- .../_primaite/_simulator/__init__.py | 2 +- .../_primaite/_simulator/_domain/__init__.py | 2 +- .../_simulator/_domain/test_account.py | 2 +- .../_simulator/_domain/test_controller.py | 2 +- .../_simulator/_file_system/__init__.py | 2 +- .../_simulator/_file_system/test_file.py | 2 +- .../_file_system/test_file_actions.py | 2 +- .../_file_system/test_file_system.py | 2 +- .../_file_system/test_file_system_actions.py | 2 +- .../_simulator/_file_system/test_folder.py | 2 +- .../_file_system/test_folder_actions.py | 2 +- .../_primaite/_simulator/_network/__init__.py | 2 +- .../_simulator/_network/_hardware/__init__.py | 2 +- .../_network/_hardware/nodes/__init__.py | 2 +- .../_network/_hardware/nodes/test_acl.py | 2 +- .../_network/_hardware/nodes/test_router.py | 2 +- .../_network/_hardware/nodes/test_switch.py | 2 +- .../test_network_interface_actions.py | 2 +- .../_simulator/_network/_hardware/test_nic.py | 2 +- .../_network/_hardware/test_node_actions.py | 2 +- .../_network/_transmission/__init__.py | 2 +- .../_transmission/test_data_link_layer.py | 2 +- .../_transmission/test_network_layer.py | 2 +- .../_simulator/_network/test_container.py | 2 +- .../_simulator/_network/test_creation.py | 2 +- .../_simulator/_network/test_utils.py | 2 +- .../_primaite/_simulator/_system/__init__.py | 2 +- .../_system/_applications/__init__.py | 2 +- .../_red_applications/__init__.py | 2 +- .../_red_applications/test_c2_suite.py | 2 +- .../test_data_manipulation_bot.py | 2 +- .../_red_applications/test_dos_bot.py | 2 +- .../_applications/test_application_actions.py | 2 +- .../test_application_registry.py | 2 +- .../_applications/test_applications.py | 2 +- .../_applications/test_database_client.py | 2 +- .../_system/_applications/test_web_browser.py | 2 +- .../_simulator/_system/_services/__init__.py | 2 +- .../_system/_services/test_database.py | 2 +- .../_system/_services/test_dns_client.py | 2 +- .../_system/_services/test_dns_server.py | 2 +- .../_system/_services/test_ftp_client.py | 2 +- .../_system/_services/test_ftp_server.py | 2 +- .../_system/_services/test_service_actions.py | 2 +- .../_system/_services/test_services.py | 2 +- .../_system/_services/test_terminal.py | 2 +- .../_system/_services/test_web_server.py | 2 +- .../_simulator/_system/core/test_sys_log.py | 2 +- .../_simulator/_system/test_software.py | 2 +- .../_primaite/_simulator/test_core.py | 2 +- .../_simulator/test_sim_container.py | 2 +- tests/unit_tests/_primaite/_utils/__init__.py | 2 +- .../_primaite/_utils/_validation/__init__.py | 2 +- .../_utils/_validation/test_ip_protocol.py | 2 +- .../_primaite/_utils/_validation/test_port.py | 2 +- .../_utils/test_dict_enum_keys_conversion.py | 2 +- 400 files changed, 400 insertions(+), 4190 deletions(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 4ad398b9..ddedebb7 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, Optional, Tuple from gymnasium.core import ObsType diff --git a/benchmark/primaite_benchmark.py b/benchmark/primaite_benchmark.py index 86ed22a9..70ea8900 100644 --- a/benchmark/primaite_benchmark.py +++ b/benchmark/primaite_benchmark.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import shutil from datetime import datetime diff --git a/benchmark/report.py b/benchmark/report.py index 4035ceca..c11528ab 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import sys from datetime import datetime diff --git a/benchmark/utils.py b/benchmark/utils.py index 2e92d80d..f17c64b7 100644 --- a/benchmark/utils.py +++ b/benchmark/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import platform from typing import Dict diff --git a/docs/_templates/custom-class-template.rst b/docs/_templates/custom-class-template.rst index 920158d5..71e992bc 100644 --- a/docs/_templates/custom-class-template.rst +++ b/docs/_templates/custom-class-template.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. Credit to https://github.com/JamesALeedham/Sphinx-Autosummary-Recursion for the custom templates. diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst index 98627e43..3a2ced35 100644 --- a/docs/_templates/custom-module-template.rst +++ b/docs/_templates/custom-module-template.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. Credit to https://github.com/JamesALeedham/Sphinx-Autosummary-Recursion for the custom templates. diff --git a/docs/api.rst b/docs/api.rst index 977f9e87..eb7e4719 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,7 +2,7 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. DO NOT DELETE THIS FILE! It contains the all-important `.. autosummary::` directive with `:recursive:` option, without diff --git a/docs/conf.py b/docs/conf.py index 318829fd..60739499 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: diff --git a/docs/index.rst b/docs/index.rst index 2ba43162..42cc1d6d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Welcome to PrimAITE's documentation ==================================== diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index 264ab254..dad6a484 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Action Masking ************** diff --git a/docs/source/config.rst b/docs/source/config.rst index eb0b9906..0fa4a4d5 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK PrimAITE |VERSION| Configuration ******************************** diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index dece94c5..d11f7892 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``agents`` diff --git a/docs/source/configuration/game.rst b/docs/source/configuration/game.rst index 2048708c..b3c139b2 100644 --- a/docs/source/configuration/game.rst +++ b/docs/source/configuration/game.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``game`` diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index 1c9585c9..ab3a978e 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``io_settings`` diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index fa1d774a..0b2067d8 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``simulation`` diff --git a/docs/source/configuration/simulation/nodes/common/common.rst b/docs/source/configuration/simulation/nodes/common/common.rst index a0f2eb13..c45eccf6 100644 --- a/docs/source/configuration/simulation/nodes/common/common.rst +++ b/docs/source/configuration/simulation/nodes/common/common.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Node Attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst index bb3b2a52..b717340e 100644 --- a/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _common_host_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst index d556e2dc..035c7e55 100644 --- a/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _common_network_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst index 6a95911f..542b817b 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _common_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/node_type_list.rst b/docs/source/configuration/simulation/nodes/common/node_type_list.rst index 1ec496d9..21181019 100644 --- a/docs/source/configuration/simulation/nodes/common/node_type_list.rst +++ b/docs/source/configuration/simulation/nodes/common/node_type_list.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``type`` -------- diff --git a/docs/source/configuration/simulation/nodes/computer.rst b/docs/source/configuration/simulation/nodes/computer.rst index 32e0b2b9..456d11a2 100644 --- a/docs/source/configuration/simulation/nodes/computer.rst +++ b/docs/source/configuration/simulation/nodes/computer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _computer_configuration: diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst index 775ffabd..84b5c99e 100644 --- a/docs/source/configuration/simulation/nodes/firewall.rst +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _firewall_configuration: diff --git a/docs/source/configuration/simulation/nodes/network_examples.rst b/docs/source/configuration/simulation/nodes/network_examples.rst index 2a34a206..80e934e5 100644 --- a/docs/source/configuration/simulation/nodes/network_examples.rst +++ b/docs/source/configuration/simulation/nodes/network_examples.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _network_examples: diff --git a/docs/source/configuration/simulation/nodes/router.rst b/docs/source/configuration/simulation/nodes/router.rst index ac9d6411..4b41784c 100644 --- a/docs/source/configuration/simulation/nodes/router.rst +++ b/docs/source/configuration/simulation/nodes/router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _router_configuration: diff --git a/docs/source/configuration/simulation/nodes/server.rst b/docs/source/configuration/simulation/nodes/server.rst index 92b33ca7..616efb38 100644 --- a/docs/source/configuration/simulation/nodes/server.rst +++ b/docs/source/configuration/simulation/nodes/server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _server_configuration: diff --git a/docs/source/configuration/simulation/nodes/switch.rst b/docs/source/configuration/simulation/nodes/switch.rst index 17cf76f9..d09f5ba7 100644 --- a/docs/source/configuration/simulation/nodes/switch.rst +++ b/docs/source/configuration/simulation/nodes/switch.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _switch_configuration: diff --git a/docs/source/configuration/simulation/software/applications.rst b/docs/source/configuration/simulation/software/applications.rst index 8c590d53..9973a167 100644 --- a/docs/source/configuration/simulation/software/applications.rst +++ b/docs/source/configuration/simulation/software/applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``applications`` ---------------- diff --git a/docs/source/configuration/simulation/software/services.rst b/docs/source/configuration/simulation/software/services.rst index fafdf2e8..ec6bbba9 100644 --- a/docs/source/configuration/simulation/software/services.rst +++ b/docs/source/configuration/simulation/software/services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``services`` ------------ diff --git a/docs/source/customising_scenarios.rst b/docs/source/customising_scenarios.rst index 092f306b..df7d4b1e 100644 --- a/docs/source/customising_scenarios.rst +++ b/docs/source/customising_scenarios.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Customising Agents ****************** diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst index 74f3cd14..e8be00d3 100644 --- a/docs/source/dependencies.rst +++ b/docs/source/dependencies.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. role:: raw-html(raw) :format: html diff --git a/docs/source/developer_tools.rst b/docs/source/developer_tools.rst index a66b7902..b3d81a27 100644 --- a/docs/source/developer_tools.rst +++ b/docs/source/developer_tools.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Developer Tools: diff --git a/docs/source/environment.rst b/docs/source/environment.rst index a282c09e..251b1090 100644 --- a/docs/source/environment.rst +++ b/docs/source/environment.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK RL Environments *************** diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index 920175c9..6caeae3d 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _example jupyter notebooks: diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 775c02b5..58a274d9 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK PrimAITE Game layer ******************* diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index ded92c60..427d1823 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _getting-started: diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 8fff0ea3..02c578d1 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Glossary ============= diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst index 866f0139..3c247478 100644 --- a/docs/source/node_sets.rst +++ b/docs/source/node_sets.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _network_node_adder: diff --git a/docs/source/notebooks/executed_notebooks.rst b/docs/source/notebooks/executed_notebooks.rst index 3431d344..f4acfad6 100644 --- a/docs/source/notebooks/executed_notebooks.rst +++ b/docs/source/notebooks/executed_notebooks.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Executed Notebooks: diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst index 8367ee61..14a96349 100644 --- a/docs/source/primaite-dependencies.rst +++ b/docs/source/primaite-dependencies.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | Name | Version | License | Description | URL | diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index f2d2e68d..b89d0906 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Request System ************** diff --git a/docs/source/rewards.rst b/docs/source/rewards.rst index 0163284c..254237ee 100644 --- a/docs/source/rewards.rst +++ b/docs/source/rewards.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Rewards ####### diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index cc723e40..95807703 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Simulation diff --git a/docs/source/simulation_components/network/airspace.rst b/docs/source/simulation_components/network/airspace.rst index 06a884a7..a6967b91 100644 --- a/docs/source/simulation_components/network/airspace.rst +++ b/docs/source/simulation_components/network/airspace.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _airspace: diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index ce1e5c74..8b325ffc 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ############# Base Hardware diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index 4cc121a3..152b74b8 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _network: diff --git a/docs/source/simulation_components/network/network_interfaces.rst b/docs/source/simulation_components/network/network_interfaces.rst index c6b97a8e..663af7ba 100644 --- a/docs/source/simulation_components/network/network_interfaces.rst +++ b/docs/source/simulation_components/network/network_interfaces.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ################################# Network Interface Hierarchy Model diff --git a/docs/source/simulation_components/network/nodes/firewall.rst b/docs/source/simulation_components/network/nodes/firewall.rst index 1ef16d63..f2d7e61a 100644 --- a/docs/source/simulation_components/network/nodes/firewall.rst +++ b/docs/source/simulation_components/network/nodes/firewall.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ######## Firewall diff --git a/docs/source/simulation_components/network/nodes/host_node.rst b/docs/source/simulation_components/network/nodes/host_node.rst index b8aae098..2c1e75d0 100644 --- a/docs/source/simulation_components/network/nodes/host_node.rst +++ b/docs/source/simulation_components/network/nodes/host_node.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ######### diff --git a/docs/source/simulation_components/network/nodes/network_node.rst b/docs/source/simulation_components/network/nodes/network_node.rst index e1fa976c..4aebe09f 100644 --- a/docs/source/simulation_components/network/nodes/network_node.rst +++ b/docs/source/simulation_components/network/nodes/network_node.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ############ Network Node diff --git a/docs/source/simulation_components/network/nodes/router.rst b/docs/source/simulation_components/network/nodes/router.rst index 5d3de60f..fb582b23 100644 --- a/docs/source/simulation_components/network/nodes/router.rst +++ b/docs/source/simulation_components/network/nodes/router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ###### Router diff --git a/docs/source/simulation_components/network/nodes/switch.rst b/docs/source/simulation_components/network/nodes/switch.rst index 0ecbcbf3..e7143f0c 100644 --- a/docs/source/simulation_components/network/nodes/switch.rst +++ b/docs/source/simulation_components/network/nodes/switch.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ###### Switch diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index c0c245b2..d7207846 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ###### Wireless Router diff --git a/docs/source/simulation_components/network/transport_to_data_link_layer.rst b/docs/source/simulation_components/network/transport_to_data_link_layer.rst index 02bfdcdc..54118c90 100644 --- a/docs/source/simulation_components/network/transport_to_data_link_layer.rst +++ b/docs/source/simulation_components/network/transport_to_data_link_layer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Transport Layer to Data Link Layer ================================== diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index d045949a..3dd2b4fc 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _C2_Suite: diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 1a387514..91c33ede 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DataManipulationBot: diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 1fea78ab..75a396b5 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DatabaseClient: diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index 6ad45424..5c0ae86a 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DoSBot: diff --git a/docs/source/simulation_components/system/applications/nmap.rst b/docs/source/simulation_components/system/applications/nmap.rst index a5615a43..a82735c8 100644 --- a/docs/source/simulation_components/system/applications/nmap.rst +++ b/docs/source/simulation_components/system/applications/nmap.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _NMAP: diff --git a/docs/source/simulation_components/system/applications/ransomware_script.rst b/docs/source/simulation_components/system/applications/ransomware_script.rst index 5bff6991..b79ca802 100644 --- a/docs/source/simulation_components/system/applications/ransomware_script.rst +++ b/docs/source/simulation_components/system/applications/ransomware_script.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _RansomwareScript: diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index c56c450d..7062887b 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _WebBrowser: diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index c53ac8b8..411fd529 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Common Configuration: diff --git a/docs/source/simulation_components/system/common/db_payload_list.rst b/docs/source/simulation_components/system/common/db_payload_list.rst index 0930f09d..89668665 100644 --- a/docs/source/simulation_components/system/common/db_payload_list.rst +++ b/docs/source/simulation_components/system/common/db_payload_list.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Database Payload List: diff --git a/docs/source/simulation_components/system/internal_frame_processing.rst b/docs/source/simulation_components/system/internal_frame_processing.rst index 65336f9b..f82dec13 100644 --- a/docs/source/simulation_components/system/internal_frame_processing.rst +++ b/docs/source/simulation_components/system/internal_frame_processing.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _internal_frame_processing: diff --git a/docs/source/simulation_components/system/list_of_applications.rst b/docs/source/simulation_components/system/list_of_applications.rst index 94090d93..a7e05ea6 100644 --- a/docs/source/simulation_components/system/list_of_applications.rst +++ b/docs/source/simulation_components/system/list_of_applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. toctree:: :maxdepth: 1 diff --git a/docs/source/simulation_components/system/list_of_services.rst b/docs/source/simulation_components/system/list_of_services.rst index b6995647..2082ac6f 100644 --- a/docs/source/simulation_components/system/list_of_services.rst +++ b/docs/source/simulation_components/system/list_of_services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. toctree:: :maxdepth: 1 diff --git a/docs/source/simulation_components/system/list_of_system_applications.rst b/docs/source/simulation_components/system/list_of_system_applications.rst index c8807ef0..0c66662f 100644 --- a/docs/source/simulation_components/system/list_of_system_applications.rst +++ b/docs/source/simulation_components/system/list_of_system_applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``system applications`` """"""""""""""""""""""" diff --git a/docs/source/simulation_components/system/list_of_system_services.rst b/docs/source/simulation_components/system/list_of_system_services.rst index 9b5c3265..01df4dc8 100644 --- a/docs/source/simulation_components/system/list_of_system_services.rst +++ b/docs/source/simulation_components/system/list_of_system_services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``system services`` """"""""""""""""""" diff --git a/docs/source/simulation_components/system/pcap.rst b/docs/source/simulation_components/system/pcap.rst index 830c28bd..0da28a39 100644 --- a/docs/source/simulation_components/system/pcap.rst +++ b/docs/source/simulation_components/system/pcap.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK PCAP ==== diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index f3e800cd..b41c1097 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DatabaseService: diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index eca152f0..6475b4d4 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DNSClient: diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index 1e30b9bd..3d699048 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DNSServer: diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index f9c7b4ce..47566e5f 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _FTPClient: diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index f52fa043..e4cada29 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _FTPServer: diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index 7af831bf..fb965029 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _NTPClient: diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index a09c8bdd..68fadca9 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _NTPServer: diff --git a/docs/source/simulation_components/system/services/terminal.rst b/docs/source/simulation_components/system/services/terminal.rst index 6909786e..bc5cee48 100644 --- a/docs/source/simulation_components/system/services/terminal.rst +++ b/docs/source/simulation_components/system/services/terminal.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Terminal: diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index cec20a60..011aa00f 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _WebServer: diff --git a/docs/source/simulation_components/system/session_and_software_manager.rst b/docs/source/simulation_components/system/session_and_software_manager.rst index 230f6687..f20af556 100644 --- a/docs/source/simulation_components/system/session_and_software_manager.rst +++ b/docs/source/simulation_components/system/session_and_software_manager.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Session and Software Manager ============================ diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index c8f0e2d3..d28815bb 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _software: diff --git a/docs/source/simulation_components/system/sys_log.rst b/docs/source/simulation_components/system/sys_log.rst index cdf19faa..05629993 100644 --- a/docs/source/simulation_components/system/sys_log.rst +++ b/docs/source/simulation_components/system/sys_log.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK SysLog ====== diff --git a/docs/source/simulation_structure.rst b/docs/source/simulation_structure.rst index cd9ac409..7debe112 100644 --- a/docs/source/simulation_structure.rst +++ b/docs/source/simulation_structure.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Simulation Structure diff --git a/docs/source/state_system.rst b/docs/source/state_system.rst index e31474ea..a5fd1df1 100644 --- a/docs/source/state_system.rst +++ b/docs/source/state_system.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Simulation State ================ diff --git a/docs/source/varying_config_files.rst b/docs/source/varying_config_files.rst index fa66f0d9..942e522b 100644 --- a/docs/source/varying_config_files.rst +++ b/docs/source/varying_config_files.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Defining variations in the config files ======================================= diff --git a/src/primaite/__init__.py b/src/primaite/__init__.py index 8dd84428..54eac69d 100644 --- a/src/primaite/__init__.py +++ b/src/primaite/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import datetime as datetime import logging import logging.config diff --git a/src/primaite/cli.py b/src/primaite/cli.py index 4fbbdec9..2bd18baf 100644 --- a/src/primaite/cli.py +++ b/src/primaite/cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Provides a CLI using Typer as an entry point.""" import logging import os diff --git a/src/primaite/config/__init__.py b/src/primaite/config/__init__.py index c2ae1b5b..7b5e2889 100644 --- a/src/primaite/config/__init__.py +++ b/src/primaite/config/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Configuration parameters for running experiments.""" diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index 39040d76..3553f527 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Dict, Final, Union diff --git a/src/primaite/exceptions.py b/src/primaite/exceptions.py index afc55271..4487111d 100644 --- a/src/primaite/exceptions.py +++ b/src/primaite/exceptions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK class PrimaiteError(Exception): """The root PrimAITE Error.""" diff --git a/src/primaite/game/__init__.py b/src/primaite/game/__init__.py index 39034e92..57f96a56 100644 --- a/src/primaite/game/__init__.py +++ b/src/primaite/game/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """PrimAITE Game Layer.""" diff --git a/src/primaite/game/agent/__init__.py b/src/primaite/game/agent/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/game/agent/__init__.py +++ b/src/primaite/game/agent/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 2e6189c0..5ec122ec 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """ This module contains the ActionManager class which belongs to the Agent class. diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 62ef4884..59fb4702 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import logging from pathlib import Path diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 14b97821..4acc9108 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Interface for agents.""" from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING diff --git a/src/primaite/game/agent/observations/__init__.py b/src/primaite/game/agent/observations/__init__.py index 6c88f844..68435eed 100644 --- a/src/primaite/game/agent/observations/__init__.py +++ b/src/primaite/game/agent/observations/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa # Pre-import all the observations when we load up the observations module so that they can be resolved by the parser. from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index 41af5a8f..86a6463a 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 1c73d026..836b79af 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -1,259 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -from typing import Dict, Iterable, List, Optional - -from gymnasium import spaces -from gymnasium.core import ObsType - -from primaite import getLogger -from primaite.game.agent.observations.observations import AbstractObservation, WhereType -from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE - -_LOGGER = getLogger(__name__) - - -class FileObservation(AbstractObservation, identifier="FILE"): - """File observation, provides status information about a file within the simulation environment.""" - - class ConfigSchema(AbstractObservation.ConfigSchema): - """Configuration schema for FileObservation.""" - - file_name: str - """Name of the file, used for querying simulation state dictionary.""" - include_num_access: Optional[bool] = None - """Whether to include the number of accesses to the file in the observation.""" - file_system_requires_scan: Optional[bool] = None - """If True, the file must be scanned to update the health state. Tf False, the true state is always shown.""" - - def __init__(self, where: WhereType, include_num_access: bool, file_system_requires_scan: bool) -> None: - """ - Initialise a file observation instance. - - :param where: Where in the simulation state dictionary to find the relevant information for this file. - A typical location for a file might be - ['network', 'nodes', , 'file_system', 'folder', , 'files', ]. - :type where: WhereType - :param include_num_access: Whether to include the number of accesses to the file in the observation. - :type include_num_access: bool - :param file_system_requires_scan: If True, the file must be scanned to update the health state. Tf False, - the true state is always shown. - :type file_system_requires_scan: bool - """ - self.where: WhereType = where - self.include_num_access: bool = include_num_access - self.file_system_requires_scan: bool = file_system_requires_scan - - self.default_observation: ObsType = {"health_status": 0} - if self.include_num_access: - self.default_observation["num_access"] = 0 - - # TODO: allow these to be configured in yaml - self.high_threshold = 10 - self.med_threshold = 5 - self.low_threshold = 0 - - def _categorise_num_access(self, num_access: int) -> int: - """ - Represent number of file accesses as a categorical variable. - - :param num_access: Number of file accesses. - :return: Bin number corresponding to the number of accesses. - """ - if num_access > self.high_threshold: - return 3 - elif num_access > self.med_threshold: - return 2 - elif num_access > self.low_threshold: - return 1 - return 0 - - def observe(self, state: Dict) -> ObsType: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary. - :type state: Dict - :return: Observation containing the health status of the file and optionally the number of accesses. - :rtype: ObsType - """ - file_state = access_from_nested_dict(state, self.where) - if file_state is NOT_PRESENT_IN_STATE: - return self.default_observation - if self.file_system_requires_scan: - health_status = file_state["visible_status"] - else: - health_status = file_state["health_status"] - obs = {"health_status": health_status} - if self.include_num_access: - obs["num_access"] = self._categorise_num_access(file_state["num_access"]) - return obs - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Gymnasium space representing the observation space for file status. - :rtype: spaces.Space - """ - space = {"health_status": spaces.Discrete(6)} - if self.include_num_access: - space["num_access"] = spaces.Discrete(4) - return spaces.Dict(space) - - @classmethod - def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FileObservation: - """ - Create a file observation from a configuration schema. - - :param config: Configuration schema containing the necessary information for the file observation. - :type config: ConfigSchema - :param parent_where: Where in the simulation state dictionary to find the information about this file's - parent node. A typical location for a node might be ['network', 'nodes', ]. - :type parent_where: WhereType, optional - :return: Constructed file observation instance. - :rtype: FileObservation - :param file_system_requires_scan: If True, the folder must be scanned to update the health state. Tf False, - the true state is always shown. - :type file_system_requires_scan: bool - """ - return cls( - where=parent_where + ["files", config.file_name], - include_num_access=config.include_num_access, - file_system_requires_scan=config.file_system_requires_scan, - ) - - -class FolderObservation(AbstractObservation, identifier="FOLDER"): - """Folder observation, provides status information about a folder within the simulation environment.""" - - class ConfigSchema(AbstractObservation.ConfigSchema): - """Configuration schema for FolderObservation.""" - - folder_name: str - """Name of the folder, used for querying simulation state dictionary.""" - files: List[FileObservation.ConfigSchema] = [] - """List of file configurations within the folder.""" - num_files: Optional[int] = None - """Number of spaces for file observations in this folder.""" - include_num_access: Optional[bool] = None - """Whether files in this folder should include the number of accesses in their observation.""" - file_system_requires_scan: Optional[bool] = None - """If True, the folder must be scanned to update the health state. Tf False, the true state is always shown.""" - - def __init__( - self, - where: WhereType, - files: Iterable[FileObservation], - num_files: int, - include_num_access: bool, - file_system_requires_scan: bool, - ) -> None: - """ - Initialise a folder observation instance. - - :param where: Where in the simulation state dictionary to find the relevant information for this folder. - A typical location for a folder might be ['network', 'nodes', , 'folders', ]. - :type where: WhereType - :param files: List of file observation instances within the folder. - :type files: Iterable[FileObservation] - :param num_files: Number of files expected in the folder. - :type num_files: int - :param include_num_access: Whether to include the number of accesses to files in the observation. - :type include_num_access: bool - :param file_system_requires_scan: If True, the folder must be scanned to update the health state. Tf False, - the true state is always shown. - :type file_system_requires_scan: bool - """ - self.where: WhereType = where - - self.file_system_requires_scan: bool = file_system_requires_scan - - self.files: List[FileObservation] = files - while len(self.files) < num_files: - self.files.append( - FileObservation( - where=None, - include_num_access=include_num_access, - file_system_requires_scan=self.file_system_requires_scan, - ) - ) - while len(self.files) > num_files: - truncated_file = self.files.pop() - msg = f"Too many files in folder observation. Truncating file {truncated_file}" - _LOGGER.warning(msg) - - self.default_observation = { - "health_status": 0, - } - if self.files: - self.default_observation["FILES"] = {i + 1: f.default_observation for i, f in enumerate(self.files)} - - def observe(self, state: Dict) -> ObsType: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary. - :type state: Dict - :return: Observation containing the health status of the folder and status of files within the folder. - :rtype: ObsType - """ - folder_state = access_from_nested_dict(state, self.where) - if folder_state is NOT_PRESENT_IN_STATE: - return self.default_observation - - if self.file_system_requires_scan: - health_status = folder_state["visible_status"] - else: - health_status = folder_state["health_status"] - - obs = {} - - obs["health_status"] = health_status - if self.files: - obs["FILES"] = {i + 1: file.observe(state) for i, file in enumerate(self.files)} - - return obs - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Gymnasium space representing the observation space for folder status. - :rtype: spaces.Space - """ - shape = {"health_status": spaces.Discrete(6)} - if self.files: - shape["FILES"] = spaces.Dict({i + 1: f.space for i, f in enumerate(self.files)}) - return spaces.Dict(shape) - - @classmethod - def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FolderObservation: - """ - Create a folder observation from a configuration schema. - - :param config: Configuration schema containing the necessary information for the folder observation. - :type config: ConfigSchema - :param parent_where: Where in the simulation state dictionary to find the information about this folder's - parent node. A typical location for a node might be ['network', 'nodes', ]. - :type parent_where: WhereType, optional - :return: Constructed folder observation instance. - :rtype: FolderObservation - """ - where = parent_where + ["file_system", "folders", config.folder_name] - - # pass down shared/common config items - for file_config in config.files: - file_config.include_num_access = config.include_num_access - file_config.file_system_requires_scan = config.file_system_requires_scan - - files = [FileObservation.from_config(config=f, parent_where=where) for f in config.files] - return cls( - where=where, - files=files, - num_files=config.num_files, - include_num_access=config.include_num_access, - file_system_requires_scan=config.file_system_requires_scan, - ) +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index 42ceaff0..a89ddfc5 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 617e8eee..03e9aca1 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/link_observation.py b/src/primaite/game/agent/observations/link_observation.py index 9af39a22..836b79af 100644 --- a/src/primaite/game/agent/observations/link_observation.py +++ b/src/primaite/game/agent/observations/link_observation.py @@ -1,153 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -from typing import Any, Dict, List - -from gymnasium import spaces -from gymnasium.core import ObsType - -from primaite import getLogger -from primaite.game.agent.observations.observations import AbstractObservation, WhereType -from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE - -_LOGGER = getLogger(__name__) - - -class LinkObservation(AbstractObservation, identifier="LINK"): - """Link observation, providing information about a specific link within the simulation environment.""" - - class ConfigSchema(AbstractObservation.ConfigSchema): - """Configuration schema for LinkObservation.""" - - link_reference: str - """Reference identifier for the link.""" - - def __init__(self, where: WhereType) -> None: - """ - Initialise a link observation instance. - - :param where: Where in the simulation state dictionary to find the relevant information for this link. - A typical location for a link might be ['network', 'links', ]. - :type where: WhereType - """ - self.where = where - self.default_observation: ObsType = {"PROTOCOLS": {"ALL": 0}} - - def observe(self, state: Dict) -> Any: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary. - :type state: Dict - :return: Observation containing information about the link. - :rtype: Any - """ - link_state = access_from_nested_dict(state, self.where) - if link_state is NOT_PRESENT_IN_STATE: - self.where[-1] = "<->".join(self.where[-1].split("<->")[::-1]) # try swapping endpoint A and B - link_state = access_from_nested_dict(state, self.where) - if link_state is NOT_PRESENT_IN_STATE: - return self.default_observation - - bandwidth = link_state["bandwidth"] - load = link_state["current_load"] - if load == 0: - utilisation_category = 0 - else: - utilisation_fraction = load / bandwidth - utilisation_category = int(utilisation_fraction * 9) + 1 - - return {"PROTOCOLS": {"ALL": min(utilisation_category, 10)}} - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Gymnasium space representing the observation space for link status. - :rtype: spaces.Space - """ - return spaces.Dict({"PROTOCOLS": spaces.Dict({"ALL": spaces.Discrete(11)})}) - - @classmethod - def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> LinkObservation: - """ - Create a link observation from a configuration schema. - - :param config: Configuration schema containing the necessary information for the link observation. - :type config: ConfigSchema - :param parent_where: Where in the simulation state dictionary to find the information about this link. - A typical location might be ['network', 'links', ]. - :type parent_where: WhereType, optional - :return: Constructed link observation instance. - :rtype: LinkObservation - """ - link_reference = config.link_reference - if parent_where == []: - where = ["network", "links", link_reference] - else: - where = parent_where + ["links", link_reference] - return cls(where=where) - - -class LinksObservation(AbstractObservation, identifier="LINKS"): - """Collection of link observations representing multiple links within the simulation environment.""" - - class ConfigSchema(AbstractObservation.ConfigSchema): - """Configuration schema for LinksObservation.""" - - link_references: List[str] - """List of reference identifiers for the links.""" - - def __init__(self, where: WhereType, links: List[LinkObservation]) -> None: - """ - Initialise a links observation instance. - - :param where: Where in the simulation state dictionary to find the relevant information for these links. - A typical location for links might be ['network', 'links']. - :type where: WhereType - :param links: List of link observations. - :type links: List[LinkObservation] - """ - self.where: WhereType = where - self.links: List[LinkObservation] = links - self.default_observation: ObsType = {i + 1: l.default_observation for i, l in enumerate(self.links)} - - def observe(self, state: Dict) -> ObsType: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary. - :type state: Dict - :return: Observation containing information about multiple links. - :rtype: ObsType - """ - return {i + 1: l.observe(state) for i, l in enumerate(self.links)} - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Gymnasium space representing the observation space for multiple links. - :rtype: spaces.Space - """ - return spaces.Dict({i + 1: l.space for i, l in enumerate(self.links)}) - - @classmethod - def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> LinksObservation: - """ - Create a links observation from a configuration schema. - - :param config: Configuration schema containing the necessary information for the links observation. - :type config: ConfigSchema - :param parent_where: Where in the simulation state dictionary to find the information about these links. - A typical location might be ['network']. - :type parent_where: WhereType, optional - :return: Constructed links observation instance. - :rtype: LinksObservation - """ - where = parent_where + ["network"] - link_cfgs = [LinkObservation.ConfigSchema(link_reference=ref) for ref in config.link_references] - links = [LinkObservation.from_config(c, parent_where=where) for c in link_cfgs] - return cls(where=where, links=links) +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index d180b641..f87d2d76 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index e11521b6..03869367 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 9b20fdcb..71a60433 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Any, Dict, List, Optional diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index a9663c56..49b9ab72 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Manages the observation space for the agent.""" from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, Optional, Type, Union diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index d064936a..ca455f4c 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 15cd2447..836b79af 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -1,164 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -from typing import Dict - -from gymnasium import spaces -from gymnasium.core import ObsType - -from primaite.game.agent.observations.observations import AbstractObservation, WhereType -from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE - - -class ServiceObservation(AbstractObservation, identifier="SERVICE"): - """Service observation, shows status of a service in the simulation environment.""" - - class ConfigSchema(AbstractObservation.ConfigSchema): - """Configuration schema for ServiceObservation.""" - - service_name: str - """Name of the service, used for querying simulation state dictionary""" - - def __init__(self, where: WhereType) -> None: - """ - Initialise a service observation instance. - - :param where: Where in the simulation state dictionary to find the relevant information for this service. - A typical location for a service might be ['network', 'nodes', , 'services', ]. - :type where: WhereType - """ - self.where = where - self.default_observation = {"operating_status": 0, "health_status": 0} - - def observe(self, state: Dict) -> ObsType: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary. - :type state: Dict - :return: Observation containing the operating status and health status of the service. - :rtype: ObsType - """ - service_state = access_from_nested_dict(state, self.where) - if service_state is NOT_PRESENT_IN_STATE: - return self.default_observation - return { - "operating_status": service_state["operating_state"], - "health_status": service_state["health_state_visible"], - } - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Gymnasium space representing the observation space for service status. - :rtype: spaces.Space - """ - return spaces.Dict({"operating_status": spaces.Discrete(7), "health_status": spaces.Discrete(5)}) - - @classmethod - def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> ServiceObservation: - """ - Create a service observation from a configuration schema. - - :param config: Configuration schema containing the necessary information for the service observation. - :type config: ConfigSchema - :param parent_where: Where in the simulation state dictionary to find the information about this service's - parent node. A typical location for a node might be ['network', 'nodes', ]. - :type parent_where: WhereType, optional - :return: Constructed service observation instance. - :rtype: ServiceObservation - """ - return cls(where=parent_where + ["services", config.service_name]) - - -class ApplicationObservation(AbstractObservation, identifier="APPLICATION"): - """Application observation, shows the status of an application within the simulation environment.""" - - class ConfigSchema(AbstractObservation.ConfigSchema): - """Configuration schema for ApplicationObservation.""" - - application_name: str - """Name of the application, used for querying simulation state dictionary""" - - def __init__(self, where: WhereType) -> None: - """ - Initialise an application observation instance. - - :param where: Where in the simulation state dictionary to find the relevant information for this application. - A typical location for an application might be - ['network', 'nodes', , 'applications', ]. - :type where: WhereType - """ - self.where = where - self.default_observation = {"operating_status": 0, "health_status": 0, "num_executions": 0} - - # TODO: allow these to be configured in yaml - self.high_threshold = 10 - self.med_threshold = 5 - self.low_threshold = 0 - - def _categorise_num_executions(self, num_executions: int) -> int: - """ - Represent number of file accesses as a categorical variable. - - :param num_access: Number of file accesses. - :return: Bin number corresponding to the number of accesses. - """ - if num_executions > self.high_threshold: - return 3 - elif num_executions > self.med_threshold: - return 2 - elif num_executions > self.low_threshold: - return 1 - return 0 - - def observe(self, state: Dict) -> ObsType: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary. - :type state: Dict - :return: Obs containing the operating status, health status, and number of executions of the application. - :rtype: ObsType - """ - application_state = access_from_nested_dict(state, self.where) - if application_state is NOT_PRESENT_IN_STATE: - return self.default_observation - return { - "operating_status": application_state["operating_state"], - "health_status": application_state["health_state_visible"], - "num_executions": self._categorise_num_executions(application_state["num_executions"]), - } - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Gymnasium space representing the observation space for application status. - :rtype: spaces.Space - """ - return spaces.Dict( - { - "operating_status": spaces.Discrete(7), - "health_status": spaces.Discrete(5), - "num_executions": spaces.Discrete(4), - } - ) - - @classmethod - def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> ApplicationObservation: - """ - Create an application observation from a configuration schema. - - :param config: Configuration schema containing the necessary information for the application observation. - :type config: ConfigSchema - :param parent_where: Where in the simulation state dictionary to find the information about this application's - parent node. A typical location for a node might be ['network', 'nodes', ]. - :type parent_where: WhereType, optional - :return: Constructed application observation instance. - :rtype: ApplicationObservation - """ - return cls(where=parent_where + ["applications", config.application_name]) +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 1de34b40..7c184770 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """ Manages the reward function for the agent. diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/game/agent/scripted_agents/__init__.py +++ b/src/primaite/game/agent/scripted_agents/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 129fac1a..cf1b6ce8 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random from typing import Dict, Tuple diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index cd44644f..ce4d90d1 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Agents with predefined behaviours.""" from typing import Dict, Optional, Tuple diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index df9273f7..59fe699a 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random from typing import Dict, Optional, Tuple diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index c4f6062a..e9694a45 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random from typing import Dict, Tuple diff --git a/src/primaite/game/agent/utils.py b/src/primaite/game/agent/utils.py index 15efd0b6..87b02858 100644 --- a/src/primaite/game/agent/utils.py +++ b/src/primaite/game/agent/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, Hashable, Optional, Sequence NOT_PRESENT_IN_STATE = object() diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c8fbac4e..6555e272 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address from typing import Dict, List, Optional, Union diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py index 8d8949df..2cb5de7d 100644 --- a/src/primaite/game/science.py +++ b/src/primaite/game/science.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from random import random from typing import Any, Iterable, Mapping diff --git a/src/primaite/interface/__init__.py b/src/primaite/interface/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/interface/__init__.py +++ b/src/primaite/interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py index 1a9f0e5f..03d6491e 100644 --- a/src/primaite/interface/request.py +++ b/src/primaite/interface/request.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, ForwardRef, List, Literal, Union from pydantic import BaseModel, ConfigDict, StrictBool # , validate_call diff --git a/src/primaite/session/__init__.py b/src/primaite/session/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/session/__init__.py +++ b/src/primaite/session/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index c66663e3..8e608ede 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import random import sys diff --git a/src/primaite/session/episode_schedule.py b/src/primaite/session/episode_schedule.py index ad4d38e9..126dcf9f 100644 --- a/src/primaite/session/episode_schedule.py +++ b/src/primaite/session/episode_schedule.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from abc import ABC, abstractmethod from itertools import chain diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 78d7cb3c..6c2f4f29 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json from datetime import datetime from pathlib import Path diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 33c74b0e..33ba0540 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json from typing import Dict, SupportsFloat, Tuple diff --git a/src/primaite/setup/__init__.py b/src/primaite/setup/__init__.py index 12e7c4e7..1447a47b 100644 --- a/src/primaite/setup/__init__.py +++ b/src/primaite/setup/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Utilities to prepare the user's data folders.""" diff --git a/src/primaite/setup/reset_demo_notebooks.py b/src/primaite/setup/reset_demo_notebooks.py index f17fb211..ad4091e3 100644 --- a/src/primaite/setup/reset_demo_notebooks.py +++ b/src/primaite/setup/reset_demo_notebooks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import filecmp import shutil from logging import Logger diff --git a/src/primaite/setup/reset_example_configs.py b/src/primaite/setup/reset_example_configs.py index c7eeecd5..a94d6d4a 100644 --- a/src/primaite/setup/reset_example_configs.py +++ b/src/primaite/setup/reset_example_configs.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import filecmp import os import shutil diff --git a/src/primaite/simulator/__init__.py b/src/primaite/simulator/__init__.py index ade1a73b..e85a2d1e 100644 --- a/src/primaite/simulator/__init__.py +++ b/src/primaite/simulator/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Warning: SIM_OUTPUT is a mutable global variable for the simulation output directory.""" from datetime import datetime from enum import IntEnum diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 848570fe..567a0493 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa """Core of the PrimAITE Simulator.""" import warnings diff --git a/src/primaite/simulator/domain/__init__.py b/src/primaite/simulator/domain/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/domain/__init__.py +++ b/src/primaite/simulator/domain/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index d955cf55..85ec6d46 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """User account simulation.""" from enum import Enum from typing import Dict diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index a264ba24..d8b7782c 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Dict, Final, List, Literal, Tuple diff --git a/src/primaite/simulator/file_system/__init__.py b/src/primaite/simulator/file_system/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/file_system/__init__.py +++ b/src/primaite/simulator/file_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index ba39c791..57d01ec9 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import hashlib diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 2162915f..8ff4b6fb 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from pathlib import Path diff --git a/src/primaite/simulator/file_system/file_system_item_abc.py b/src/primaite/simulator/file_system/file_system_item_abc.py index a9db8825..48b95d20 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import math diff --git a/src/primaite/simulator/file_system/file_type.py b/src/primaite/simulator/file_system/file_type.py index e6e81070..343d3565 100644 --- a/src/primaite/simulator/file_system/file_type.py +++ b/src/primaite/simulator/file_system/file_type.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from enum import Enum diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index c98e4492..ee0f3d01 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import warnings diff --git a/src/primaite/simulator/network/__init__.py b/src/primaite/simulator/network/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/__init__.py +++ b/src/primaite/simulator/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 2b8503d6..1f6fe6b0 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 1082e172..bf677d5c 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Optional diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 5d36f58b..94c45428 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Literal, Type diff --git a/src/primaite/simulator/network/hardware/__init__.py b/src/primaite/simulator/network/hardware/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/__init__.py +++ b/src/primaite/simulator/network/hardware/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 51e200e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1,2229 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -import re -import secrets -from abc import ABC, abstractmethod -from ipaddress import IPv4Address, IPv4Network -from pathlib import Path -from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union - -from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel, Field, validate_call - -from primaite import getLogger -from primaite.exceptions import NetworkError -from primaite.interface.request import RequestResponse -from primaite.simulator import SIM_OUTPUT -from primaite.simulator.core import RequestFormat, RequestManager, RequestPermissionValidator, RequestType, SimComponent -from primaite.simulator.domain.account import Account -from primaite.simulator.file_system.file_system import FileSystem -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.nmne import NMNEConfig -from primaite.simulator.network.transmission.data_link_layer import Frame -from primaite.simulator.system.applications.application import Application -from primaite.simulator.system.core.packet_capture import PacketCapture -from primaite.simulator.system.core.session_manager import SessionManager -from primaite.simulator.system.core.software_manager import SoftwareManager -from primaite.simulator.system.core.sys_log import SysLog -from primaite.simulator.system.processes.process import Process -from primaite.simulator.system.services.service import Service -from primaite.simulator.system.services.terminal.terminal import Terminal -from primaite.simulator.system.software import IOSoftware, Software -from primaite.utils.converters import convert_dict_enum_keys_to_enum_values -from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP -from primaite.utils.validation.ipv4_address import IPV4Address -from primaite.utils.validation.port import PORT_LOOKUP - -IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware) - -_LOGGER = getLogger(__name__) - - -def generate_mac_address(oui: Optional[str] = None) -> str: - """ - Generate a random MAC Address. - - :param oui: The Organizationally Unique Identifier (OUI) portion of the MAC address. It should be a string with - the first 3 bytes (24 bits) in the format "XX:XX:XX". - :raises ValueError: If the 'oui' is not in the correct format (hexadecimal and 6 characters). - """ - random_bytes = [secrets.randbits(8) for _ in range(6)] - - if oui: - oui_pattern = re.compile(r"^([0-9A-Fa-f]{2}[:-]){2}[0-9A-Fa-f]{2}$") - if not oui_pattern.match(oui): - msg = f"Invalid oui. The oui should be in the format xx:xx:xx, where x is a hexadecimal digit, got '{oui}'" - _LOGGER.error(msg) - raise ValueError(msg) - oui_bytes = [int(chunk, 16) for chunk in oui.split(":")] - mac = oui_bytes + random_bytes[len(oui_bytes) :] - else: - mac = random_bytes - - return ":".join(f"{b:02x}" for b in mac) - - -class NetworkInterface(SimComponent, ABC): - """ - A generic Network Interface in a Node on a Network. - - This is a base class for specific types of network interfaces, providing common attributes and methods required - for network communication. It defines the fundamental properties that all network interfaces share, such as - MAC address, speed, MTU (maximum transmission unit), and the ability to enable or disable the interface. - - :ivar str mac_address: The MAC address of the network interface. Default to a randomly generated MAC address. - :ivar int speed: The speed of the interface in Mbps. Default is 100 Mbps. - :ivar int mtu: The Maximum Transmission Unit (MTU) of the interface in Bytes. Default is 1500 B. - """ - - mac_address: str = Field(default_factory=generate_mac_address) - "The MAC address of the interface." - - speed: float = 100.0 - "The speed of the interface in Mbps. Default is 100 Mbps." - - mtu: int = 1500 - "The Maximum Transmission Unit (MTU) of the interface in Bytes. Default is 1500 B" - - enabled: bool = False - "Indicates whether the interface is enabled." - - _connected_node: Optional[Node] = None - "The Node to which the interface is connected." - - port_num: Optional[int] = None - "The port number assigned to this interface on the connected node." - - port_name: Optional[str] = None - "The port name assigned to this interface on the connected node." - - pcap: Optional[PacketCapture] = None - "A PacketCapture instance for capturing and analysing packets passing through this interface." - - nmne_config: ClassVar[NMNEConfig] = NMNEConfig() - "A dataclass defining malicious network events to be captured." - - nmne: Dict = Field(default_factory=lambda: {}) - "A dict containing details of the number of malicious events captured." - - traffic: Dict = Field(default_factory=lambda: {}) - "A dict containing details of the inbound and outbound traffic by port and protocol." - - def setup_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - super().setup_for_episode(episode=episode) - self.nmne = {} - self.traffic = {} - if episode and self.pcap and SIM_OUTPUT.save_pcap_logs: - self.pcap.current_episode = episode - self.pcap.setup_logger() - self.enable() - - def _init_request_manager(self) -> RequestManager: - """ - Initialise the request manager. - - More information in user guide and docstring for SimComponent._init_request_manager. - """ - _is_network_interface_enabled = NetworkInterface._EnabledValidator(network_interface=self) - _is_network_interface_disabled = NetworkInterface._DisabledValidator(network_interface=self) - - rm = super()._init_request_manager() - - rm.add_request( - "enable", - RequestType( - func=lambda request, context: RequestResponse.from_bool(self.enable()), - validator=_is_network_interface_disabled, - ), - ) - rm.add_request( - "disable", - RequestType( - func=lambda request, context: RequestResponse.from_bool(self.disable()), - validator=_is_network_interface_enabled, - ), - ) - - return rm - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - :return: Current state of this object and child objects. - """ - state = super().describe_state() - state.update( - { - "mac_address": self.mac_address, - "speed": self.speed, - "mtu": self.mtu, - "enabled": self.enabled, - } - ) - if self.nmne_config and self.nmne_config.capture_nmne: - state.update({"nmne": self.nmne}) - state.update({"traffic": convert_dict_enum_keys_to_enum_values(self.traffic)}) - return state - - @abstractmethod - def enable(self) -> bool: - """Enable the interface.""" - pass - return False - - @abstractmethod - def disable(self) -> bool: - """Disable the interface.""" - pass - return False - - def _capture_nmne(self, frame: Frame, inbound: bool = True) -> None: - """ - Processes and captures network frame data based on predefined global NMNE settings. - - This method updates the NMNE structure with counts of malicious network events based on the frame content and - direction. The structure is dynamically adjusted according to the enabled capture settings. - - .. note:: - While there is a lot of logic in this code that defines a multi-level hierarchical NMNE structure, - most of it is unused for now as a result of all `CAPTURE_BY_<>` variables in - ``primaite.simulator.network.nmne`` being hardcoded and set as final. Once they're 'released' and made - configurable, this function will be updated to properly explain the dynamic data structure. - - :param frame: The network frame to process, containing IP, TCP/UDP, and payload information. - :param inbound: Boolean indicating if the frame direction is inbound. Defaults to True. - """ - # Exit function if NMNE capturing is disabled - if not (self.nmne_config and self.nmne_config.capture_nmne): - return - - # Initialise basic frame data variables - direction = "inbound" if inbound else "outbound" # Direction of the traffic - ip_address = str(frame.ip.src_ip_address if inbound else frame.ip.dst_ip_address) # Source or destination IP - protocol = frame.ip.protocol # Network protocol used in the frame - - # Initialise port variable; will be determined based on protocol type - port = None - - # Determine the source or destination port based on the protocol (TCP/UDP) - if frame.tcp: - port = frame.tcp.src_port if inbound else frame.tcp.dst_port - elif frame.udp: - port = frame.udp.src_port if inbound else frame.udp.dst_port - - # Convert frame payload to string for keyword checking - frame_str = str(frame.payload) - - # Proceed only if any NMNE keyword is present in the frame payload - if any(keyword in frame_str for keyword in self.nmne_config.nmne_capture_keywords): - # Start with the root of the NMNE capture structure - current_level = self.nmne - - # Update NMNE structure based on enabled settings - if self.nmne_config.capture_by_direction: - # Set or get the dictionary for the current direction - current_level = current_level.setdefault("direction", {}) - current_level = current_level.setdefault(direction, {}) - - if self.nmne_config.capture_by_ip_address: - # Set or get the dictionary for the current IP address - current_level = current_level.setdefault("ip_address", {}) - current_level = current_level.setdefault(ip_address, {}) - - if self.nmne_config.capture_by_protocol: - # Set or get the dictionary for the current protocol - current_level = current_level.setdefault("protocol", {}) - current_level = current_level.setdefault(protocol, {}) - - if self.nmne_config.capture_by_port: - # Set or get the dictionary for the current port - current_level = current_level.setdefault("port", {}) - current_level = current_level.setdefault(port, {}) - - # Ensure 'KEYWORD' level is present in the structure - keyword_level = current_level.setdefault("keywords", {}) - - # Increment the count for detected keywords in the payload - if self.nmne_config.capture_by_keyword: - for keyword in self.nmne_config.nmne_capture_keywords: - if keyword in frame_str: - # Update the count for each keyword found - keyword_level[keyword] = keyword_level.get(keyword, 0) + 1 - else: - # Increment a generic counter if keyword capturing is not enabled - keyword_level["*"] = keyword_level.get("*", 0) + 1 - - def _capture_traffic(self, frame: Frame, inbound: bool = True): - """ - Capture traffic statistics at the Network Interface. - - :param frame: The network frame containing the traffic data. - :type frame: Frame - :param inbound: Flag indicating if the traffic is inbound or outbound. Defaults to True. - :type inbound: bool - """ - # Determine the direction of the traffic - direction = "inbound" if inbound else "outbound" - - # Initialize protocol and port variables - protocol = None - port = None - - # Identify the protocol and port from the frame - if frame.tcp: - protocol = PROTOCOL_LOOKUP["TCP"] - port = frame.tcp.dst_port - elif frame.udp: - protocol = PROTOCOL_LOOKUP["UDP"] - port = frame.udp.dst_port - elif frame.icmp: - protocol = PROTOCOL_LOOKUP["ICMP"] - - # Ensure the protocol is in the capture dict - if protocol not in self.traffic: - self.traffic[protocol] = {} - - # Handle non-ICMP protocols that use ports - if protocol != PROTOCOL_LOOKUP["ICMP"]: - if port not in self.traffic[protocol]: - self.traffic[protocol][port] = {"inbound": 0, "outbound": 0} - self.traffic[protocol][port][direction] += frame.size_Mbits - else: - # Handle ICMP protocol separately (ICMP does not use ports) - if not self.traffic[protocol]: - self.traffic[protocol] = {"inbound": 0, "outbound": 0} - self.traffic[protocol][direction] += frame.size_Mbits - - @abstractmethod - def send_frame(self, frame: Frame) -> bool: - """ - Attempts to send a network frame through the interface. - - :param frame: The network frame to be sent. - :return: A boolean indicating whether the frame was successfully sent. - """ - self._capture_nmne(frame, inbound=False) - self._capture_traffic(frame, inbound=False) - - @abstractmethod - def receive_frame(self, frame: Frame) -> bool: - """ - Receives a network frame on the interface. - - :param frame: The network frame being received. - :return: A boolean indicating whether the frame was successfully received. - """ - self._capture_nmne(frame, inbound=True) - self._capture_traffic(frame, inbound=True) - - def __str__(self) -> str: - """ - String representation of the NIC. - - :return: A string combining the port number and the mac address - """ - return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}" - - def __hash__(self) -> int: - return hash(self.uuid) - - def apply_timestep(self, timestep: int) -> None: - """ - Apply a timestep evolution to this component. - - This just clears the nmne count back to 0. - """ - super().apply_timestep(timestep=timestep) - - def pre_timestep(self, timestep: int) -> None: - """Apply pre-timestep logic.""" - super().pre_timestep(timestep) - self.traffic = {} - - class _EnabledValidator(RequestPermissionValidator): - """ - When requests come in, this validator will only let them through if the NetworkInterface is enabled. - - This is useful because most actions should be being resolved if the NetworkInterface is disabled. - """ - - network_interface: NetworkInterface - """Save a reference to the node instance.""" - - def __call__(self, request: RequestFormat, context: Dict) -> bool: - """Return whether the NetworkInterface is enabled or not.""" - return self.network_interface.enabled - - @property - def fail_message(self) -> str: - """Message that is reported when a request is rejected by this validator.""" - return ( - f"Cannot perform request on NetworkInterface " - f"'{self.network_interface.mac_address}' because it is not enabled." - ) - - class _DisabledValidator(RequestPermissionValidator): - """ - When requests come in, this validator will only let them through if the NetworkInterface is disabled. - - This is useful because some actions should be being resolved if the NetworkInterface is disabled. - """ - - network_interface: NetworkInterface - """Save a reference to the node instance.""" - - def __call__(self, request: RequestFormat, context: Dict) -> bool: - """Return whether the NetworkInterface is disabled or not.""" - return not self.network_interface.enabled - - @property - def fail_message(self) -> str: - """Message that is reported when a request is rejected by this validator.""" - return ( - f"Cannot perform request on NetworkInterface " - f"'{self.network_interface.mac_address}' because it is not disabled." - ) - - -class WiredNetworkInterface(NetworkInterface, ABC): - """ - Represents a wired network interface in a network device. - - This abstract base class serves as a foundational blueprint for wired network interfaces, offering core - functionalities and enforcing the implementation of key operational methods such as enabling and disabling the - interface. It encapsulates common attributes and behaviors intrinsic to wired interfaces, including the - management of physical or logical connections to network links and the provision of methods for connecting to and - disconnecting from these links. - - Inherits from: - - NetworkInterface: Provides basic network interface properties and methods. - - - Subclasses of this class are expected to provide concrete implementations for the abstract methods defined here, - tailoring the functionality to the specific requirements of the wired interface types they represent. - """ - - _connected_link: Optional[Link] = None - "The network link to which the network interface is connected." - - def enable(self) -> bool: - """Attempt to enable the network interface.""" - if self.enabled: - return True - - if not self._connected_node: - _LOGGER.warning(f"Interface {self} cannot be enabled as it is not connected to a Node") - return False - - if self._connected_node.operating_state != NodeOperatingState.ON: - self._connected_node.sys_log.warning( - f"Interface {self} cannot be enabled as the connected Node is not powered on" - ) - return False - - if not self._connected_link: - self._connected_node.sys_log.warning(f"Interface {self} cannot be enabled as there is no Link connected.") - return False - - self.enabled = True - self._connected_node.sys_log.info(f"Network Interface {self} enabled") - self.pcap = PacketCapture( - hostname=self._connected_node.hostname, port_num=self.port_num, port_name=self.port_name - ) - if self._connected_link: - self._connected_link.endpoint_up() - return True - - def disable(self) -> bool: - """Disable the network interface.""" - if not self.enabled: - return True - self.enabled = False - if self._connected_node: - self._connected_node.sys_log.info(f"Network Interface {self} disabled") - else: - _LOGGER.debug(f"Interface {self} disabled") - if self._connected_link: - self._connected_link.endpoint_down() - return True - - def connect_link(self, link: Link): - """ - Connect this network interface to a specified link. - - This method establishes a connection between the network interface and a network link if the network interface - is not already connected. If the network interface is already connected to a link, it logs an error and does - not change the existing connection. - - :param link: The Link instance to connect to this network interface. - """ - if self._connected_link: - _LOGGER.warning(f"Cannot connect Link to network interface {self} as it already has a connection") - return - - if self._connected_link == link: - _LOGGER.warning(f"Cannot connect Link to network interface {self} as it is already connected") - return - - self._connected_link = link - self.enable() - - def disconnect_link(self): - """ - Disconnect the network interface from its connected Link, if any. - - This method removes the association between the network interface and its connected Link. It updates the - connected Link's endpoints to reflect the disconnection. - """ - if self._connected_link.endpoint_a == self: - self._connected_link.endpoint_a = None - if self._connected_link.endpoint_b == self: - self._connected_link.endpoint_b = None - self._connected_link = None - - def send_frame(self, frame: Frame) -> bool: - """ - Attempt to send a network frame through the connected Link. - - This method sends a frame if the NIC is enabled and connected to a link. It captures the frame using PCAP - (if available) and transmits it through the connected link. Returns True if the frame is successfully sent, - False otherwise (e.g., if the Network Interface is disabled). - - :param frame: The network frame to be sent. - :return: True if the frame is sent, False if the Network Interface is disabled or not connected to a link. - """ - if not self.enabled: - return False - if not self._connected_link.can_transmit_frame(frame): - # Drop frame for now. Queuing will happen here (probably) if it's done in the future. - self._connected_node.sys_log.info(f"{self}: Frame dropped as Link is at capacity") - return False - super().send_frame(frame) - frame.set_sent_timestamp() - self.pcap.capture_outbound(frame) - self._connected_link.transmit_frame(sender_nic=self, frame=frame) - return True - - @abstractmethod - def receive_frame(self, frame: Frame) -> bool: - """ - Receives a network frame on the network interface. - - :param frame: The network frame being received. - :return: A boolean indicating whether the frame was successfully received. - """ - return super().receive_frame(frame) - - -class Layer3Interface(BaseModel, ABC): - """ - Represents a Layer 3 (Network Layer) interface in a network device. - - This class serves as a base for network interfaces that operate at Layer 3 of the OSI model, providing IP - connectivity and subnetting capabilities. It's not meant to be instantiated directly but to be subclassed by - specific types of network interfaces that require IP addressing capabilities. - - :ivar IPV4Address ip_address: The IP address assigned to the interface. This address enables the interface to - participate in IP-based networking, allowing it to send and receive IP packets. - :ivar IPv4Address subnet_mask: The subnet mask assigned to the interface. This mask helps in determining the - network segment that the interface belongs to and is used in IP routing decisions. - """ - - ip_address: IPV4Address - "The IP address assigned to the interface for communication on an IP-based network." - - subnet_mask: IPV4Address - "The subnet mask assigned to the interface, defining the network portion and the host portion of the IP address." - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - :return: Current state of this object and child objects. - """ - state = { - "ip_address": str(self.ip_address), - "subnet_mask": str(self.subnet_mask), - } - - return state - - @property - def ip_network(self) -> IPv4Network: - """ - Calculate and return the IPv4Network derived from the NIC's IP address and subnet mask. - - This property constructs an IPv4Network object which represents the whole network that the NIC's IP address - belongs to, based on its subnet mask. It's useful for determining the network range and broadcast address. - - :return: An IPv4Network instance representing the network of this NIC. - """ - return IPv4Network(f"{self.ip_address}/{self.subnet_mask}", strict=False) - - -class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): - """ - Represents an IP wired network interface. - - This interface operates at both the data link layer (Layer 2) and the network layer (Layer 3) of the OSI model, - specifically tailored for IP-based communication. This abstract class serves as a template for creating specific - wired network interfaces that support Internet Protocol (IP) functionalities. - - As this class is an amalgamation of its parent classes without additional attributes or methods, it is recommended - to refer to the documentation of `WiredNetworkInterface` and `Layer3Interface` for detailed information on the - supported operations and functionalities. - - The class inherits from: - - `WiredNetworkInterface`: Provides the functionalities and characteristics of a wired connection, such as - physical link establishment and data transmission over a cable. - - `Layer3Interface`: Enables network layer capabilities, including IP address assignment, routing, and - potentially, Layer 3 protocols like IPsec. - - As an abstract class, `IPWiredNetworkInterface` does not implement specific methods but mandates that any derived - class provides implementations for the functionalities of both `WiredNetworkInterface` and `Layer3Interface`. - This structure is ideal for representing network interfaces in devices that require wired connections and are - capable of IP routing and addressing, such as routers, switches, as well as end-host devices like computers and - servers. - - Derived classes should define specific behaviors and properties of an IP-capable wired network interface, - customizing it for their specific use cases. - """ - - _connected_link: Optional[Link] = None - "The network link to which the network interface is connected." - - def model_post_init(self, __context: Any) -> None: - """ - Performs post-initialisation checks to ensure the model's IP configuration is valid. - - This method is invoked after the initialisation of a network model object to validate its network settings, - particularly to ensure that the assigned IP address is not a network address. This validation is crucial for - maintaining the integrity of network simulations and avoiding configuration errors that could lead to - unrealistic or incorrect behavior. - - :param __context: Contextual information or parameters passed to the method, used for further initializing or - validating the model post-creation. - :raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration. - """ - if self.ip_network.network_address == self.ip_address: - raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address") - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - :return: Current state of this object and child objects. - :rtype: Dict - """ - # Get the state from the WiredNetworkInterface - state = WiredNetworkInterface.describe_state(self) - - # Update the state with information from Layer3Interface - state.update(Layer3Interface.describe_state(self)) - - return state - - def enable(self) -> bool: - """ - Enables this wired network interface and attempts to send a "hello" message to the default gateway. - - This method activates the network interface, making it operational for network communications. After enabling, - it tries to initiate a default gateway "hello" process, typically to establish initial connectivity and resolve - the default gateway's MAC address. This step is crucial for ensuring the interface can successfully send data - to and receive data from the network. - - The method safely handles cases where the connected node might not have a default gateway set or the - `default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption. - """ - super().enable() - try: - self._connected_node.default_gateway_hello() - except AttributeError: - pass - return True - - @abstractmethod - def receive_frame(self, frame: Frame) -> bool: - """ - Receives a network frame on the network interface. - - :param frame: The network frame being received. - :return: A boolean indicating whether the frame was successfully received. - """ - return super().receive_frame(frame) - - -class Link(SimComponent): - """ - Represents a network link between NIC<-->NIC, NIC<-->SwitchPort, or SwitchPort<-->SwitchPort. - - :param endpoint_a: The first NIC or SwitchPort connected to the Link. - :param endpoint_b: The second NIC or SwitchPort connected to the Link. - :param bandwidth: The bandwidth of the Link in Mbps. - """ - - endpoint_a: WiredNetworkInterface - "The first WiredNetworkInterface connected to the Link." - endpoint_b: WiredNetworkInterface - "The second WiredNetworkInterface connected to the Link." - bandwidth: float - "The bandwidth of the Link in Mbps." - current_load: float = 0.0 - "The current load on the link in Mbps." - - def __init__(self, **kwargs): - """ - Ensure that endpoint_a and endpoint_b are not the same NIC. - - Connect the link to the NICs after creation. - - :raises ValueError: If endpoint_a and endpoint_b are the same NIC. - """ - if kwargs["endpoint_a"] == kwargs["endpoint_b"]: - msg = "endpoint_a and endpoint_b cannot be the same NIC or SwitchPort" - _LOGGER.error(msg) - raise ValueError(msg) - super().__init__(**kwargs) - self.endpoint_a.connect_link(self) - self.endpoint_b.connect_link(self) - self.endpoint_up() - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - - :return: Current state of this object and child objects. - :rtype: Dict - """ - state = super().describe_state() - state.update( - { - "endpoint_a": self.endpoint_a.uuid, # TODO: consider if using UUID is the best way to do this - "endpoint_b": self.endpoint_b.uuid, # TODO: consider if using UUID is the best way to do this - "bandwidth": self.bandwidth, - "current_load": self.current_load, - } - ) - return state - - @property - def current_load_percent(self) -> str: - """Get the current load formatted as a percentage string.""" - return f"{self.current_load / self.bandwidth:.5f}%" - - def endpoint_up(self): - """Let the Link know and endpoint has been brought up.""" - if self.is_up: - _LOGGER.debug(f"Link {self} up") - - def endpoint_down(self): - """Let the Link know and endpoint has been brought down.""" - if not self.is_up: - self.current_load = 0.0 - _LOGGER.debug(f"Link {self} down") - - @property - def is_up(self) -> bool: - """ - Informs whether the link is up. - - This is based upon both NIC endpoints being enabled. - """ - return self.endpoint_a.enabled and self.endpoint_b.enabled - - def can_transmit_frame(self, frame: Frame) -> bool: - """ - Determines whether a frame can be transmitted considering the current Link load and the Link's bandwidth. - - This method assesses if the transmission of a given frame is possible without exceeding the Link's total - bandwidth capacity. It checks if the current load of the Link plus the size of the frame (expressed in Mbps) - would remain within the defined bandwidth limits. The transmission is only feasible if the Link is active - ('up') and the total load including the new frame does not surpass the bandwidth limit. - - :param frame: The frame intended for transmission, which contains its size in Mbps. - :return: True if the frame can be transmitted without exceeding the bandwidth limit, False otherwise. - """ - if self.is_up: - frame_size_Mbits = frame.size_Mbits # noqa - Leaving it as Mbits as this is how they're expressed - return self.current_load + frame.size_Mbits <= self.bandwidth - return False - - def transmit_frame(self, sender_nic: WiredNetworkInterface, frame: Frame) -> bool: - """ - Send a network frame from one NIC or SwitchPort to another connected NIC or SwitchPort. - - :param sender_nic: The NIC or SwitchPort sending the frame. - :param frame: The network frame to be sent. - :return: True if the Frame can be sent, otherwise False. - """ - receiver = self.endpoint_a - if receiver == sender_nic: - receiver = self.endpoint_b - frame_size = frame.size_Mbits - - if receiver.receive_frame(frame): - # Frame transmitted successfully - # Load the frame size on the link - self.current_load += frame_size - _LOGGER.debug( - f"Added {frame_size:.3f} Mbits to {self}, current load {self.current_load:.3f} Mbits " - f"({self.current_load_percent})" - ) - return True - return False - - def __str__(self) -> str: - return f"{self.endpoint_a}<-->{self.endpoint_b}" - - def apply_timestep(self, timestep: int) -> None: - """Apply a timestep to the simulation.""" - super().apply_timestep(timestep) - - def pre_timestep(self, timestep: int) -> None: - """Apply pre-timestep logic.""" - super().pre_timestep(timestep) - self.current_load = 0.0 - - -class User(SimComponent): - """ - Represents a user in the PrimAITE system. - - :ivar username: The username of the user - :ivar password: The password of the user - :ivar disabled: Boolean flag indicating whether the user is disabled - :ivar is_admin: Boolean flag indicating whether the user has admin privileges - """ - - username: str - """The username of the user""" - - password: str - """The password of the user""" - - disabled: bool = False - """Boolean flag indicating whether the user is disabled""" - - is_admin: bool = False - """Boolean flag indicating whether the user has admin privileges""" - - num_of_logins: int = 0 - """Counts the number of the User has logged in""" - - def describe_state(self) -> Dict: - """ - Returns a dictionary representing the current state of the user. - - :return: A dict containing the state of the user - """ - return self.model_dump() - - -class UserManager(Service): - """ - Manages users within the PrimAITE system, handling creation, authentication, and administration. - - :param users: A dictionary of all users by their usernames - :param admins: A dictionary of admin users by their usernames - :param disabled_admins: A dictionary of currently disabled admin users by their usernames - """ - - users: Dict[str, User] = {} - - def __init__(self, **kwargs): - """ - Initializes a UserManager instanc. - - :param username: The username for the default admin user - :param password: The password for the default admin user - """ - kwargs["name"] = "UserManager" - kwargs["port"] = PORT_LOOKUP["NONE"] - kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] - super().__init__(**kwargs) - - self.start() - - def _init_request_manager(self) -> RequestManager: - """ - Initialise the request manager. - - More information in user guide and docstring for SimComponent._init_request_manager. - """ - rm = super()._init_request_manager() - - # todo add doc about requeest schemas - rm.add_request( - "change_password", - RequestType( - func=lambda request, context: RequestResponse.from_bool( - self.change_user_password(username=request[0], current_password=request[1], new_password=request[2]) - ) - ), - ) - return rm - - def describe_state(self) -> Dict: - """ - Returns the state of the UserManager along with the number of users and admins. - - :return: A dict containing detailed state information - """ - state = super().describe_state() - state.update({"total_users": len(self.users), "total_admins": len(self.admins) + len(self.disabled_admins)}) - state["users"] = {k: v.describe_state() for k, v in self.users.items()} - return state - - def show(self, markdown: bool = False): - """ - Display the Users. - - :param markdown: Whether to display the table in Markdown format or not. Default is `False`. - """ - table = PrettyTable(["Username", "Admin", "Disabled"]) - if markdown: - table.set_style(MARKDOWN) - table.align = "l" - table.title = f"{self.sys_log.hostname} User Manager" - for user in self.users.values(): - table.add_row([user.username, user.is_admin, user.disabled]) - print(table.get_string(sortby="Username")) - - @property - def non_admins(self) -> Dict[str, User]: - """ - Returns a dictionary of all enabled non-admin users. - - :return: A dictionary with usernames as keys and User objects as values for non-admin, non-disabled users. - """ - return {k: v for k, v in self.users.items() if not v.is_admin and not v.disabled} - - @property - def disabled_non_admins(self) -> Dict[str, User]: - """ - Returns a dictionary of all disabled non-admin users. - - :return: A dictionary with usernames as keys and User objects as values for non-admin, disabled users. - """ - return {k: v for k, v in self.users.items() if not v.is_admin and v.disabled} - - @property - def admins(self) -> Dict[str, User]: - """ - Returns a dictionary of all enabled admin users. - - :return: A dictionary with usernames as keys and User objects as values for admin, non-disabled users. - """ - return {k: v for k, v in self.users.items() if v.is_admin and not v.disabled} - - @property - def disabled_admins(self) -> Dict[str, User]: - """ - Returns a dictionary of all disabled admin users. - - :return: A dictionary with usernames as keys and User objects as values for admin, disabled users. - """ - return {k: v for k, v in self.users.items() if v.is_admin and v.disabled} - - def install(self) -> None: - """Setup default user during first-time installation.""" - self.add_user(username="admin", password="admin", is_admin=True, bypass_can_perform_action=True) - - def _is_last_admin(self, username: str) -> bool: - return username in self.admins and len(self.admins) == 1 - - def add_user( - self, username: str, password: str, is_admin: bool = False, bypass_can_perform_action: bool = False - ) -> bool: - """ - Adds a new user to the system. - - :param username: The username for the new user - :param password: The password for the new user - :param is_admin: Flag indicating if the new user is an admin - :return: True if user was successfully added, False otherwise - """ - if not bypass_can_perform_action and not self._can_perform_action(): - return False - if username in self.users: - self.sys_log.info(f"{self.name}: Failed to create new user {username} as this user name already exists") - return False - user = User(username=username, password=password, is_admin=is_admin) - self.users[username] = user - self.sys_log.info(f"{self.name}: Added new {'admin' if is_admin else 'user'}: {username}") - return True - - def authenticate_user(self, username: str, password: str) -> Optional[User]: - """ - Authenticates a user's login attempt. - - :param username: The username of the user trying to log in - :param password: The password provided by the user - :return: The User object if authentication is successful, None otherwise - """ - if not self._can_perform_action(): - return None - user = self.users.get(username) - if user and not user.disabled and user.password == password: - self.sys_log.info(f"{self.name}: User authenticated: {username}") - return user - self.sys_log.info(f"{self.name}: Authentication failed for: {username}") - return None - - def change_user_password(self, username: str, current_password: str, new_password: str) -> bool: - """ - Changes a user's password. - - :param username: The username of the user changing their password - :param current_password: The current password of the user - :param new_password: The new password for the user - :return: True if the password was changed successfully, False otherwise - """ - if not self._can_perform_action(): - return False - user = self.users.get(username) - if user and user.password == current_password: - user.password = new_password - self.sys_log.info(f"{self.name}: Password changed for {username}") - self._user_session_manager._logout_user(user=user) - return True - self.sys_log.info(f"{self.name}: Password change failed for {username}") - return False - - def disable_user(self, username: str) -> bool: - """ - Disables a user account, preventing them from logging in. - - :param username: The username of the user to disable - :return: True if the user was disabled successfully, False otherwise - """ - if not self._can_perform_action(): - return False - if username in self.users and not self.users[username].disabled: - if self._is_last_admin(username): - self.sys_log.info(f"{self.name}: Cannot disable User {username} as they are the only enabled admin") - return False - self.users[username].disabled = True - self.sys_log.info(f"{self.name}: User disabled: {username}") - return True - self.sys_log.info(f"{self.name}: Failed to disable user: {username}") - return False - - def enable_user(self, username: str) -> bool: - """ - Enables a previously disabled user account. - - :param username: The username of the user to enable - :return: True if the user was enabled successfully, False otherwise - """ - if username in self.users and self.users[username].disabled: - self.users[username].disabled = False - self.sys_log.info(f"{self.name}: User enabled: {username}") - return True - self.sys_log.info(f"{self.name}: Failed to enable user: {username}") - return False - - @property - def _user_session_manager(self) -> "UserSessionManager": - return self.software_manager.software["UserSessionManager"] # noqa - - -class UserSession(SimComponent): - """ - Represents a user session on the Node. - - This class manages the state of a user session, including the user, session start, last active step, - and end step. It also indicates whether the session is local. - - :ivar user: The user associated with this session. - :ivar start_step: The timestep when the session was started. - :ivar last_active_step: The last timestep when the session was active. - :ivar end_step: The timestep when the session ended, if applicable. - :ivar local: Indicates if the session is local. Defaults to True. - """ - - user: User - """The user associated with this session.""" - - start_step: int - """The timestep when the session was started.""" - - last_active_step: int - """The last timestep when the session was active.""" - - end_step: Optional[int] = None - """The timestep when the session ended, if applicable.""" - - local: bool = True - """Indicates if the session is a local session or a remote session. Defaults to True as a local session.""" - - @classmethod - def create(cls, user: User, timestep: int) -> UserSession: - """ - Creates a new instance of UserSession. - - This class method initialises a user session with the given user and timestep. - - :param user: The user associated with this session. - :param timestep: The timestep when the session is created. - :return: An instance of UserSession. - """ - user.num_of_logins += 1 - return UserSession(user=user, start_step=timestep, last_active_step=timestep) - - def describe_state(self) -> Dict: - """ - Describes the current state of the user session. - - :return: A dictionary representing the state of the user session. - """ - return self.model_dump() - - -class RemoteUserSession(UserSession): - """ - Represents a remote user session on the Node. - - This class extends the UserSession class to include additional attributes and methods specific to remote sessions. - - :ivar remote_ip_address: The IP address of the remote user. - :ivar local: Indicates that this is not a local session. Always set to False. - """ - - remote_ip_address: IPV4Address - """The IP address of the remote user.""" - - local: bool = False - """Indicates that this is not a local session. Always set to False.""" - - @classmethod - def create(cls, user: User, timestep: int, remote_ip_address: IPV4Address) -> RemoteUserSession: # noqa - """ - Creates a new instance of RemoteUserSession. - - This class method initialises a remote user session with the given user, timestep, and remote IP address. - - :param user: The user associated with this session. - :param timestep: The timestep when the session is created. - :param remote_ip_address: The IP address of the remote user. - :return: An instance of RemoteUserSession. - """ - return RemoteUserSession( - user=user, start_step=timestep, last_active_step=timestep, remote_ip_address=remote_ip_address - ) - - def describe_state(self) -> Dict: - """ - Describes the current state of the remote user session. - - This method extends the base describe_state method to include the remote IP address. - - :return: A dictionary representing the state of the remote user session. - """ - state = super().describe_state() - state["remote_ip_address"] = str(self.remote_ip_address) - return state - - -class UserSessionManager(Service): - """ - Manages user sessions on a Node, including local and remote sessions. - - This class handles authentication, session management, and session timeouts for users interacting with the Node. - """ - - local_session: Optional[UserSession] = None - """The current local user session, if any.""" - - remote_sessions: Dict[str, RemoteUserSession] = {} - """A dictionary of active remote user sessions.""" - - historic_sessions: List[UserSession] = Field(default_factory=list) - """A list of historic user sessions.""" - - local_session_timeout_steps: int = 30 - """The number of steps before a local session times out due to inactivity.""" - - remote_session_timeout_steps: int = 30 - """The number of steps before a remote session times out due to inactivity.""" - - max_remote_sessions: int = 3 - """The maximum number of concurrent remote sessions allowed.""" - - current_timestep: int = 0 - """The current timestep in the simulation.""" - - def __init__(self, **kwargs): - """ - Initializes a UserSessionManager instance. - - :param username: The username for the default admin user - :param password: The password for the default admin user - """ - kwargs["name"] = "UserSessionManager" - kwargs["port"] = PORT_LOOKUP["NONE"] - kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] - super().__init__(**kwargs) - self.start() - - def _init_request_manager(self) -> RequestManager: - """ - Initialise the request manager. - - More information in user guide and docstring for SimComponent._init_request_manager. - """ - rm = super()._init_request_manager() - - def _remote_login(request: RequestFormat, context: Dict) -> RequestResponse: - """Request should take the form [username, password, remote_ip_address].""" - username, password, remote_ip_address = request - response = RequestResponse.from_bool(self.remote_login(username, password, remote_ip_address)) - response.data = {"remote_hostname": self.parent.hostname, "username": username} - return response - - rm.add_request("remote_login", RequestType(func=_remote_login)) - - rm.add_request( - "remote_logout", - RequestType( - func=lambda request, context: RequestResponse.from_bool( - self.remote_logout(remote_session_id=request[0]) - ) - ), - ) - return rm - - def show(self, markdown: bool = False, include_session_id: bool = False, include_historic: bool = False): - """ - Displays a table of the user sessions on the Node. - - :param markdown: Whether to display the table in markdown format. - :param include_session_id: Whether to include session IDs in the table. - :param include_historic: Whether to include historic sessions in the table. - """ - headers = ["Session ID", "Username", "Type", "Remote IP", "Start Step", "Step Last Active", "End Step"] - - if not include_session_id: - headers = headers[1:] - - table = PrettyTable(headers) - - if markdown: - table.set_style(MARKDOWN) - table.align = "l" - table.title = f"{self.parent.hostname} User Sessions" - - def _add_session_to_table(user_session: UserSession): - """ - Adds a user session to the table for display. - - This helper function determines whether the session is local or remote and formats the session data - accordingly. It then adds the session data to the table. - - :param user_session: The user session to add to the table. - """ - session_type = "local" - remote_ip = "" - if isinstance(user_session, RemoteUserSession): - session_type = "remote" - remote_ip = str(user_session.remote_ip_address) - data = [ - user_session.uuid, - user_session.user.username, - session_type, - remote_ip, - user_session.start_step, - user_session.last_active_step, - user_session.end_step if user_session.end_step else "", - ] - if not include_session_id: - data = data[1:] - table.add_row(data) - - if self.local_session is not None: - _add_session_to_table(self.local_session) - - for user_session in self.remote_sessions.values(): - _add_session_to_table(user_session) - - if include_historic: - for user_session in self.historic_sessions: - _add_session_to_table(user_session) - - print(table.get_string(sortby="Step Last Active", reversesort=True)) - - def describe_state(self) -> Dict: - """ - Describes the current state of the UserSessionManager. - - :return: A dictionary representing the current state. - """ - state = super().describe_state() - state["current_local_user"] = None if not self.local_session else self.local_session.user.username - state["active_remote_sessions"] = list(self.remote_sessions.keys()) - return state - - @property - def _user_manager(self) -> UserManager: - """ - Returns the UserManager instance. - - :return: The UserManager instance. - """ - return self.software_manager.software["UserManager"] # noqa - - def pre_timestep(self, timestep: int) -> None: - """Apply any pre-timestep logic that helps make sure we have the correct observations.""" - self.current_timestep = timestep - inactive_sessions: list = [] - if self.local_session: - if self.local_session.last_active_step + self.local_session_timeout_steps <= timestep: - inactive_sessions.append(self.local_session) - - for session in self.remote_sessions: - remote_session = self.remote_sessions[session] - if remote_session.last_active_step + self.remote_session_timeout_steps <= timestep: - inactive_sessions.append(remote_session) - - for sessions in inactive_sessions: - self._timeout_session(sessions) - - def _timeout_session(self, session: UserSession) -> None: - """ - Handles session timeout logic. - - :param session: The session to be timed out. - """ - session.end_step = self.current_timestep - session_identity = session.user.username - if session.local: - self.local_session = None - session_type = "Local" - else: - self.remote_sessions.pop(session.uuid) - session_type = "Remote" - session_identity = f"{session_identity} {session.remote_ip_address}" - self.parent.terminal._connections.pop(session.uuid) - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload={"type": "user_timeout", "connection_id": session.uuid}, - dest_port=PORT_LOOKUP["SSH"], - dest_ip_address=session.remote_ip_address, - ) - - self.sys_log.info(f"{self.name}: {session_type} {session_identity} session timeout due to inactivity") - - @property - def remote_session_limit_reached(self) -> bool: - """ - Checks if the maximum number of remote sessions has been reached. - - :return: True if the limit is reached, otherwise False. - """ - return len(self.remote_sessions) >= self.max_remote_sessions - - def validate_remote_session_uuid(self, remote_session_id: str) -> bool: - """ - Validates if a given remote session ID exists. - - :param remote_session_id: The remote session ID to validate. - :return: True if the session ID exists, otherwise False. - """ - return remote_session_id in self.remote_sessions - - def _login( - self, username: str, password: str, local: bool = True, remote_ip_address: Optional[IPv4Address] = None - ) -> Optional[str]: - """ - Logs a user in either locally or remotely. - - :param username: The username of the account. - :param password: The password of the account. - :param local: Whether the login is local or remote. - :param remote_ip_address: The remote IP address for remote login. - :return: The session ID if login is successful, otherwise None. - """ - if not self._can_perform_action(): - return None - - user = self._user_manager.authenticate_user(username=username, password=password) - - if not user: - self.sys_log.info(f"{self.name}: Incorrect username or password") - return None - - session_id = None - if local: - create_new_session = True - if self.local_session: - if self.local_session.user != user: - # logout the current user - self.local_logout() - else: - # not required as existing logged-in user attempting to re-login - create_new_session = False - - if create_new_session: - self.local_session = UserSession.create(user=user, timestep=self.current_timestep) - - session_id = self.local_session.uuid - else: - if not self.remote_session_limit_reached: - remote_session = RemoteUserSession.create( - user=user, timestep=self.current_timestep, remote_ip_address=remote_ip_address - ) - session_id = remote_session.uuid - self.remote_sessions[session_id] = remote_session - self.sys_log.info(f"{self.name}: User {user.username} logged in") - return session_id - - def local_login(self, username: str, password: str) -> Optional[str]: - """ - Logs a user in locally. - - :param username: The username of the account. - :param password: The password of the account. - :return: The session ID if login is successful, otherwise None. - """ - return self._login(username=username, password=password, local=True) - - @validate_call() - def remote_login(self, username: str, password: str, remote_ip_address: IPV4Address) -> Optional[str]: - """ - Logs a user in remotely. - - :param username: The username of the account. - :param password: The password of the account. - :param remote_ip_address: The remote IP address for the remote login. - :return: The session ID if login is successful, otherwise None. - """ - return self._login(username=username, password=password, local=False, remote_ip_address=remote_ip_address) - - def _logout(self, local: bool = True, remote_session_id: Optional[str] = None) -> bool: - """ - Logs a user out either locally or remotely. - - :param local: Whether the logout is local or remote. - :param remote_session_id: The remote session ID for remote logout. - :return: True if logout successful, otherwise False. - """ - if not self._can_perform_action(): - return False - session = None - if local and self.local_session: - session = self.local_session - session.end_step = self.current_timestep - self.local_session = None - - if not local and remote_session_id: - self.parent.terminal._disconnect(remote_session_id) - session = self.remote_sessions.pop(remote_session_id) - if session: - self.historic_sessions.append(session) - self.sys_log.info(f"{self.name}: User {session.user.username} logged out") - return True - return False - - def local_logout(self) -> bool: - """ - Logs out the current local user. - - :return: True if logout successful, otherwise False. - """ - return self._logout(local=True) - - def remote_logout(self, remote_session_id: str) -> bool: - """ - Logs out a remote user by session ID. - - :param remote_session_id: The remote session ID. - :return: True if logout successful, otherwise False. - """ - return self._logout(local=False, remote_session_id=remote_session_id) - - def _logout_user(self, user: Union[str, User]) -> bool: - """End a user session by username or user object.""" - if isinstance(user, str): - user = self._user_manager.users[user] # grab user object from username - for sess_id, session in self.remote_sessions.items(): - if session.user is user: - self._logout(local=False, remote_session_id=sess_id) - return True - if self.local_user_logged_in and self.local_session.user is user: - self.local_logout() - return True - return False - - @property - def local_user_logged_in(self) -> bool: - """ - Checks if a local user is currently logged in. - - :return: True if a local user is logged in, otherwise False. - """ - return self.local_session is not None - - -class Node(SimComponent): - """ - A basic Node class that represents a node on the network. - - This class manages the state of the node, including the NICs (Network Interface Cards), accounts, applications, - services, processes, file system, and various managers like ARP, ICMP, SessionManager, and SoftwareManager. - - :param hostname: The node hostname on the network. - :param operating_state: The node operating state, either ON or OFF. - """ - - hostname: str - "The node hostname on the network." - default_gateway: Optional[IPV4Address] = None - "The default gateway IP address for forwarding network traffic to other networks." - operating_state: NodeOperatingState = NodeOperatingState.OFF - "The hardware state of the node." - network_interfaces: Dict[str, NetworkInterface] = {} - "The Network Interfaces on the node." - network_interface: Dict[int, NetworkInterface] = {} - "The Network Interfaces on the node by port id." - dns_server: Optional[IPv4Address] = None - "List of IP addresses of DNS servers used for name resolution." - - accounts: Dict[str, Account] = {} - "All accounts on the node." - applications: Dict[str, Application] = {} - "All applications on the node." - services: Dict[str, Service] = {} - "All services on the node." - processes: Dict[str, Process] = {} - "All processes on the node." - file_system: FileSystem - "The nodes file system." - root: Path - "Root directory for simulation output." - sys_log: SysLog - session_manager: SessionManager - software_manager: SoftwareManager - - revealed_to_red: bool = False - "Informs whether the node has been revealed to a red agent." - - start_up_duration: int = 3 - "Time steps needed for the node to start up." - - start_up_countdown: int = 0 - "Time steps needed until node is booted up." - - shut_down_duration: int = 3 - "Time steps needed for the node to shut down." - - shut_down_countdown: int = 0 - "Time steps needed until node is shut down." - - is_resetting: bool = False - "If true, the node will try turning itself off then back on again." - - node_scan_duration: int = 10 - "How many timesteps until the whole node is scanned. Default 10 time steps." - - node_scan_countdown: int = 0 - "Time steps until scan is complete" - - red_scan_countdown: int = 0 - "Time steps until reveal to red scan is complete." - - SYSTEM_SOFTWARE: ClassVar[Dict[str, Type[Software]]] = {} - "Base system software that must be preinstalled." - - _registry: ClassVar[Dict[str, Type["Node"]]] = {} - """Registry of application types. Automatically populated when subclasses are defined.""" - - _identifier: ClassVar[str] = "unknown" - """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" - - def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: - """ - Register a node type. - - :param identifier: Uniquely specifies an node class by name. Used for finding items by config. - :type identifier: str - :raises ValueError: When attempting to register an node with a name that is already allocated. - """ - if identifier == "default": - return - identifier = identifier.lower() - super().__init_subclass__(**kwargs) - if identifier in cls._registry: - raise ValueError(f"Tried to define new node {identifier}, but this name is already reserved.") - cls._registry[identifier] = cls - cls._identifier = identifier - - def __init__(self, **kwargs): - """ - Initialize the Node with various components and managers. - - This method initialises the ARP cache, ICMP handler, session manager, and software manager if they are not - provided. - """ - if not kwargs.get("sys_log"): - kwargs["sys_log"] = SysLog(kwargs["hostname"]) - if not kwargs.get("session_manager"): - kwargs["session_manager"] = SessionManager(sys_log=kwargs.get("sys_log")) - if not kwargs.get("root"): - kwargs["root"] = SIM_OUTPUT.path / kwargs["hostname"] - if not kwargs.get("file_system"): - kwargs["file_system"] = FileSystem(sys_log=kwargs["sys_log"], sim_root=kwargs["root"] / "fs") - if not kwargs.get("software_manager"): - kwargs["software_manager"] = SoftwareManager( - parent_node=self, - sys_log=kwargs.get("sys_log"), - session_manager=kwargs.get("session_manager"), - file_system=kwargs.get("file_system"), - dns_server=kwargs.get("dns_server"), - ) - super().__init__(**kwargs) - self._install_system_software() - self.session_manager.node = self - self.session_manager.software_manager = self.software_manager - - @property - def user_manager(self) -> Optional[UserManager]: - """The Nodes User Manager.""" - return self.software_manager.software.get("UserManager") # noqa - - @property - def user_session_manager(self) -> Optional[UserSessionManager]: - """The Nodes User Session Manager.""" - return self.software_manager.software.get("UserSessionManager") # noqa - - @property - def terminal(self) -> Optional[Terminal]: - """The Nodes Terminal.""" - return self.software_manager.software.get("Terminal") - - def local_login(self, username: str, password: str) -> Optional[str]: - """ - Attempt to log in to the node uas a local user. - - This method attempts to authenticate a local user with the given username and password. If successful, it - returns a session token. If authentication fails, it returns None. - - :param username: The username of the account attempting to log in. - :param password: The password of the account attempting to log in. - :return: A session token if the login is successful, otherwise None. - """ - return self.user_session_manager.local_login(username, password) - - def local_logout(self) -> None: - """ - Log out the current local user from the node. - - This method ends the current local user's session and invalidates the session token. - """ - return self.user_session_manager.local_logout() - - def ip_is_network_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool: - """ - Checks if a given IP address belongs to any of the nodes interfaces. - - :param ip_address: The IP address to check. - :param enabled_only: If True, only considers enabled network interfaces. - :return: True if the IP address is assigned to one of the nodes interfaces; False otherwise. - """ - for network_interface in self.network_interface.values(): - if not hasattr(network_interface, "ip_address"): - continue - if network_interface.ip_address == ip_address: - if enabled_only: - return network_interface.enabled - else: - return True - return False - - def setup_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - super().setup_for_episode(episode=episode) - - # Reset File System - self.file_system.setup_for_episode(episode=episode) - - # Reset all Nics - for network_interface in self.network_interfaces.values(): - network_interface.setup_for_episode(episode=episode) - - for software in self.software_manager.software.values(): - software.setup_for_episode(episode=episode) - - if episode and self.sys_log: - self.sys_log.current_episode = episode - self.sys_log.setup_logger() - - class _NodeIsOnValidator(RequestPermissionValidator): - """ - When requests come in, this validator will only let them through if the node is on. - - This is useful because no actions should be being resolved if the node is off. - """ - - node: Node - """Save a reference to the node instance.""" - - def __call__(self, request: RequestFormat, context: Dict) -> bool: - """Return whether the node is on or off.""" - return self.node.operating_state == NodeOperatingState.ON - - @property - def fail_message(self) -> str: - """Message that is reported when a request is rejected by this validator.""" - return f"Cannot perform request on node '{self.node.hostname}' because it is not powered on." - - class _NodeIsOffValidator(RequestPermissionValidator): - """ - When requests come in, this validator will only let them through if the node is off. - - This is useful because some actions require the node to be in an off state. - """ - - node: Node - """Save a reference to the node instance.""" - - def __call__(self, request: RequestFormat, context: Dict) -> bool: - """Return whether the node is on or off.""" - return self.node.operating_state == NodeOperatingState.OFF - - @property - def fail_message(self) -> str: - """Message that is reported when a request is rejected by this validator.""" - return f"Cannot perform request on node '{self.node.hostname}' because it is not turned off." - - def _init_request_manager(self) -> RequestManager: - """ - Initialise the request manager. - - More information in user guide and docstring for SimComponent._init_request_manager. - """ - - def _install_application(request: RequestFormat, context: Dict) -> RequestResponse: - """ - Allows agents to install applications to the node. - - :param request: list containing the application name as the only element - :type request: RequestFormat - :param context: additional context for resolving this action, currently unused - :type context: dict - :return: Request response with a success code if the application was installed. - :rtype: RequestResponse - """ - application_name = request[0] - if self.software_manager.software.get(application_name): - self.sys_log.warning(f"Can't install {application_name}. It's already installed.") - return RequestResponse(status="success", data={"reason": "already installed"}) - application_class = Application._registry[application_name] - self.software_manager.install(application_class) - application_instance = self.software_manager.software.get(application_name) - self.applications[application_instance.uuid] = application_instance - _LOGGER.debug(f"Added application {application_instance.name} to node {self.hostname}") - self._application_request_manager.add_request( - application_name, RequestType(func=application_instance._request_manager) - ) - application_instance.install() - if application_name in self.software_manager.software: - return RequestResponse.from_bool(True) - else: - return RequestResponse.from_bool(False) - - def _uninstall_application(request: RequestFormat, context: Dict) -> RequestResponse: - """ - Uninstall and completely remove application from this node. - - This method is useful for allowing agents to take this action. - - :param request: list containing the application name as the only element - :type request: RequestFormat - :param context: additional context for resolving this action, currently unused - :type context: dict - :return: Request response with a success code if the application was uninstalled. - :rtype: RequestResponse - """ - application_name = request[0] - if application_name not in self.software_manager.software: - self.sys_log.warning(f"Can't uninstall {application_name}. It's not installed.") - return RequestResponse.from_bool(False) - - application_instance = self.software_manager.software.get(application_name) - self.software_manager.uninstall(application_instance.name) - if application_instance.name not in self.software_manager.software: - return RequestResponse.from_bool(True) - else: - return RequestResponse.from_bool(False) - - _node_is_on = Node._NodeIsOnValidator(node=self) - _node_is_off = Node._NodeIsOffValidator(node=self) - - rm = super()._init_request_manager() - # since there are potentially many services, create an request manager that can map service name - self._service_request_manager = RequestManager() - rm.add_request("service", RequestType(func=self._service_request_manager, validator=_node_is_on)) - self._nic_request_manager = RequestManager() - rm.add_request("network_interface", RequestType(func=self._nic_request_manager, validator=_node_is_on)) - - rm.add_request("file_system", RequestType(func=self.file_system._request_manager, validator=_node_is_on)) - - # currently we don't have any applications nor processes, so these will be empty - self._process_request_manager = RequestManager() - rm.add_request("process", RequestType(func=self._process_request_manager, validator=_node_is_on)) - self._application_request_manager = RequestManager() - rm.add_request("application", RequestType(func=self._application_request_manager, validator=_node_is_on)) - - rm.add_request( - "scan", - RequestType( - func=lambda request, context: RequestResponse.from_bool(self.reveal_to_red()), validator=_node_is_on - ), - ) - - rm.add_request( - "shutdown", - RequestType( - func=lambda request, context: RequestResponse.from_bool(self.power_off()), validator=_node_is_on - ), - ) - rm.add_request( - "startup", - RequestType( - func=lambda request, context: RequestResponse.from_bool(self.power_on()), validator=_node_is_off - ), - ) - rm.add_request( - "reset", - RequestType(func=lambda request, context: RequestResponse.from_bool(self.reset()), validator=_node_is_on), - ) # TODO implement node reset - rm.add_request( - "logon", RequestType(func=lambda request, context: RequestResponse.from_bool(False), validator=_node_is_on) - ) # TODO implement logon request - rm.add_request( - "logoff", RequestType(func=lambda request, context: RequestResponse.from_bool(False), validator=_node_is_on) - ) # TODO implement logoff request - - self._os_request_manager = RequestManager() - self._os_request_manager.add_request( - "scan", - RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()), validator=_node_is_on), - ) - rm.add_request("os", RequestType(func=self._os_request_manager, validator=_node_is_on)) - - self._software_request_manager = RequestManager() - rm.add_request("software_manager", RequestType(func=self._software_request_manager, validator=_node_is_on)) - self._application_manager = RequestManager() - self._software_request_manager.add_request( - name="application", request_type=RequestType(func=self._application_manager) - ) - - self._application_manager.add_request(name="install", request_type=RequestType(func=_install_application)) - self._application_manager.add_request(name="uninstall", request_type=RequestType(func=_uninstall_application)) - - return rm - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - - :return: Current state of this object and child objects. - :rtype: Dict - """ - state = super().describe_state() - state.update( - { - "hostname": self.hostname, - "operating_state": self.operating_state.value, - "NICs": { - eth_num: network_interface.describe_state() - for eth_num, network_interface in self.network_interface.items() - }, - "file_system": self.file_system.describe_state(), - "applications": {app.name: app.describe_state() for app in self.applications.values()}, - "services": {svc.name: svc.describe_state() for svc in self.services.values()}, - "process": {proc.name: proc.describe_state() for proc in self.processes.values()}, - "revealed_to_red": self.revealed_to_red, - } - ) - return state - - def show(self, markdown: bool = False): - """Show function that calls both show NIC and show open ports.""" - self.show_nic(markdown) - self.show_open_ports(markdown) - - def show_open_ports(self, markdown: bool = False): - """Prints a table of the open ports on the Node.""" - table = PrettyTable(["Port"]) - if markdown: - table.set_style(MARKDOWN) - table.align = "l" - table.title = f"{self.hostname} Open Ports" - for port in self.software_manager.get_open_ports(): - if port > 0: - table.add_row([port]) - print(table.get_string(sortby="Port")) - - @property - def has_enabled_network_interface(self) -> bool: - """ - Checks if the node has at least one enabled network interface. - - Iterates through all network interfaces associated with the node to determine if at least one is enabled. This - property is essential for determining the node's ability to communicate within the network. - - :return: True if there is at least one enabled network interface; otherwise, False. - """ - for network_interface in self.network_interfaces.values(): - if network_interface.enabled: - return True - return False - - def show_nic(self, markdown: bool = False): - """Prints a table of the NICs on the Node.""" - table = PrettyTable(["Port", "Type", "MAC Address", "Address", "Speed", "Status", "NMNE"]) - if markdown: - table.set_style(MARKDOWN) - table.align = "l" - table.title = f"{self.hostname} Network Interface Cards" - for port, network_interface in self.network_interface.items(): - ip_address = "" - if hasattr(network_interface, "ip_address"): - ip_address = f"{network_interface.ip_address}/{network_interface.ip_network.prefixlen}" - table.add_row( - [ - port, - network_interface.__class__.__name__, - network_interface.mac_address, - ip_address, - network_interface.speed, - "Enabled" if network_interface.enabled else "Disabled", - network_interface.nmne if network_interface.nmne_config.capture_nmne else "Disabled", - ] - ) - print(table) - - def apply_timestep(self, timestep: int): - """ - Apply a single timestep of simulation dynamics to this node. - - In this instance, if any multi-timestep processes are currently occurring - (such as starting up or shutting down), then they are brought one step closer to - being finished. - - :param timestep: The current timestep number. (Amount of time since simulation episode began) - :type timestep: int - """ - super().apply_timestep(timestep=timestep) - - for network_interface in self.network_interfaces.values(): - network_interface.apply_timestep(timestep=timestep) - - # count down to boot up - if self.start_up_countdown > 0: - self.start_up_countdown -= 1 - else: - if self.operating_state == NodeOperatingState.BOOTING: - self.operating_state = NodeOperatingState.ON - self.sys_log.info(f"{self.hostname}: Turned on") - for network_interface in self.network_interfaces.values(): - network_interface.enable() - - self._start_up_actions() - - # count down to shut down - if self.shut_down_countdown > 0: - self.shut_down_countdown -= 1 - else: - if self.operating_state == NodeOperatingState.SHUTTING_DOWN: - self.operating_state = NodeOperatingState.OFF - self.sys_log.info(f"{self.hostname}: Turned off") - self._shut_down_actions() - - # if resetting turn back on - if self.is_resetting: - self.is_resetting = False - self.power_on() - - # time steps which require the node to be on - if self.operating_state == NodeOperatingState.ON: - # node scanning - if self.node_scan_countdown > 0: - self.node_scan_countdown -= 1 - - if self.node_scan_countdown == 0: - # scan everything! - for process_id in self.processes: - self.processes[process_id].scan() - - # scan services - for service_id in self.services: - self.services[service_id].scan() - - # scan applications - for application_id in self.applications: - self.applications[application_id].scan() - - # scan file system - self.file_system.scan(instant_scan=True) - - if self.red_scan_countdown > 0: - self.red_scan_countdown -= 1 - - if self.red_scan_countdown == 0: - # scan processes - for process_id in self.processes: - self.processes[process_id].reveal_to_red() - - # scan services - for service_id in self.services: - self.services[service_id].reveal_to_red() - - # scan applications - for application_id in self.applications: - self.applications[application_id].reveal_to_red() - - # scan file system - self.file_system.reveal_to_red(instant_scan=True) - - for process_id in self.processes: - self.processes[process_id].apply_timestep(timestep=timestep) - - for service_id in self.services: - self.services[service_id].apply_timestep(timestep=timestep) - - for application_id in self.applications: - self.applications[application_id].apply_timestep(timestep=timestep) - - self.file_system.apply_timestep(timestep=timestep) - - def pre_timestep(self, timestep: int) -> None: - """Apply pre-timestep logic.""" - super().pre_timestep(timestep) - for network_interface in self.network_interfaces.values(): - network_interface.pre_timestep(timestep=timestep) - - for process_id in self.processes: - self.processes[process_id].pre_timestep(timestep=timestep) - - for service_id in self.services: - self.services[service_id].pre_timestep(timestep=timestep) - - for application_id in self.applications: - self.applications[application_id].pre_timestep(timestep=timestep) - - self.file_system.pre_timestep(timestep=timestep) - - def scan(self) -> bool: - """ - Scan the node and all the items within it. - - Scans the: - - Processes - - Services - - Applications - - Folders - - Files - - to the red agent. - """ - self.node_scan_countdown = self.node_scan_duration - return True - - def reveal_to_red(self) -> bool: - """ - Reveals the node and all the items within it to the red agent. - - Set all the: - - Processes - - Services - - Applications - - Folders - - Files - - `revealed_to_red` to `True`. - """ - self.red_scan_countdown = self.node_scan_duration - return True - - def power_on(self) -> bool: - """Power on the Node, enabling its NICs if it is in the OFF state.""" - if self.start_up_duration <= 0: - self.operating_state = NodeOperatingState.ON - self._start_up_actions() - self.sys_log.info("Power on") - for network_interface in self.network_interfaces.values(): - network_interface.enable() - return True - if self.operating_state == NodeOperatingState.OFF: - self.operating_state = NodeOperatingState.BOOTING - self.start_up_countdown = self.start_up_duration - return True - - return False - - def power_off(self) -> bool: - """Power off the Node, disabling its NICs if it is in the ON state.""" - if self.shut_down_duration <= 0: - self._shut_down_actions() - self.operating_state = NodeOperatingState.OFF - self.sys_log.info("Power off") - return True - if self.operating_state == NodeOperatingState.ON: - for network_interface in self.network_interfaces.values(): - network_interface.disable() - self.operating_state = NodeOperatingState.SHUTTING_DOWN - self.shut_down_countdown = self.shut_down_duration - return True - return False - - def reset(self) -> bool: - """ - Resets the node. - - Powers off the node and sets is_resetting to True. - Applying more timesteps will eventually turn the node back on. - """ - if self.operating_state.ON: - self.is_resetting = True - self.sys_log.info("Resetting") - self.power_off() - return True - return False - - def connect_nic(self, network_interface: NetworkInterface, port_name: Optional[str] = None): - """ - Connect a Network Interface to the node. - - :param network_interface: The NIC to connect. - :raise NetworkError: If the NIC is already connected. - """ - if network_interface.uuid not in self.network_interface: - self.network_interfaces[network_interface.uuid] = network_interface - new_nic_num = len(self.network_interfaces) - self.network_interface[new_nic_num] = network_interface - network_interface._connected_node = self - network_interface.port_num = new_nic_num - if port_name: - network_interface.port_name = port_name - network_interface.parent = self - self.sys_log.info(f"Connected Network Interface {network_interface}") - if self.operating_state == NodeOperatingState.ON: - network_interface.enable() - self._nic_request_manager.add_request(new_nic_num, RequestType(func=network_interface._request_manager)) - else: - msg = f"Cannot connect NIC {network_interface} as it is already connected" - self.sys_log.logger.warning(msg) - raise NetworkError(msg) - - def disconnect_nic(self, network_interface: Union[NetworkInterface, str]): - """ - Disconnect a NIC (Network Interface Card) from the node. - - :param network_interface: The NIC to Disconnect, or its UUID. - :raise NetworkError: If the NIC is not connected. - """ - if isinstance(network_interface, str): - network_interface = self.network_interfaces.get(network_interface) - if network_interface or network_interface.uuid in self.network_interfaces: - network_interface_num = -1 - for port, _network_interface in self.network_interface.items(): - if network_interface == _network_interface: - self.network_interface.pop(port) - network_interface_num = port - break - self.network_interfaces.pop(network_interface.uuid) - network_interface.parent = None - network_interface.disable() - self.sys_log.info(f"Disconnected Network Interface {network_interface}") - if network_interface_num != -1: - self._nic_request_manager.remove_request(network_interface_num) - else: - msg = f"Cannot disconnect Network Interface {network_interface} as it is not connected" - self.sys_log.logger.warning(msg) - raise NetworkError(msg) - - def ping(self, target_ip_address: Union[IPv4Address, str], pings: int = 4) -> bool: - """ - Ping an IP address, performing a standard ICMP echo request/response. - - :param target_ip_address: The target IP address to ping. - :param pings: The number of pings to attempt, default is 4. - :return: True if the ping is successful, otherwise False. - """ - if not isinstance(target_ip_address, IPv4Address): - target_ip_address = IPv4Address(target_ip_address) - if self.software_manager.icmp: - return self.software_manager.icmp.ping(target_ip_address, pings) - return False - - @abstractmethod - def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): - """ - Receive a Frame from the connected NIC and process it. - - This is an abstract implementation of receive_frame with some very basic functionality (ARP population). All - Node subclasses should have their own implementation of receive_frame that first calls super().receive_frame( - ) before implementing its own internal receive_frame logic. - - :param frame: The Frame being received. - :param from_network_interface: The Network Interface that received the frame. - """ - if self.operating_state == NodeOperatingState.ON: - if frame.ip: - if self.software_manager.arp: - self.software_manager.arp.add_arp_cache_entry( - ip_address=frame.ip.src_ip_address, - mac_address=frame.ethernet.src_mac_addr, - network_interface=from_network_interface, - ) - else: - return - - def _shut_down_actions(self): - """Actions to perform when the node is shut down.""" - # Turn off all the services in the node - for service_id in self.services: - self.services[service_id].stop() - - # Turn off all the applications in the node - for app_id in self.applications: - self.applications[app_id].close() - - # Turn off all processes in the node - # for process_id in self.processes: - # self.processes[process_id] - - def _start_up_actions(self): - """Actions to perform when the node is starting up.""" - # Turn on all the services in the node - for service_id in self.services: - self.services[service_id].start() - - # Turn on all the applications in the node - for app_id in self.applications: - self.applications[app_id].run() - - # Turn off all processes in the node - # for process_id in self.processes: - # self.processes[process_id] - - def _install_system_software(self) -> None: - """Preinstall required software.""" - for _, software_class in self.SYSTEM_SOFTWARE.items(): - self.software_manager.install(software_class) - - def __contains__(self, item: Any) -> bool: - if isinstance(item, Service): - return item.uuid in self.services - elif isinstance(item, Application): - return item.uuid in self.applications - return None +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/__init__.py b/src/primaite/simulator/network/hardware/network_interface/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/network_interface/__init__.py +++ b/src/primaite/simulator/network/hardware/network_interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py index a9a31768..3997872c 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from primaite.simulator.network.hardware.base import ( diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py index eebaedc5..9bc4cd6f 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from primaite.simulator.network.hardware.base import ( diff --git a/src/primaite/simulator/network/hardware/node_operating_state.py b/src/primaite/simulator/network/hardware/node_operating_state.py index e64ef08b..8771cb84 100644 --- a/src/primaite/simulator/network/hardware/node_operating_state.py +++ b/src/primaite/simulator/network/hardware/node_operating_state.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum diff --git a/src/primaite/simulator/network/hardware/nodes/__init__.py b/src/primaite/simulator/network/hardware/nodes/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/nodes/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/host/__init__.py b/src/primaite/simulator/network/hardware/nodes/host/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/host/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 4253d15c..11b925b9 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 0c309136..c51afbca 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index bf1ef39b..e16cfd8f 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/src/primaite/simulator/network/hardware/nodes/network/__init__.py b/src/primaite/simulator/network/hardware/nodes/network/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 84cf8530..f1ca4930 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Final, Union diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index a5b8544f..22ff2b28 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from typing import Optional diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index e921faff..4a049f99 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import secrets diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index d29152a4..db923f1a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, Optional diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index aed314d2..804a570e 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, Optional, Union diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 2c3c15b4..c840748e 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import yaml diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index c9cff5de..a2e5f1fe 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List from pydantic import BaseModel, ConfigDict diff --git a/src/primaite/simulator/network/protocols/__init__.py b/src/primaite/simulator/network/protocols/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/protocols/__init__.py +++ b/src/primaite/simulator/network/protocols/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/protocols/arp.py b/src/primaite/simulator/network/protocols/arp.py index 9e7f7ebe..86e461d0 100644 --- a/src/primaite/simulator/network/protocols/arp.py +++ b/src/primaite/simulator/network/protocols/arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/protocols/dns.py b/src/primaite/simulator/network/protocols/dns.py index eb7b74ad..c0fed1aa 100644 --- a/src/primaite/simulator/network/protocols/dns.py +++ b/src/primaite/simulator/network/protocols/dns.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py index c570a634..fd8fdd2b 100644 --- a/src/primaite/simulator/network/protocols/ftp.py +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Any, Optional, Union diff --git a/src/primaite/simulator/network/protocols/http.py b/src/primaite/simulator/network/protocols/http.py index 5390cd26..54abdd98 100644 --- a/src/primaite/simulator/network/protocols/http.py +++ b/src/primaite/simulator/network/protocols/http.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum, IntEnum from primaite.simulator.network.protocols.packet import DataPacket diff --git a/src/primaite/simulator/network/protocols/icmp.py b/src/primaite/simulator/network/protocols/icmp.py index 9f0626f0..fcbe15da 100644 --- a/src/primaite/simulator/network/protocols/icmp.py +++ b/src/primaite/simulator/network/protocols/icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import secrets from enum import Enum from typing import Union diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index 5c5f03b2..e0ed26b7 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Optional diff --git a/src/primaite/simulator/network/protocols/ntp.py b/src/primaite/simulator/network/protocols/ntp.py index 74e02dab..c9b6f877 100644 --- a/src/primaite/simulator/network/protocols/ntp.py +++ b/src/primaite/simulator/network/protocols/ntp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from datetime import datetime diff --git a/src/primaite/simulator/network/protocols/packet.py b/src/primaite/simulator/network/protocols/packet.py index 7eeec13b..6f28f716 100644 --- a/src/primaite/simulator/network/protocols/packet.py +++ b/src/primaite/simulator/network/protocols/packet.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any from pydantic import BaseModel diff --git a/src/primaite/simulator/network/protocols/ssh.py b/src/primaite/simulator/network/protocols/ssh.py index be7f842f..03411fb5 100644 --- a/src/primaite/simulator/network/protocols/ssh.py +++ b/src/primaite/simulator/network/protocols/ssh.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import IntEnum from typing import Optional diff --git a/src/primaite/simulator/network/transmission/__init__.py b/src/primaite/simulator/network/transmission/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/transmission/__init__.py +++ b/src/primaite/simulator/network/transmission/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index 259d62e3..e7c2a124 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from datetime import datetime from typing import Any, Optional diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index 49dcd1f5..7a6b34c9 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from pydantic import BaseModel diff --git a/src/primaite/simulator/network/transmission/primaite_layer.py b/src/primaite/simulator/network/transmission/primaite_layer.py index 981b6fbc..8ff4ac02 100644 --- a/src/primaite/simulator/network/transmission/primaite_layer.py +++ b/src/primaite/simulator/network/transmission/primaite_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from pydantic import BaseModel diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index 10cf802c..689eea2f 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import List diff --git a/src/primaite/simulator/network/utils.py b/src/primaite/simulator/network/utils.py index 4fd1834a..b4d6c815 100644 --- a/src/primaite/simulator/network/utils.py +++ b/src/primaite/simulator/network/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Union diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 809b52db..2a1deef4 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from primaite.interface.request import RequestResponse diff --git a/src/primaite/simulator/system/__init__.py b/src/primaite/simulator/system/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/__init__.py +++ b/src/primaite/simulator/system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/__init__.py b/src/primaite/simulator/system/applications/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/applications/__init__.py +++ b/src/primaite/simulator/system/applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index a7871315..1752c09a 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index cd4b2a03..840214f3 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index e2b9117d..f064eae3 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, Final, List, Optional, Set, Tuple, Union diff --git a/src/primaite/simulator/system/applications/red_applications/__init__.py b/src/primaite/simulator/system/applications/red_applications/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/applications/red_applications/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index 60e39743..33cc555f 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Optional, Union from pydantic import BaseModel, Field, field_validator, ValidationInfo diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index f77bc33a..4cd54d69 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from enum import Enum from ipaddress import IPv4Address diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index c0c3d872..b25eea6e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index f948d696..654b86e7 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 9fdbae57..0423087e 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index fb2c8847..99c4acb3 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 93b4c50d..3a8ac5ae 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index c57a9bd3..ff185e2a 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address from typing import Dict, List, Optional diff --git a/src/primaite/simulator/system/core/__init__.py b/src/primaite/simulator/system/core/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/core/__init__.py +++ b/src/primaite/simulator/system/core/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index ea8b00a5..813c288e 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import logging from pathlib import Path diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 75322e86..48f1f383 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address, IPv4Network diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 04a3e3fb..f0ee6f7c 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from copy import deepcopy from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union diff --git a/src/primaite/simulator/system/core/sys_log.py b/src/primaite/simulator/system/core/sys_log.py index 9e22696d..741e5d33 100644 --- a/src/primaite/simulator/system/core/sys_log.py +++ b/src/primaite/simulator/system/core/sys_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import logging from pathlib import Path diff --git a/src/primaite/simulator/system/processes/__init__.py b/src/primaite/simulator/system/processes/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/processes/__init__.py +++ b/src/primaite/simulator/system/processes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index 225505c8..ad2babc1 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from enum import Enum from typing import Dict diff --git a/src/primaite/simulator/system/services/__init__.py b/src/primaite/simulator/system/services/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/__init__.py +++ b/src/primaite/simulator/system/services/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/__init__.py b/src/primaite/simulator/system/services/access/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/access/__init__.py +++ b/src/primaite/simulator/system/services/access/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_manager.py b/src/primaite/simulator/system/services/access/user_manager.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/access/user_manager.py +++ b/src/primaite/simulator/system/services/access/user_manager.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_session_manager.py b/src/primaite/simulator/system/services/access/user_session_manager.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/access/user_session_manager.py +++ b/src/primaite/simulator/system/services/access/user_session_manager.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/arp/__init__.py b/src/primaite/simulator/system/services/arp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/arp/__init__.py +++ b/src/primaite/simulator/system/services/arp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 816eb99e..31938e83 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/database/__init__.py b/src/primaite/simulator/system/services/database/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/database/__init__.py +++ b/src/primaite/simulator/system/services/database/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index b7cd8886..3a5f5b31 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 diff --git a/src/primaite/simulator/system/services/dns/__init__.py b/src/primaite/simulator/system/services/dns/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/dns/__init__.py +++ b/src/primaite/simulator/system/services/dns/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 78642fa6..02cf54ae 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 5b380320..b7c9a42c 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, Optional diff --git a/src/primaite/simulator/system/services/ftp/__init__.py b/src/primaite/simulator/system/services/ftp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/ftp/__init__.py +++ b/src/primaite/simulator/system/services/ftp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 00b70332..836b79af 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,338 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from ipaddress import IPv4Address -from typing import Dict, Optional - -from primaite import getLogger -from primaite.interface.request import RequestFormat, RequestResponse -from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.file_system.file_system import File -from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.system.core.software_manager import SoftwareManager -from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP -from primaite.utils.validation.port import Port, PORT_LOOKUP - -_LOGGER = getLogger(__name__) - - -class FTPClient(FTPServiceABC): - """ - A class for simulating an FTP client service. - - This class inherits from the `Service` class and provides methods to emulate FTP - RFC 959: https://datatracker.ietf.org/doc/html/rfc959 - """ - - def __init__(self, **kwargs): - kwargs["name"] = "FTPClient" - kwargs["port"] = PORT_LOOKUP["FTP"] - kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] - super().__init__(**kwargs) - self.start() - - def _init_request_manager(self) -> RequestManager: - """ - Initialise the request manager. - - More information in user guide and docstring for SimComponent._init_request_manager. - """ - rm = super()._init_request_manager() - - def _send_data_request(request: RequestFormat, context: Dict) -> RequestResponse: - """ - Request for sending data via the ftp_client using the request options parameters. - - :param request: Request with one element containing a dict of parameters for the send method. - :type request: RequestFormat - :param context: additional context for resolving this action, currently unused - :type context: dict - :return: RequestResponse object with a success code reflecting whether the configuration could be applied. - :rtype: RequestResponse - """ - dest_ip = request[-1].get("dest_ip_address") - dest_ip = None if dest_ip is None else IPv4Address(dest_ip) - - # Missing FTP Options results is an automatic failure. - src_folder = request[-1].get("src_folder_name", None) - src_file_name = request[-1].get("src_file_name", None) - dest_folder = request[-1].get("dest_folder_name", None) - dest_file_name = request[-1].get("dest_file_name", None) - - if not self.file_system.access_file(folder_name=src_folder, file_name=src_file_name): - self.sys_log.debug( - f"{self.name}: Received a FTP Request to transfer file: {src_file_name} to Remote IP: {dest_ip}." - ) - return RequestResponse( - status="failure", - data={ - "reason": "Unable to locate given file on local file system. Perhaps given options are invalid?" - }, - ) - - return RequestResponse.from_bool( - self.send_file( - dest_ip_address=dest_ip, - src_folder_name=src_folder, - src_file_name=src_file_name, - dest_folder_name=dest_folder, - dest_file_name=dest_file_name, - ) - ) - - rm.add_request("send", request_type=RequestType(func=_send_data_request)), - return rm - - def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: - """ - Process the command in the FTP Packet. - - :param: payload: The FTP Packet to process - :type: payload: FTPPacket - :param: session_id: session ID linked to the FTP Packet. Optional. - :type: session_id: Optional[str] - """ - # if client service is down, return error - if not self._can_perform_action(): - payload.status_code = FTPStatusCode.ERROR - return payload - - self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}") - - # process client specific commands, otherwise call super - return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) - - def _connect_to_server( - self, - dest_ip_address: Optional[IPv4Address] = None, - dest_port: Optional[Port] = PORT_LOOKUP["FTP"], - session_id: Optional[str] = None, - is_reattempt: Optional[bool] = False, - ) -> bool: - """ - Connects the client to a given FTP server. - - :param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional. - :type: dest_ip_address: Optional[IPv4Address] - :param: dest_port: Port of the FTP server the client needs to connect to. Optional. - :type: dest_port: Optional[Port] - :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False. - :type: is_reattempt: Optional[bool] - """ - # make sure the service is running before attempting - if not self._can_perform_action(): - return False - - # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now - # create FTP packet - payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=PORT_LOOKUP["FTP"]) - - if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id): - if payload.status_code == FTPStatusCode.OK: - self.sys_log.info( - f"{self.name}: Successfully connected to FTP Server " - f"{dest_ip_address} via port {payload.ftp_command_args}" - ) - self.add_connection(connection_id="server_connection", session_id=session_id) - return True - else: - if is_reattempt: - # reattempt failed - self.sys_log.warning( - f"{self.name}: Unable to connect to FTP Server " - f"{dest_ip_address} via port {payload.ftp_command_args}" - ) - return False - else: - # try again - self._connect_to_server( - dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, is_reattempt=True - ) - else: - self.sys_log.warning(f"{self.name}: Unable to send FTPPacket") - return False - - def _disconnect_from_server( - self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = PORT_LOOKUP["FTP"] - ) -> bool: - """ - Connects the client from a given FTP server. - - :param: dest_ip_address: IP address of the FTP server the client needs to disconnect from. Optional. - :type: dest_ip_address: Optional[IPv4Address] - :param: dest_port: Port of the FTP server the client needs to disconnect from. Optional. - :type: dest_port: Optional[Port] - :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. - :type: is_reattempt: Optional[bool] - """ - # send a disconnect request payload to FTP server - payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port - ) - return payload.status_code == FTPStatusCode.OK - - def send_file( - self, - dest_ip_address: IPv4Address, - src_folder_name: str, - src_file_name: str, - dest_folder_name: str, - dest_file_name: str, - dest_port: Optional[Port] = PORT_LOOKUP["FTP"], - session_id: Optional[str] = None, - ) -> bool: - """ - Send a file to a target IP address. - - The function checks if the file exists in the FTP Client host. - The STOR command is then sent to the FTP Server. - - :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. - :type: dest_ip_address: IPv4Address - - :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. - :type: src_folder_name: str - - :param: src_file_name: The name of the file to send to the FTP Server. - :type: src_file_name: str - - :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. - :type: dest_folder_name: str - - :param: dest_file_name: The name of the file to be saved on the FTP Server. - :type: dest_file_name: str - - :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. - :type: dest_port: Optional[Port] - - :param: session_id: The id of the session - :type: session_id: Optional[str] - """ - # check if the file to transfer exists on the client - file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) - if not file_to_transfer: - self.sys_log.warning(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}") - return False - - # check if FTP is currently connected to IP - self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) - - if not len(self.connections): - return False - else: - self.sys_log.info(f"Sending file {src_folder_name}/{src_file_name} to {str(dest_ip_address)}") - # send STOR request - if self._send_data( - file=file_to_transfer, - dest_folder_name=dest_folder_name, - dest_file_name=dest_file_name, - dest_ip_address=dest_ip_address, - dest_port=dest_port, - ): - return self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) - - return False - - def request_file( - self, - dest_ip_address: IPv4Address, - src_folder_name: str, - src_file_name: str, - dest_folder_name: str, - dest_file_name: str, - dest_port: Optional[Port] = PORT_LOOKUP["FTP"], - ) -> bool: - """ - Request a file from a target IP address. - - Sends a RETR command to the FTP Server. - - :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. - :type: dest_ip_address: IPv4Address - - :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. - :type: src_folder_name: str - - :param: src_file_name: The name of the file to send to the FTP Server. - :type: src_file_name: str - - :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. - :type: dest_folder_name: str - - :param: dest_file_name: The name of the file to be saved on the FTP Server. - :type: dest_file_name: str - - :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. - :type: dest_port: Optional[int] - """ - # check if FTP is currently connected to IP - self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) - - if not len(self.connections): - return False - else: - # send retrieve request - payload: FTPPacket = FTPPacket( - ftp_command=FTPCommand.RETR, - ftp_command_args={ - "src_folder_name": src_folder_name, - "src_file_name": src_file_name, - "dest_file_name": dest_file_name, - "dest_folder_name": dest_folder_name, - }, - ) - self.sys_log.info(f"Requesting file {src_folder_name}/{src_file_name} from {str(dest_ip_address)}") - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port - ) - - # the payload should have ok status code - if payload.status_code == FTPStatusCode.OK: - self.sys_log.info(f"{self.name}: File {src_folder_name}/{src_file_name} found in FTP server.") - return True - else: - self.sys_log.error(f"{self.name}: File {src_folder_name}/{src_file_name} does not exist in FTP server") - return False - - def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: - """ - Receives a payload from the SessionManager. - - :param: payload: FTPPacket payload. - :type: payload: FTPPacket - - :param: session_id: ID of the session. Optional. - :type: session_id: Optional[str] - """ - if not isinstance(payload, FTPPacket): - self.sys_log.warning(f"{self.name}: Payload is not an FTP packet") - self.sys_log.debug(f"{self.name}: {payload}") - return False - - """ - Ignore ftp payload if status code is None. - - This helps prevent an FTP request loop - FTP client and servers can exist on - the same node. - """ - if not self._can_perform_action(): - return False - - if payload.status_code is None: - self.sys_log.error(f"FTP Server could not be found - Error Code: {FTPStatusCode.NOT_FOUND.value}") - return False - - # if PORT succeeded, add the connection as an active connection list - if payload.ftp_command is FTPCommand.PORT and payload.status_code is FTPStatusCode.OK: - self.add_connection(connection_id=session_id, session_id=session_id) - - # if QUIT succeeded, remove the session from active connection list - if payload.ftp_command is FTPCommand.QUIT and payload.status_code is FTPStatusCode.OK: - self.terminate_connection(connection_id=session_id) - - self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}") - - self._process_ftp_command(payload=payload, session_id=session_id) - return True +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 671200f5..9ce7d658 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Optional from primaite import getLogger diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 77d82997..52f451e1 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import ABC from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/icmp/__init__.py b/src/primaite/simulator/system/services/icmp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/icmp/__init__.py +++ b/src/primaite/simulator/system/services/icmp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 84ad995d..933d0591 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import secrets from ipaddress import IPv4Address from typing import Any, Dict, Optional, Tuple, Union diff --git a/src/primaite/simulator/system/services/icmp/router_icmp.py b/src/primaite/simulator/system/services/icmp/router_icmp.py index 19c0ac2d..63fbd4b2 100644 --- a/src/primaite/simulator/system/services/icmp/router_icmp.py +++ b/src/primaite/simulator/system/services/icmp/router_icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # class RouterICMP(ICMP): # """ # A class to represent a router's Internet Control Message Protocol (ICMP) handler. diff --git a/src/primaite/simulator/system/services/ntp/__init__.py b/src/primaite/simulator/system/services/ntp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/ntp/__init__.py +++ b/src/primaite/simulator/system/services/ntp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index ed89971f..9606c61f 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from datetime import datetime from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index b674a296..6e73ccc6 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from datetime import datetime from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 4f0b879c..3dc080b4 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/terminal/__init__.py b/src/primaite/simulator/system/services/terminal/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/terminal/__init__.py +++ b/src/primaite/simulator/system/services/terminal/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index ae3557f7..836b79af 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -1,545 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from __future__ import annotations - -from abc import abstractmethod -from datetime import datetime -from ipaddress import IPv4Address -from typing import Any, Dict, List, Optional, Union -from uuid import uuid4 - -from pydantic import BaseModel - -from primaite.interface.request import RequestFormat, RequestResponse -from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.protocols.ssh import ( - SSHConnectionMessage, - SSHPacket, - SSHTransportMessage, - SSHUserCredentials, -) -from primaite.simulator.system.core.software_manager import SoftwareManager -from primaite.simulator.system.services.service import Service, ServiceOperatingState -from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP -from primaite.utils.validation.port import PORT_LOOKUP - - -# TODO 2824: Since remote terminal connections and remote user sessions are the same thing, we could refactor -# the terminal to leverage the user session manager's list. This way we avoid potential bugs and code ducplication -class TerminalClientConnection(BaseModel): - """ - TerminalClientConnection Class. - - This class is used to record current User Connections to the Terminal class. - """ - - parent_terminal: Terminal - """The parent Node that this connection was created on.""" - - ssh_session_id: str = None - """Session ID that connection is linked to, used for sending commands via session manager.""" - - connection_uuid: str = None - """Connection UUID""" - - connection_request_id: str = None - """Connection request ID""" - - time: datetime = None - """Timestamp connection was created.""" - - ip_address: IPv4Address - """Source IP of Connection""" - - is_active: bool = True - """Flag to state whether the connection is active or not""" - - def __str__(self) -> str: - return f"{self.__class__.__name__}(connection_id: '{self.connection_uuid}, ip_address: {self.ip_address}')" - - def __repr__(self) -> str: - return self.__str__() - - def __getitem__(self, key: Any) -> Any: - return getattr(self, key) - - @property - def client(self) -> Optional[Terminal]: - """The Terminal that holds this connection.""" - return self.parent_terminal - - def disconnect(self) -> bool: - """Disconnect the session.""" - return self.parent_terminal._disconnect(connection_uuid=self.connection_uuid) - - @abstractmethod - def execute(self, command: Any) -> bool: - """Execute a given command.""" - pass - - -class LocalTerminalConnection(TerminalClientConnection): - """ - LocalTerminalConnectionClass. - - This class represents a local terminal when connected. - """ - - ip_address: str = "Local Connection" - - def execute(self, command: Any) -> Optional[RequestResponse]: - """Execute a given command on local Terminal.""" - if self.parent_terminal.operating_state != ServiceOperatingState.RUNNING: - self.parent_terminal.sys_log.warning("Cannot process command as system not running") - return None - if not self.is_active: - self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") - return None - return self.parent_terminal.execute(command) - - -class RemoteTerminalConnection(TerminalClientConnection): - """ - RemoteTerminalConnection Class. - - This class acts as broker between the terminal and remote. - - """ - - def execute(self, command: Any) -> bool: - """Execute a given command on the remote Terminal.""" - if self.parent_terminal.operating_state != ServiceOperatingState.RUNNING: - self.parent_terminal.sys_log.warning("Cannot process command as system not running") - return False - if not self.is_active: - self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") - return False - # Send command to remote terminal to process. - - transport_message: SSHTransportMessage = SSHTransportMessage.SSH_MSG_SERVICE_REQUEST - connection_message: SSHConnectionMessage = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA - - payload: SSHPacket = SSHPacket( - transport_message=transport_message, - connection_message=connection_message, - connection_request_uuid=self.connection_request_id, - connection_uuid=self.connection_uuid, - ssh_command=command, - ) - - return self.parent_terminal.send(payload=payload, session_id=self.ssh_session_id) - - -class Terminal(Service): - """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" - - _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} - """Dictionary of connect requests made to remote nodes.""" - - def __init__(self, **kwargs): - kwargs["name"] = "Terminal" - kwargs["port"] = PORT_LOOKUP["SSH"] - kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] - super().__init__(**kwargs) - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - - :return: Current state of this object and child objects. - :rtype: Dict - """ - state = super().describe_state() - return state - - def show(self, markdown: bool = False): - """ - Display the remote connections to this terminal instance in tabular format. - - :param markdown: Whether to display the table in Markdown format or not. Default is `False`. - """ - self.show_connections(markdown=markdown) - - def _init_request_manager(self) -> RequestManager: - """Initialise Request manager.""" - rm = super()._init_request_manager() - - def _remote_login(request: RequestFormat, context: Dict) -> RequestResponse: - login = self._send_remote_login(username=request[0], password=request[1], ip_address=request[2]) - if login: - return RequestResponse( - status="success", - data={ - "ip_address": str(login.ip_address), - "username": request[0], - }, - ) - else: - return RequestResponse(status="failure", data={}) - - rm.add_request( - "ssh_to_remote", - request_type=RequestType(func=_remote_login), - ) - - def _remote_logoff(request: RequestFormat, context: Dict) -> RequestResponse: - """Logoff from remote connection.""" - ip_address = IPv4Address(request[0]) - remote_connection = self._get_connection_from_ip(ip_address=ip_address) - if remote_connection: - outcome = self._disconnect(remote_connection.connection_uuid) - if outcome: - return RequestResponse(status="success", data={}) - - return RequestResponse(status="failure", data={}) - - rm.add_request("remote_logoff", request_type=RequestType(func=_remote_logoff)) - - def remote_execute_request(request: RequestFormat, context: Dict) -> RequestResponse: - """Execute an instruction.""" - ip_address: IPv4Address = IPv4Address(request[0]) - command: str = request[1]["command"] - remote_connection = self._get_connection_from_ip(ip_address=ip_address) - if remote_connection: - outcome = remote_connection.execute(command) - if outcome: - return RequestResponse( - status="success", - data={}, - ) - else: - return RequestResponse( - status="failure", - data={}, - ) - - rm.add_request( - "send_remote_command", - request_type=RequestType(func=remote_execute_request), - ) - - return rm - - def execute(self, command: List[Any]) -> Optional[RequestResponse]: - """Execute a passed ssh command via the request manager.""" - return self.parent.apply_request(command) - - def _get_connection_from_ip(self, ip_address: IPv4Address) -> Optional[RemoteTerminalConnection]: - """Find Remote Terminal Connection from a given IP.""" - for connection in self._connections.values(): - if connection.ip_address == ip_address: - return connection - - def _create_local_connection(self, connection_uuid: str, session_id: str) -> TerminalClientConnection: - """Create a new connection object and amend to list of active connections. - - :param connection_uuid: Connection ID of the new local connection - :param session_id: Session ID of the new local connection - :return: TerminalClientConnection object - """ - new_connection = LocalTerminalConnection( - parent_terminal=self, - connection_uuid=connection_uuid, - ssh_session_id=session_id, - time=datetime.now(), - ) - self._connections[connection_uuid] = new_connection - self._client_connection_requests[connection_uuid] = new_connection - - return new_connection - - def login( - self, username: str, password: str, ip_address: Optional[IPv4Address] = None - ) -> Optional[TerminalClientConnection]: - """Login to the terminal. Will attempt a remote login if ip_address is given, else local. - - :param: username: Username used to connect to the remote node. - :type: username: str - - :param: password: Password used to connect to the remote node - :type: password: str - - :param: ip_address: Target Node IP address for login attempt. If None, login is assumed local. - :type: ip_address: Optional[IPv4Address] - """ - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.warning(f"{self.name}: Cannot login as service is not running.") - return None - if ip_address: - # Assuming that if IP is passed we are connecting to remote - return self._send_remote_login(username=username, password=password, ip_address=ip_address) - else: - return self._process_local_login(username=username, password=password) - - def _process_local_login(self, username: str, password: str) -> Optional[TerminalClientConnection]: - """Local session login to terminal. - - :param username: Username for login. - :param password: Password for login. - :return: boolean, True if successful, else False - """ - # TODO: Un-comment this when UserSessionManager is merged. - connection_uuid = self.parent.user_session_manager.local_login(username=username, password=password) - if connection_uuid: - self.sys_log.info(f"{self.name}: Login request authorised, connection uuid: {connection_uuid}") - # Add new local session to list of connections and return - return self._create_local_connection(connection_uuid=connection_uuid, session_id="Local_Connection") - else: - self.sys_log.warning(f"{self.name}: Login failed, incorrect Username or Password") - return None - - def _validate_client_connection_request(self, connection_id: str) -> bool: - """Check that client_connection_id is valid.""" - return connection_id in self._client_connection_requests - - def _check_client_connection(self, connection_id: str) -> bool: - """Check that client_connection_id is valid.""" - if not self.parent.user_session_manager.validate_remote_session_uuid(connection_id): - self._disconnect(connection_id) - return False - return connection_id in self._connections - - def _send_remote_login( - self, - username: str, - password: str, - ip_address: IPv4Address, - connection_request_id: Optional[str] = None, - is_reattempt: bool = False, - ) -> Optional[RemoteTerminalConnection]: - """Send a remote login attempt and connect to Node. - - :param: username: Username used to connect to the remote node. - :type: username: str - :param: password: Password used to connect to the remote node - :type: password: str - :param: ip_address: Target Node IP address for login attempt. - :type: ip_address: IPv4Address - :param: connection_request_id: Connection Request ID, if not provided, a new one is generated - :type: connection_request_id: Optional[str] - :param: is_reattempt: True if the request has been reattempted. Default False. - :type: is_reattempt: Optional[bool] - :return: RemoteTerminalConnection: Connection Object for sending further commands if successful, else False. - """ - connection_request_id = connection_request_id or str(uuid4()) - if is_reattempt: - valid_connection_request = self._validate_client_connection_request(connection_id=connection_request_id) - if valid_connection_request: - remote_terminal_connection = self._client_connection_requests.pop(connection_request_id) - if isinstance(remote_terminal_connection, RemoteTerminalConnection): - self.sys_log.info(f"{self.name}: Remote Connection to {ip_address} authorised.") - return remote_terminal_connection - else: - self.sys_log.warning(f"{self.name}: Connection request {connection_request_id} declined") - return None - else: - self.sys_log.warning(f"{self.name}: Remote connection to {ip_address} declined.") - return None - - self.sys_log.info( - f"{self.name}: Sending Remote login attempt to {ip_address}. Connection_id is {connection_request_id}" - ) - transport_message: SSHTransportMessage = SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST - connection_message: SSHConnectionMessage = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA - user_details: SSHUserCredentials = SSHUserCredentials(username=username, password=password) - - payload_contents = { - "type": "login_request", - "username": username, - "password": password, - "connection_request_id": connection_request_id, - } - - payload: SSHPacket = SSHPacket( - payload=payload_contents, - transport_message=transport_message, - connection_message=connection_message, - user_account=user_details, - connection_request_uuid=connection_request_id, - ) - - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=ip_address, dest_port=self.port - ) - return self._send_remote_login( - username=username, - password=password, - ip_address=ip_address, - is_reattempt=True, - connection_request_id=connection_request_id, - ) - - def _create_remote_connection( - self, connection_id: str, connection_request_id: str, session_id: str, source_ip: str - ) -> None: - """Create a new TerminalClientConnection Object. - - :param: connection_request_id: Connection Request ID - :type: connection_request_id: str - - :param: session_id: Session ID of connection. - :type: session_id: str - """ - client_connection = RemoteTerminalConnection( - parent_terminal=self, - ssh_session_id=session_id, - connection_uuid=connection_id, - ip_address=source_ip, - connection_request_id=connection_request_id, - time=datetime.now(), - ) - self._connections[connection_id] = client_connection - self._client_connection_requests[connection_request_id] = client_connection - - def receive(self, session_id: str, payload: Union[SSHPacket, Dict], **kwargs) -> bool: - """ - Receive a payload from the Software Manager. - - :param payload: A payload to receive. - :param session_id: The session id the payload relates to. - :return: True. - """ - source_ip = kwargs["frame"].ip.src_ip_address - self.sys_log.info(f"{self.name}: Received payload: {payload}. Source: {source_ip}") - if isinstance(payload, SSHPacket): - if payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST: - # validate & add connection - # TODO: uncomment this as part of 2781 - username = payload.user_account.username - password = payload.user_account.password - connection_id = self.parent.user_session_manager.remote_login( - username=username, password=password, remote_ip_address=source_ip - ) - if connection_id: - connection_request_id = payload.connection_request_uuid - self.sys_log.info(f"{self.name}: Connection authorised, session_id: {session_id}") - self._create_remote_connection( - connection_id=connection_id, - connection_request_id=connection_request_id, - session_id=session_id, - source_ip=source_ip, - ) - - transport_message = SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS - connection_message = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA - - payload_contents = { - "type": "login_success", - "username": username, - "password": password, - "connection_request_id": connection_request_id, - "connection_id": connection_id, - } - payload: SSHPacket = SSHPacket( - payload=payload_contents, - transport_message=transport_message, - connection_message=connection_message, - connection_request_uuid=connection_request_id, - connection_uuid=connection_id, - ) - - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload=payload, dest_port=self.port, session_id=session_id - ) - elif payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS: - self.sys_log.info(f"{self.name}: Login Successful") - self._create_remote_connection( - connection_id=payload.connection_uuid, - connection_request_id=payload.connection_request_uuid, - session_id=session_id, - source_ip=source_ip, - ) - - elif payload.transport_message == SSHTransportMessage.SSH_MSG_SERVICE_REQUEST: - # Requesting a command to be executed - self.sys_log.info(f"{self.name}: Received command to execute") - command = payload.ssh_command - valid_connection = self._check_client_connection(payload.connection_uuid) - if valid_connection: - remote_session = self.software_manager.node.user_session_manager.remote_sessions.get( - payload.connection_uuid - ) - remote_session.last_active_step = self.software_manager.node.user_session_manager.current_timestep - self.execute(command) - return True - else: - self.sys_log.error( - f"{self.name}: Connection UUID:{payload.connection_uuid} is not valid. Rejecting Command." - ) - - if isinstance(payload, dict) and payload.get("type"): - if payload["type"] == "disconnect": - connection_id = payload["connection_id"] - valid_id = self._check_client_connection(connection_id) - if valid_id: - self.sys_log.info(f"{self.name}: Received disconnect command for {connection_id=} from remote.") - self._disconnect(payload["connection_id"]) - self.parent.user_session_manager.remote_logout(remote_session_id=connection_id) - else: - self.sys_log.error(f"{self.name}: No Active connection held for received connection ID.") - - if payload["type"] == "user_timeout": - connection_id = payload["connection_id"] - valid_id = connection_id in self._connections - if valid_id: - connection = self._connections.pop(connection_id) - connection.is_active = False - self.sys_log.info(f"{self.name}: Connection {connection_id} disconnected due to inactivity.") - else: - self.sys_log.error(f"{self.name}: Connection {connection_id} is invalid.") - - return True - - def _disconnect(self, connection_uuid: str) -> bool: - """Disconnect connection. - - :param connection_uuid: Connection ID that we want to disconnect. - :return True if successful, False otherwise. - """ - # TODO: Handle the possibility of attempting to disconnect - if not self._connections: - self.sys_log.warning(f"{self.name}: No remote connection present") - return False - - connection = self._connections.pop(connection_uuid, None) - if not connection: - return False - connection.is_active = False - - if isinstance(connection, RemoteTerminalConnection): - # Send disconnect command via software manager - session_id = connection.ssh_session_id - - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload={"type": "disconnect", "connection_id": connection_uuid}, - dest_port=self.port, - session_id=session_id, - ) - self.sys_log.info(f"{self.name}: Disconnected {connection_uuid}") - return True - - elif isinstance(connection, LocalTerminalConnection): - self.parent.user_session_manager.local_logout() - return True - - def send( - self, payload: SSHPacket, dest_ip_address: Optional[IPv4Address] = None, session_id: Optional[str] = None - ) -> bool: - """ - Send a payload out from the Terminal. - - :param payload: The payload to be sent. - :param dest_up_address: The IP address of the payload destination. - """ - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.warning(f"{self.name}: Cannot send commands when Operating state is {self.operating_state}!") - return False - - self.sys_log.debug(f"{self.name}: Sending payload: {payload}") - return super().send( - payload=payload, dest_ip_address=dest_ip_address, dest_port=self.port, session_id=session_id - ) +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/web_server/__init__.py b/src/primaite/simulator/system/services/web_server/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/web_server/__init__.py +++ b/src/primaite/simulator/system/services/web_server/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 75d9c472..1aab374d 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Optional from urllib.parse import urlparse diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 6fb09a16..34c893eb 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from abc import abstractmethod from datetime import datetime diff --git a/src/primaite/utils/__init__.py b/src/primaite/utils/__init__.py index 4d7c430e..1dced372 100644 --- a/src/primaite/utils/__init__.py +++ b/src/primaite/utils/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Utilities for PrimAITE.""" diff --git a/src/primaite/utils/cli/__init__.py b/src/primaite/utils/cli/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/utils/cli/__init__.py +++ b/src/primaite/utils/cli/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/cli/dev_cli.py b/src/primaite/utils/cli/dev_cli.py index 8946a4ca..581cd0b1 100644 --- a/src/primaite/utils/cli/dev_cli.py +++ b/src/primaite/utils/cli/dev_cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import click import typer from rich import print diff --git a/src/primaite/utils/cli/primaite_config_utils.py b/src/primaite/utils/cli/primaite_config_utils.py index 635be5a7..1fefd0a4 100644 --- a/src/primaite/utils/cli/primaite_config_utils.py +++ b/src/primaite/utils/cli/primaite_config_utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, Optional import yaml diff --git a/src/primaite/utils/converters.py b/src/primaite/utils/converters.py index f803851d..95956448 100644 --- a/src/primaite/utils/converters.py +++ b/src/primaite/utils/converters.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Any, Dict diff --git a/src/primaite/utils/package_data.py b/src/primaite/utils/package_data.py index af0252f9..ed091dd0 100644 --- a/src/primaite/utils/package_data.py +++ b/src/primaite/utils/package_data.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import os from logging import Logger from pathlib import Path diff --git a/src/primaite/utils/session_metadata_parser.py b/src/primaite/utils/session_metadata_parser.py index f6594666..1a7345ea 100644 --- a/src/primaite/utils/session_metadata_parser.py +++ b/src/primaite/utils/session_metadata_parser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/session_output_reader.py b/src/primaite/utils/session_output_reader.py index b9ad68a1..f25bbe6a 100644 --- a/src/primaite/utils/session_output_reader.py +++ b/src/primaite/utils/session_output_reader.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/session_output_writer.py b/src/primaite/utils/session_output_writer.py index 75a97f60..a8cefe35 100644 --- a/src/primaite/utils/session_output_writer.py +++ b/src/primaite/utils/session_output_writer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/validation/__init__.py b/src/primaite/utils/validation/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/utils/validation/__init__.py +++ b/src/primaite/utils/validation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/validation/ip_protocol.py b/src/primaite/utils/validation/ip_protocol.py index 4e358305..654a5156 100644 --- a/src/primaite/utils/validation/ip_protocol.py +++ b/src/primaite/utils/validation/ip_protocol.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Define a custom IP protocol validator from typing import Any diff --git a/src/primaite/utils/validation/ipv4_address.py b/src/primaite/utils/validation/ipv4_address.py index eb0e2574..c385ed1e 100644 --- a/src/primaite/utils/validation/ipv4_address.py +++ b/src/primaite/utils/validation/ipv4_address.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address diff --git a/src/primaite/utils/validation/port.py b/src/primaite/utils/validation/port.py index 90c36add..564e843c 100644 --- a/src/primaite/utils/validation/port.py +++ b/src/primaite/utils/validation/port.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Define a custom port validator from typing import Any diff --git a/tests/__init__.py b/tests/__init__.py index 846ec808..900649b2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Final diff --git a/tests/conftest.py b/tests/conftest.py index 64fe0699..0d2cc363 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, Tuple import pytest diff --git a/tests/e2e_integration_tests/__init__.py b/tests/e2e_integration_tests/__init__.py index be6c00e7..836b79af 100644 --- a/tests/e2e_integration_tests/__init__.py +++ b/tests/e2e_integration_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/__init__.py b/tests/e2e_integration_tests/action_masking/__init__.py index be6c00e7..836b79af 100644 --- a/tests/e2e_integration_tests/action_masking/__init__.py +++ b/tests/e2e_integration_tests/action_masking/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py index addf6dca..a34d430b 100644 --- a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py +++ b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict import pytest diff --git a/tests/e2e_integration_tests/environments/__init__.py b/tests/e2e_integration_tests/environments/__init__.py index be6c00e7..836b79af 100644 --- a/tests/e2e_integration_tests/environments/__init__.py +++ b/tests/e2e_integration_tests/environments/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py index 26e690d0..06b080d8 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from ray.rllib.algorithms.ppo import PPOConfig diff --git a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py index 265257e4..da0ca458 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import tempfile from pathlib import Path diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index a07d5d2e..9ca3525a 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Test that we can create a primaite environment and train sb3 agent with no crash.""" import tempfile from pathlib import Path diff --git a/tests/e2e_integration_tests/test_environment.py b/tests/e2e_integration_tests/test_environment.py index dcd51193..881681aa 100644 --- a/tests/e2e_integration_tests/test_environment.py +++ b/tests/e2e_integration_tests/test_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pydantic import pytest import yaml diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 7ec38d72..fa4781db 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/__init__.py +++ b/tests/integration_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/cli/__init__.py b/tests/integration_tests/cli/__init__.py index cfce7ae6..603d228f 100644 --- a/tests/integration_tests/cli/__init__.py +++ b/tests/integration_tests/cli/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List from typer.testing import CliRunner, Result diff --git a/tests/integration_tests/cli/test_dev_cli.py b/tests/integration_tests/cli/test_dev_cli.py index cd390555..16c3de9f 100644 --- a/tests/integration_tests/cli/test_dev_cli.py +++ b/tests/integration_tests/cli/test_dev_cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import os import shutil import tempfile diff --git a/tests/integration_tests/component_creation/__init__.py b/tests/integration_tests/component_creation/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/component_creation/__init__.py +++ b/tests/integration_tests/component_creation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index 7bdc80fc..8b81b7d3 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.core import RequestType from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/component_creation/test_permission_system.py b/tests/integration_tests/component_creation/test_permission_system.py index baf75523..c7faa81b 100644 --- a/tests/integration_tests/component_creation/test_permission_system.py +++ b/tests/integration_tests/component_creation/test_permission_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Dict, List, Literal diff --git a/tests/integration_tests/configuration_file_parsing/__init__.py b/tests/integration_tests/configuration_file_parsing/__init__.py index 7e23a4c2..09861acb 100644 --- a/tests/integration_tests/configuration_file_parsing/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py index 7f251613..234e7342 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index d10c7dbb..16f4dee5 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py index 8526ab78..764a7aac 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.config.load import data_manipulation_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index a642564c..0ff6754d 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py index 13be830b..c588829b 100644 --- a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py +++ b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py index 32d88c92..4153adc0 100644 --- a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_io_settings.py b/tests/integration_tests/configuration_file_parsing/test_io_settings.py index 82977b82..79812d80 100644 --- a/tests/integration_tests/configuration_file_parsing/test_io_settings.py +++ b/tests/integration_tests/configuration_file_parsing/test_io_settings.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py index 26fc562d..016d264f 100644 --- a/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py index 168ebee0..b1c644cc 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from pathlib import Path from typing import Union diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 70dc7cba..9863dbba 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address from typing import Dict, List, Optional diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index e4100741..37a05b6e 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from prettytable import MARKDOWN, PrettyTable diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 80f7e3c3..4af1b748 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index ddaf4a1e..0924a91b 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 5addcbd7..5515d900 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import os from primaite.config.load import get_extended_config_path diff --git a/tests/integration_tests/game_layer/actions/__init__.py b/tests/integration_tests/game_layer/actions/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/game_layer/actions/__init__.py +++ b/tests/integration_tests/game_layer/actions/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index 36a7ae57..e90fa591 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 187fb1fe..7930304e 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 508bd5a4..4e72e60d 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 1c143aed..91aa9fcd 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import uuid from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index e5e0806a..56bbbd4e 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import uuid from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index d796b75e..8846809d 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index fdf04ad5..d75567e9 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index 3054c73b..ebc9fd3b 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index a70cea72..96110656 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/observations/__init__.py b/tests/integration_tests/game_layer/observations/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/game_layer/observations/__init__.py +++ b/tests/integration_tests/game_layer/observations/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index e7212f3c..02cf005a 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index e2ab2990..0268cb95 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 05cf910c..97608132 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.firewall_observation import FirewallObservation from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py index 7d1c1939..630e29ea 100644 --- a/tests/integration_tests/game_layer/observations/test_link_observations.py +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 8254dad2..0ad03198 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py index 69d9f106..63ca8f6b 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from uuid import uuid4 diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index 4ced02f5..f4bfb193 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pprint import pprint from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py index 998aa755..291ee395 100644 --- a/tests/integration_tests/game_layer/observations/test_software_observations.py +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index e7287eee..92c533c9 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index 0c6d567d..45ad2708 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pprint import pprint import pytest diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 64464724..3d26b73d 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index e03a7d26..beb7b6a8 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Plan for creating integration tests for the actions: # I need to test that the requests coming out of the actions have the intended effect on the simulation. # I can do this by creating a simulation, and then running the action on the simulation, and then checking diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index d5679007..23364f13 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from gymnasium import spaces from primaite.game.agent.observations.file_system_observations import FileObservation diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 0005b508..dc7ed132 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/network/__init__.py b/tests/integration_tests/network/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/network/__init__.py +++ b/tests/integration_tests/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py index e000f6ae..e8abc0f2 100644 --- a/tests/integration_tests/network/test_airspace_config.py +++ b/tests/integration_tests/network/test_airspace_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py index b7317c3d..36c77fe1 100644 --- a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.file_system.file_type import FileType from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.system.services.ftp.ftp_client import FTPClient diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index f07f02e7..33fe70c3 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Tuple diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index debf5b1c..b32d9657 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 79452318..24fbfd05 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index fc2d146e..327c87e5 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_multi_lan_internet_example_network.py b/tests/integration_tests/network/test_multi_lan_internet_example_network.py index bcc9ad94..ea7e1c45 100644 --- a/tests/integration_tests/network/test_multi_lan_internet_example_network.py +++ b/tests/integration_tests/network/test_multi_lan_internet_example_network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.networks import multi_lan_internet_network_example diff --git a/tests/integration_tests/network/test_network_creation.py b/tests/integration_tests/network/test_network_creation.py index 794ddde5..1ee3ccc2 100644 --- a/tests/integration_tests/network/test_network_creation.py +++ b/tests/integration_tests/network/test_network_creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_nic_link_connection.py b/tests/integration_tests/network/test_nic_link_connection.py index ab9160c8..8c45f511 100644 --- a/tests/integration_tests/network/test_nic_link_connection.py +++ b/tests/integration_tests/network/test_nic_link_connection.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Link diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 04cdbe78..948b409f 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/network/test_switched_network.py b/tests/integration_tests/network/test_switched_network.py index ae0aa8a7..67392da3 100644 --- a/tests/integration_tests/network/test_switched_network.py +++ b/tests/integration_tests/network/test_switched_network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK def test_switched_network(client_switch_server): """Tests a node can ping another node via the switch.""" computer, switch, server = client_switch_server diff --git a/tests/integration_tests/network/test_users_creation_from_config.py b/tests/integration_tests/network/test_users_creation_from_config.py index 8cd3b037..1963b1dd 100644 --- a/tests/integration_tests/network/test_users_creation_from_config.py +++ b/tests/integration_tests/network/test_users_creation_from_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index fb0035e9..26e50f4a 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/system/__init__.py b/tests/integration_tests/system/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/system/__init__.py +++ b/tests/integration_tests/system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 2cbd4d11..d88f8249 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 50b0ceac..3ef6469e 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index 1a09e875..cb0195f0 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index a5adbb04..14b83e6a 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index ffb5cc7f..836b79af 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -1,109 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK -from typing import Tuple - -import pytest - -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.system.applications.application import Application, ApplicationOperatingState - - -@pytest.fixture(scope="function") -def populated_node(application_class) -> Tuple[Application, Computer]: - computer: Computer = Computer( - hostname="test_computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - shut_down_duration=0, - ) - computer.power_on() - computer.software_manager.install(application_class) - - app = computer.software_manager.software.get("DummyApplication") - app.run() - - return app, computer - - -def test_application_on_offline_node(application_class): - """Test to check that the application cannot be interacted with when node it is on is off.""" - computer: Computer = Computer( - hostname="test_computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - shut_down_duration=0, - ) - computer.software_manager.install(application_class) - - app: Application = computer.software_manager.software.get("DummyApplication") - - computer.power_off() - - assert computer.operating_state is NodeOperatingState.OFF - assert app.operating_state is ApplicationOperatingState.CLOSED - - app.run() - assert app.operating_state is ApplicationOperatingState.CLOSED - - -def test_server_turns_off_application(populated_node): - """Check that the application is turned off when the server is turned off""" - app, computer = populated_node - - assert computer.operating_state is NodeOperatingState.ON - assert app.operating_state is ApplicationOperatingState.RUNNING - - computer.power_off() - - assert computer.operating_state is NodeOperatingState.OFF - assert app.operating_state is ApplicationOperatingState.CLOSED - - -def test_application_cannot_be_turned_on_when_computer_is_off(populated_node): - """Check that the application cannot be started when the computer is off.""" - app, computer = populated_node - - assert computer.operating_state is NodeOperatingState.ON - assert app.operating_state is ApplicationOperatingState.RUNNING - - computer.power_off() - - assert computer.operating_state is NodeOperatingState.OFF - assert app.operating_state is ApplicationOperatingState.CLOSED - - app.run() - - assert computer.operating_state is NodeOperatingState.OFF - assert app.operating_state is ApplicationOperatingState.CLOSED - - -def test_computer_runs_applications(populated_node): - """Check that turning on the computer will turn on applications.""" - app, computer = populated_node - - assert computer.operating_state is NodeOperatingState.ON - assert app.operating_state is ApplicationOperatingState.RUNNING - - computer.power_off() - - assert computer.operating_state is NodeOperatingState.OFF - assert app.operating_state is ApplicationOperatingState.CLOSED - - computer.power_on() - - assert computer.operating_state is NodeOperatingState.ON - assert app.operating_state is ApplicationOperatingState.RUNNING - - computer.power_off() - - assert computer.operating_state is NodeOperatingState.OFF - assert app.operating_state is ApplicationOperatingState.CLOSED - - computer.power_on() - - assert computer.operating_state is NodeOperatingState.ON - assert app.operating_state is ApplicationOperatingState.RUNNING +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/system/test_arp.py b/tests/integration_tests/system/test_arp.py index be8656aa..055d58c6 100644 --- a/tests/integration_tests/system/test_arp.py +++ b/tests/integration_tests/system/test_arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.network.router import RouterARP from primaite.simulator.system.services.arp.arp import ARP from tests.integration_tests.network.test_routing import multi_hop_network diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 965b4ae8..674603fa 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 480a90bc..38caf1a2 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index 22c5d484..fa4df0a9 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index c52b5caa..d1925a94 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address, IPv4Network diff --git a/tests/integration_tests/system/test_ntp_client_server.py b/tests/integration_tests/system/test_ntp_client_server.py index 957c1aeb..42340eb3 100644 --- a/tests/integration_tests/system/test_ntp_client_server.py +++ b/tests/integration_tests/system/test_ntp_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from time import sleep from typing import Tuple 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 7a085ee1..2d3679ed 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, List, Set import yaml diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index cf9728ce..4e73a050 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_user_session_manager_logins.py b/tests/integration_tests/system/test_user_session_manager_logins.py index 4318530c..0c591a4b 100644 --- a/tests/integration_tests/system/test_user_session_manager_logins.py +++ b/tests/integration_tests/system/test_user_session_manager_logins.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple from uuid import uuid4 diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index 05cbae4f..c1028e8e 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index f2ac1183..8fb6dc18 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/test_simulation/__init__.py b/tests/integration_tests/test_simulation/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/test_simulation/__init__.py +++ b/tests/integration_tests/test_simulation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index a767f365..21152199 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # some test cases: # 0. test that sending a request to a valid target results in a success # 1. test that sending a request to a component that doesn't exist results in a failure diff --git a/tests/mock_and_patch/__init__.py b/tests/mock_and_patch/__init__.py index be6c00e7..836b79af 100644 --- a/tests/mock_and_patch/__init__.py +++ b/tests/mock_and_patch/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/mock_and_patch/get_session_path_mock.py b/tests/mock_and_patch/get_session_path_mock.py index f315fca4..073028a7 100644 --- a/tests/mock_and_patch/get_session_path_mock.py +++ b/tests/mock_and_patch/get_session_path_mock.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import tempfile from datetime import datetime from pathlib import Path diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/__init__.py +++ b/tests/unit_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/__init__.py b/tests/unit_tests/_primaite/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/__init__.py +++ b/tests/unit_tests/_primaite/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/__init__.py b/tests/unit_tests/_primaite/_game/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_game/__init__.py +++ b/tests/unit_tests/_primaite/_game/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/_agent/__init__.py b/tests/unit_tests/_primaite/_game/_agent/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_game/_agent/__init__.py +++ b/tests/unit_tests/_primaite/_game/_agent/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index c2d31ee1..63ea1b07 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from unittest.mock import Mock import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py index d61e1a23..a7713437 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index 7f590685..bb3ad33c 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index ec18f1fb..d8f800e4 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 58f0fcc1..0e4bf1bb 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import ( diff --git a/tests/unit_tests/_primaite/_interface/__init__.py b/tests/unit_tests/_primaite/_interface/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_interface/__init__.py +++ b/tests/unit_tests/_primaite/_interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_interface/test_request.py b/tests/unit_tests/_primaite/_interface/test_request.py index 6067f9e4..d9fae083 100644 --- a/tests/unit_tests/_primaite/_interface/test_request.py +++ b/tests/unit_tests/_primaite/_interface/test_request.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from pydantic import ValidationError diff --git a/tests/unit_tests/_primaite/_session/__init__.py b/tests/unit_tests/_primaite/_session/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_session/__init__.py +++ b/tests/unit_tests/_primaite/_session/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_session/test_episode_schedule.py b/tests/unit_tests/_primaite/_session/test_episode_schedule.py index 21448339..ff26bb02 100644 --- a/tests/unit_tests/_primaite/_session/test_episode_schedule.py +++ b/tests/unit_tests/_primaite/_session/test_episode_schedule.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/unit_tests/_primaite/_simulator/__init__.py b/tests/unit_tests/_primaite/_simulator/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_domain/__init__.py b/tests/unit_tests/_primaite/_simulator/_domain/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py index 8db68565..f5294844 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Test the account module of the simulator.""" import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py b/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py b/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py index 0b9bdc8e..6cbf93c8 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import warnings import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 594c7afe..4ec1ec57 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py index 4eb0dd10..5554b9ef 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py index 7d022ea4..44a4e22a 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py index 724d7903..473e0db2 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 4a561b97..609e29c4 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import warnings from typing import Tuple diff --git a/tests/unit_tests/_primaite/_simulator/_network/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 6eca0c44..79392d66 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index fe9387de..fe0c3a57 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index 2613d536..e6bff60e 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py index f35cf171..5cff4407 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import NetworkInterface, Node diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py index 29d5ec67..f9ff0328 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import re from ipaddress import IPv4Address diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 44c5c781..605f8c3b 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py index e7e425b1..161d9cb4 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py index 658726b5..990a0bbf 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index f764f9b5..b1de710a 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py index 2e86ebbc..9885df67 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py index c80189c1..d86aa876 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.utils import convert_bytes_to_megabits, convert_megabits_to_bytes diff --git a/tests/unit_tests/_primaite/_simulator/_system/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 12dddf67..4ff387ce 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index 34a29cd0..6e9ee224 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index e9762476..9d8b7809 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py index 0e9c536c..a69dc844 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.application import Application, ApplicationOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py index f97e915e..16a4c9ad 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.system.applications.application import Application diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py index aef5d6d1..dd29f18e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index e456ed78..5917fde7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple from uuid import uuid4 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index f1be475a..f78b3261 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index 9e7ab1d2..ef165c8f 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index db7e8d58..1bc5b353 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index c64602c0..3bc2b1a4 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 95788834..d3e679db 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 291cdede..37c3d019 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py index 537beb8b..60cd2422 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py index 8c12adaa..ad6fe135 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 9b6a4bf3..08bef92d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple from uuid import uuid4 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 54f86ec8..606a195c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py b/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py index 053211cd..5a734b6e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py +++ b/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 300f8d9d..a203a636 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict import pytest diff --git a/tests/unit_tests/_primaite/_simulator/test_core.py b/tests/unit_tests/_primaite/_simulator/test_core.py index 02960978..271375eb 100644 --- a/tests/unit_tests/_primaite/_simulator/test_core.py +++ b/tests/unit_tests/_primaite/_simulator/test_core.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Callable, Dict, List, Literal, Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/test_sim_container.py b/tests/unit_tests/_primaite/_simulator/test_sim_container.py index fe702307..f482d7e6 100644 --- a/tests/unit_tests/_primaite/_simulator/test_sim_container.py +++ b/tests/unit_tests/_primaite/_simulator/test_sim_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.sim_container import Simulation diff --git a/tests/unit_tests/_primaite/_utils/__init__.py b/tests/unit_tests/_primaite/_utils/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_utils/__init__.py +++ b/tests/unit_tests/_primaite/_utils/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/__init__.py b/tests/unit_tests/_primaite/_utils/_validation/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/__init__.py +++ b/tests/unit_tests/_primaite/_utils/_validation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py index 27829570..7acbe4a7 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py +++ b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.utils.validation.ip_protocol import IPProtocol, is_valid_protocol, PROTOCOL_LOOKUP, protocol_validator diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_port.py b/tests/unit_tests/_primaite/_utils/_validation/test_port.py index 6a8a2429..2e30ab76 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/test_port.py +++ b/tests/unit_tests/_primaite/_utils/_validation/test_port.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.utils.validation.port import is_valid_port, Port, PORT_LOOKUP, port_validator diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index 1a1848ac..d0a64ece 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.utils.converters import convert_dict_enum_keys_to_enum_values from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP From 491de6fc474fa4fdd885895c9a2772d21e9f220f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 2 Jan 2025 15:11:02 +0000 Subject: [PATCH 099/224] Revert pre-commit deleting files --- .../observations/file_system_observations.py | 258 ++ .../agent/observations/link_observation.py | 152 ++ .../observations/software_observation.py | 163 ++ .../simulator/network/hardware/base.py | 2228 +++++++++++++++++ .../system/services/ftp/ftp_client.py | 337 +++ .../system/services/terminal/terminal.py | 544 ++++ .../system/test_application_on_node.py | 108 + 7 files changed, 3790 insertions(+) diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 836b79af..50ca93fd 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -1 +1,259 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from __future__ import annotations + +from typing import Dict, Iterable, List, Optional + +from gymnasium import spaces +from gymnasium.core import ObsType + +from primaite import getLogger +from primaite.game.agent.observations.observations import AbstractObservation, WhereType +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE + +_LOGGER = getLogger(__name__) + + +class FileObservation(AbstractObservation, identifier="FILE"): + """File observation, provides status information about a file within the simulation environment.""" + + class ConfigSchema(AbstractObservation.ConfigSchema): + """Configuration schema for FileObservation.""" + + file_name: str + """Name of the file, used for querying simulation state dictionary.""" + include_num_access: Optional[bool] = None + """Whether to include the number of accesses to the file in the observation.""" + file_system_requires_scan: Optional[bool] = None + """If True, the file must be scanned to update the health state. Tf False, the true state is always shown.""" + + def __init__(self, where: WhereType, include_num_access: bool, file_system_requires_scan: bool) -> None: + """ + Initialise a file observation instance. + + :param where: Where in the simulation state dictionary to find the relevant information for this file. + A typical location for a file might be + ['network', 'nodes', , 'file_system', 'folder', , 'files', ]. + :type where: WhereType + :param include_num_access: Whether to include the number of accesses to the file in the observation. + :type include_num_access: bool + :param file_system_requires_scan: If True, the file must be scanned to update the health state. Tf False, + the true state is always shown. + :type file_system_requires_scan: bool + """ + self.where: WhereType = where + self.include_num_access: bool = include_num_access + self.file_system_requires_scan: bool = file_system_requires_scan + + self.default_observation: ObsType = {"health_status": 0} + if self.include_num_access: + self.default_observation["num_access"] = 0 + + # TODO: allow these to be configured in yaml + self.high_threshold = 10 + self.med_threshold = 5 + self.low_threshold = 0 + + def _categorise_num_access(self, num_access: int) -> int: + """ + Represent number of file accesses as a categorical variable. + + :param num_access: Number of file accesses. + :return: Bin number corresponding to the number of accesses. + """ + if num_access > self.high_threshold: + return 3 + elif num_access > self.med_threshold: + return 2 + elif num_access > self.low_threshold: + return 1 + return 0 + + def observe(self, state: Dict) -> ObsType: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary. + :type state: Dict + :return: Observation containing the health status of the file and optionally the number of accesses. + :rtype: ObsType + """ + file_state = access_from_nested_dict(state, self.where) + if file_state is NOT_PRESENT_IN_STATE: + return self.default_observation + if self.file_system_requires_scan: + health_status = file_state["visible_status"] + else: + health_status = file_state["health_status"] + obs = {"health_status": health_status} + if self.include_num_access: + obs["num_access"] = self._categorise_num_access(file_state["num_access"]) + return obs + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Gymnasium space representing the observation space for file status. + :rtype: spaces.Space + """ + space = {"health_status": spaces.Discrete(6)} + if self.include_num_access: + space["num_access"] = spaces.Discrete(4) + return spaces.Dict(space) + + @classmethod + def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FileObservation: + """ + Create a file observation from a configuration schema. + + :param config: Configuration schema containing the necessary information for the file observation. + :type config: ConfigSchema + :param parent_where: Where in the simulation state dictionary to find the information about this file's + parent node. A typical location for a node might be ['network', 'nodes', ]. + :type parent_where: WhereType, optional + :return: Constructed file observation instance. + :rtype: FileObservation + :param file_system_requires_scan: If True, the folder must be scanned to update the health state. Tf False, + the true state is always shown. + :type file_system_requires_scan: bool + """ + return cls( + where=parent_where + ["files", config.file_name], + include_num_access=config.include_num_access, + file_system_requires_scan=config.file_system_requires_scan, + ) + + +class FolderObservation(AbstractObservation, identifier="FOLDER"): + """Folder observation, provides status information about a folder within the simulation environment.""" + + class ConfigSchema(AbstractObservation.ConfigSchema): + """Configuration schema for FolderObservation.""" + + folder_name: str + """Name of the folder, used for querying simulation state dictionary.""" + files: List[FileObservation.ConfigSchema] = [] + """List of file configurations within the folder.""" + num_files: Optional[int] = None + """Number of spaces for file observations in this folder.""" + include_num_access: Optional[bool] = None + """Whether files in this folder should include the number of accesses in their observation.""" + file_system_requires_scan: Optional[bool] = None + """If True, the folder must be scanned to update the health state. Tf False, the true state is always shown.""" + + def __init__( + self, + where: WhereType, + files: Iterable[FileObservation], + num_files: int, + include_num_access: bool, + file_system_requires_scan: bool, + ) -> None: + """ + Initialise a folder observation instance. + + :param where: Where in the simulation state dictionary to find the relevant information for this folder. + A typical location for a folder might be ['network', 'nodes', , 'folders', ]. + :type where: WhereType + :param files: List of file observation instances within the folder. + :type files: Iterable[FileObservation] + :param num_files: Number of files expected in the folder. + :type num_files: int + :param include_num_access: Whether to include the number of accesses to files in the observation. + :type include_num_access: bool + :param file_system_requires_scan: If True, the folder must be scanned to update the health state. Tf False, + the true state is always shown. + :type file_system_requires_scan: bool + """ + self.where: WhereType = where + + self.file_system_requires_scan: bool = file_system_requires_scan + + self.files: List[FileObservation] = files + while len(self.files) < num_files: + self.files.append( + FileObservation( + where=None, + include_num_access=include_num_access, + file_system_requires_scan=self.file_system_requires_scan, + ) + ) + while len(self.files) > num_files: + truncated_file = self.files.pop() + msg = f"Too many files in folder observation. Truncating file {truncated_file}" + _LOGGER.warning(msg) + + self.default_observation = { + "health_status": 0, + } + if self.files: + self.default_observation["FILES"] = {i + 1: f.default_observation for i, f in enumerate(self.files)} + + def observe(self, state: Dict) -> ObsType: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary. + :type state: Dict + :return: Observation containing the health status of the folder and status of files within the folder. + :rtype: ObsType + """ + folder_state = access_from_nested_dict(state, self.where) + if folder_state is NOT_PRESENT_IN_STATE: + return self.default_observation + + if self.file_system_requires_scan: + health_status = folder_state["visible_status"] + else: + health_status = folder_state["health_status"] + + obs = {} + + obs["health_status"] = health_status + if self.files: + obs["FILES"] = {i + 1: file.observe(state) for i, file in enumerate(self.files)} + + return obs + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Gymnasium space representing the observation space for folder status. + :rtype: spaces.Space + """ + shape = {"health_status": spaces.Discrete(6)} + if self.files: + shape["FILES"] = spaces.Dict({i + 1: f.space for i, f in enumerate(self.files)}) + return spaces.Dict(shape) + + @classmethod + def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FolderObservation: + """ + Create a folder observation from a configuration schema. + + :param config: Configuration schema containing the necessary information for the folder observation. + :type config: ConfigSchema + :param parent_where: Where in the simulation state dictionary to find the information about this folder's + parent node. A typical location for a node might be ['network', 'nodes', ]. + :type parent_where: WhereType, optional + :return: Constructed folder observation instance. + :rtype: FolderObservation + """ + where = parent_where + ["file_system", "folders", config.folder_name] + + # pass down shared/common config items + for file_config in config.files: + file_config.include_num_access = config.include_num_access + file_config.file_system_requires_scan = config.file_system_requires_scan + + files = [FileObservation.from_config(config=f, parent_where=where) for f in config.files] + return cls( + where=where, + files=files, + num_files=config.num_files, + include_num_access=config.include_num_access, + file_system_requires_scan=config.file_system_requires_scan, + ) diff --git a/src/primaite/game/agent/observations/link_observation.py b/src/primaite/game/agent/observations/link_observation.py index 836b79af..851e9557 100644 --- a/src/primaite/game/agent/observations/link_observation.py +++ b/src/primaite/game/agent/observations/link_observation.py @@ -1 +1,153 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from __future__ import annotations + +from typing import Any, Dict, List + +from gymnasium import spaces +from gymnasium.core import ObsType + +from primaite import getLogger +from primaite.game.agent.observations.observations import AbstractObservation, WhereType +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE + +_LOGGER = getLogger(__name__) + + +class LinkObservation(AbstractObservation, identifier="LINK"): + """Link observation, providing information about a specific link within the simulation environment.""" + + class ConfigSchema(AbstractObservation.ConfigSchema): + """Configuration schema for LinkObservation.""" + + link_reference: str + """Reference identifier for the link.""" + + def __init__(self, where: WhereType) -> None: + """ + Initialise a link observation instance. + + :param where: Where in the simulation state dictionary to find the relevant information for this link. + A typical location for a link might be ['network', 'links', ]. + :type where: WhereType + """ + self.where = where + self.default_observation: ObsType = {"PROTOCOLS": {"ALL": 0}} + + def observe(self, state: Dict) -> Any: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary. + :type state: Dict + :return: Observation containing information about the link. + :rtype: Any + """ + link_state = access_from_nested_dict(state, self.where) + if link_state is NOT_PRESENT_IN_STATE: + self.where[-1] = "<->".join(self.where[-1].split("<->")[::-1]) # try swapping endpoint A and B + link_state = access_from_nested_dict(state, self.where) + if link_state is NOT_PRESENT_IN_STATE: + return self.default_observation + + bandwidth = link_state["bandwidth"] + load = link_state["current_load"] + if load == 0: + utilisation_category = 0 + else: + utilisation_fraction = load / bandwidth + utilisation_category = int(utilisation_fraction * 9) + 1 + + return {"PROTOCOLS": {"ALL": min(utilisation_category, 10)}} + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Gymnasium space representing the observation space for link status. + :rtype: spaces.Space + """ + return spaces.Dict({"PROTOCOLS": spaces.Dict({"ALL": spaces.Discrete(11)})}) + + @classmethod + def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> LinkObservation: + """ + Create a link observation from a configuration schema. + + :param config: Configuration schema containing the necessary information for the link observation. + :type config: ConfigSchema + :param parent_where: Where in the simulation state dictionary to find the information about this link. + A typical location might be ['network', 'links', ]. + :type parent_where: WhereType, optional + :return: Constructed link observation instance. + :rtype: LinkObservation + """ + link_reference = config.link_reference + if parent_where == []: + where = ["network", "links", link_reference] + else: + where = parent_where + ["links", link_reference] + return cls(where=where) + + +class LinksObservation(AbstractObservation, identifier="LINKS"): + """Collection of link observations representing multiple links within the simulation environment.""" + + class ConfigSchema(AbstractObservation.ConfigSchema): + """Configuration schema for LinksObservation.""" + + link_references: List[str] + """List of reference identifiers for the links.""" + + def __init__(self, where: WhereType, links: List[LinkObservation]) -> None: + """ + Initialise a links observation instance. + + :param where: Where in the simulation state dictionary to find the relevant information for these links. + A typical location for links might be ['network', 'links']. + :type where: WhereType + :param links: List of link observations. + :type links: List[LinkObservation] + """ + self.where: WhereType = where + self.links: List[LinkObservation] = links + self.default_observation: ObsType = {i + 1: l.default_observation for i, l in enumerate(self.links)} + + def observe(self, state: Dict) -> ObsType: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary. + :type state: Dict + :return: Observation containing information about multiple links. + :rtype: ObsType + """ + return {i + 1: l.observe(state) for i, l in enumerate(self.links)} + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Gymnasium space representing the observation space for multiple links. + :rtype: spaces.Space + """ + return spaces.Dict({i + 1: l.space for i, l in enumerate(self.links)}) + + @classmethod + def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> LinksObservation: + """ + Create a links observation from a configuration schema. + + :param config: Configuration schema containing the necessary information for the links observation. + :type config: ConfigSchema + :param parent_where: Where in the simulation state dictionary to find the information about these links. + A typical location might be ['network']. + :type parent_where: WhereType, optional + :return: Constructed links observation instance. + :rtype: LinksObservation + """ + where = parent_where + ["network"] + link_cfgs = [LinkObservation.ConfigSchema(link_reference=ref) for ref in config.link_references] + links = [LinkObservation.from_config(c, parent_where=where) for c in link_cfgs] + return cls(where=where, links=links) diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 836b79af..37810c6e 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -1 +1,164 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from __future__ import annotations + +from typing import Dict + +from gymnasium import spaces +from gymnasium.core import ObsType + +from primaite.game.agent.observations.observations import AbstractObservation, WhereType +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE + + +class ServiceObservation(AbstractObservation, identifier="SERVICE"): + """Service observation, shows status of a service in the simulation environment.""" + + class ConfigSchema(AbstractObservation.ConfigSchema): + """Configuration schema for ServiceObservation.""" + + service_name: str + """Name of the service, used for querying simulation state dictionary""" + + def __init__(self, where: WhereType) -> None: + """ + Initialise a service observation instance. + + :param where: Where in the simulation state dictionary to find the relevant information for this service. + A typical location for a service might be ['network', 'nodes', , 'services', ]. + :type where: WhereType + """ + self.where = where + self.default_observation = {"operating_status": 0, "health_status": 0} + + def observe(self, state: Dict) -> ObsType: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary. + :type state: Dict + :return: Observation containing the operating status and health status of the service. + :rtype: ObsType + """ + service_state = access_from_nested_dict(state, self.where) + if service_state is NOT_PRESENT_IN_STATE: + return self.default_observation + return { + "operating_status": service_state["operating_state"], + "health_status": service_state["health_state_visible"], + } + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Gymnasium space representing the observation space for service status. + :rtype: spaces.Space + """ + return spaces.Dict({"operating_status": spaces.Discrete(7), "health_status": spaces.Discrete(5)}) + + @classmethod + def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> ServiceObservation: + """ + Create a service observation from a configuration schema. + + :param config: Configuration schema containing the necessary information for the service observation. + :type config: ConfigSchema + :param parent_where: Where in the simulation state dictionary to find the information about this service's + parent node. A typical location for a node might be ['network', 'nodes', ]. + :type parent_where: WhereType, optional + :return: Constructed service observation instance. + :rtype: ServiceObservation + """ + return cls(where=parent_where + ["services", config.service_name]) + + +class ApplicationObservation(AbstractObservation, identifier="APPLICATION"): + """Application observation, shows the status of an application within the simulation environment.""" + + class ConfigSchema(AbstractObservation.ConfigSchema): + """Configuration schema for ApplicationObservation.""" + + application_name: str + """Name of the application, used for querying simulation state dictionary""" + + def __init__(self, where: WhereType) -> None: + """ + Initialise an application observation instance. + + :param where: Where in the simulation state dictionary to find the relevant information for this application. + A typical location for an application might be + ['network', 'nodes', , 'applications', ]. + :type where: WhereType + """ + self.where = where + self.default_observation = {"operating_status": 0, "health_status": 0, "num_executions": 0} + + # TODO: allow these to be configured in yaml + self.high_threshold = 10 + self.med_threshold = 5 + self.low_threshold = 0 + + def _categorise_num_executions(self, num_executions: int) -> int: + """ + Represent number of file accesses as a categorical variable. + + :param num_access: Number of file accesses. + :return: Bin number corresponding to the number of accesses. + """ + if num_executions > self.high_threshold: + return 3 + elif num_executions > self.med_threshold: + return 2 + elif num_executions > self.low_threshold: + return 1 + return 0 + + def observe(self, state: Dict) -> ObsType: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary. + :type state: Dict + :return: Obs containing the operating status, health status, and number of executions of the application. + :rtype: ObsType + """ + application_state = access_from_nested_dict(state, self.where) + if application_state is NOT_PRESENT_IN_STATE: + return self.default_observation + return { + "operating_status": application_state["operating_state"], + "health_status": application_state["health_state_visible"], + "num_executions": self._categorise_num_executions(application_state["num_executions"]), + } + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Gymnasium space representing the observation space for application status. + :rtype: spaces.Space + """ + return spaces.Dict( + { + "operating_status": spaces.Discrete(7), + "health_status": spaces.Discrete(5), + "num_executions": spaces.Discrete(4), + } + ) + + @classmethod + def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> ApplicationObservation: + """ + Create an application observation from a configuration schema. + + :param config: Configuration schema containing the necessary information for the application observation. + :type config: ConfigSchema + :param parent_where: Where in the simulation state dictionary to find the information about this application's + parent node. A typical location for a node might be ['network', 'nodes', ]. + :type parent_where: WhereType, optional + :return: Constructed application observation instance. + :rtype: ApplicationObservation + """ + return cls(where=parent_where + ["applications", config.application_name]) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 836b79af..8324715f 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1 +1,2229 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from __future__ import annotations + +import re +import secrets +from abc import ABC, abstractmethod +from ipaddress import IPv4Address, IPv4Network +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union + +from prettytable import MARKDOWN, PrettyTable +from pydantic import BaseModel, Field, validate_call + +from primaite import getLogger +from primaite.exceptions import NetworkError +from primaite.interface.request import RequestResponse +from primaite.simulator import SIM_OUTPUT +from primaite.simulator.core import RequestFormat, RequestManager, RequestPermissionValidator, RequestType, SimComponent +from primaite.simulator.domain.account import Account +from primaite.simulator.file_system.file_system import FileSystem +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.nmne import NMNEConfig +from primaite.simulator.network.transmission.data_link_layer import Frame +from primaite.simulator.system.applications.application import Application +from primaite.simulator.system.core.packet_capture import PacketCapture +from primaite.simulator.system.core.session_manager import SessionManager +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.core.sys_log import SysLog +from primaite.simulator.system.processes.process import Process +from primaite.simulator.system.services.service import Service +from primaite.simulator.system.services.terminal.terminal import Terminal +from primaite.simulator.system.software import IOSoftware, Software +from primaite.utils.converters import convert_dict_enum_keys_to_enum_values +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import PORT_LOOKUP + +IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware) + +_LOGGER = getLogger(__name__) + + +def generate_mac_address(oui: Optional[str] = None) -> str: + """ + Generate a random MAC Address. + + :param oui: The Organizationally Unique Identifier (OUI) portion of the MAC address. It should be a string with + the first 3 bytes (24 bits) in the format "XX:XX:XX". + :raises ValueError: If the 'oui' is not in the correct format (hexadecimal and 6 characters). + """ + random_bytes = [secrets.randbits(8) for _ in range(6)] + + if oui: + oui_pattern = re.compile(r"^([0-9A-Fa-f]{2}[:-]){2}[0-9A-Fa-f]{2}$") + if not oui_pattern.match(oui): + msg = f"Invalid oui. The oui should be in the format xx:xx:xx, where x is a hexadecimal digit, got '{oui}'" + _LOGGER.error(msg) + raise ValueError(msg) + oui_bytes = [int(chunk, 16) for chunk in oui.split(":")] + mac = oui_bytes + random_bytes[len(oui_bytes) :] + else: + mac = random_bytes + + return ":".join(f"{b:02x}" for b in mac) + + +class NetworkInterface(SimComponent, ABC): + """ + A generic Network Interface in a Node on a Network. + + This is a base class for specific types of network interfaces, providing common attributes and methods required + for network communication. It defines the fundamental properties that all network interfaces share, such as + MAC address, speed, MTU (maximum transmission unit), and the ability to enable or disable the interface. + + :ivar str mac_address: The MAC address of the network interface. Default to a randomly generated MAC address. + :ivar int speed: The speed of the interface in Mbps. Default is 100 Mbps. + :ivar int mtu: The Maximum Transmission Unit (MTU) of the interface in Bytes. Default is 1500 B. + """ + + mac_address: str = Field(default_factory=generate_mac_address) + "The MAC address of the interface." + + speed: float = 100.0 + "The speed of the interface in Mbps. Default is 100 Mbps." + + mtu: int = 1500 + "The Maximum Transmission Unit (MTU) of the interface in Bytes. Default is 1500 B" + + enabled: bool = False + "Indicates whether the interface is enabled." + + _connected_node: Optional[Node] = None + "The Node to which the interface is connected." + + port_num: Optional[int] = None + "The port number assigned to this interface on the connected node." + + port_name: Optional[str] = None + "The port name assigned to this interface on the connected node." + + pcap: Optional[PacketCapture] = None + "A PacketCapture instance for capturing and analysing packets passing through this interface." + + nmne_config: ClassVar[NMNEConfig] = NMNEConfig() + "A dataclass defining malicious network events to be captured." + + nmne: Dict = Field(default_factory=lambda: {}) + "A dict containing details of the number of malicious events captured." + + traffic: Dict = Field(default_factory=lambda: {}) + "A dict containing details of the inbound and outbound traffic by port and protocol." + + def setup_for_episode(self, episode: int): + """Reset the original state of the SimComponent.""" + super().setup_for_episode(episode=episode) + self.nmne = {} + self.traffic = {} + if episode and self.pcap and SIM_OUTPUT.save_pcap_logs: + self.pcap.current_episode = episode + self.pcap.setup_logger() + self.enable() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + _is_network_interface_enabled = NetworkInterface._EnabledValidator(network_interface=self) + _is_network_interface_disabled = NetworkInterface._DisabledValidator(network_interface=self) + + rm = super()._init_request_manager() + + rm.add_request( + "enable", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.enable()), + validator=_is_network_interface_disabled, + ), + ) + rm.add_request( + "disable", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.disable()), + validator=_is_network_interface_enabled, + ), + ) + + return rm + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + """ + state = super().describe_state() + state.update( + { + "mac_address": self.mac_address, + "speed": self.speed, + "mtu": self.mtu, + "enabled": self.enabled, + } + ) + if self.nmne_config and self.nmne_config.capture_nmne: + state.update({"nmne": self.nmne}) + state.update({"traffic": convert_dict_enum_keys_to_enum_values(self.traffic)}) + return state + + @abstractmethod + def enable(self) -> bool: + """Enable the interface.""" + pass + return False + + @abstractmethod + def disable(self) -> bool: + """Disable the interface.""" + pass + return False + + def _capture_nmne(self, frame: Frame, inbound: bool = True) -> None: + """ + Processes and captures network frame data based on predefined global NMNE settings. + + This method updates the NMNE structure with counts of malicious network events based on the frame content and + direction. The structure is dynamically adjusted according to the enabled capture settings. + + .. note:: + While there is a lot of logic in this code that defines a multi-level hierarchical NMNE structure, + most of it is unused for now as a result of all `CAPTURE_BY_<>` variables in + ``primaite.simulator.network.nmne`` being hardcoded and set as final. Once they're 'released' and made + configurable, this function will be updated to properly explain the dynamic data structure. + + :param frame: The network frame to process, containing IP, TCP/UDP, and payload information. + :param inbound: Boolean indicating if the frame direction is inbound. Defaults to True. + """ + # Exit function if NMNE capturing is disabled + if not (self.nmne_config and self.nmne_config.capture_nmne): + return + + # Initialise basic frame data variables + direction = "inbound" if inbound else "outbound" # Direction of the traffic + ip_address = str(frame.ip.src_ip_address if inbound else frame.ip.dst_ip_address) # Source or destination IP + protocol = frame.ip.protocol # Network protocol used in the frame + + # Initialise port variable; will be determined based on protocol type + port = None + + # Determine the source or destination port based on the protocol (TCP/UDP) + if frame.tcp: + port = frame.tcp.src_port if inbound else frame.tcp.dst_port + elif frame.udp: + port = frame.udp.src_port if inbound else frame.udp.dst_port + + # Convert frame payload to string for keyword checking + frame_str = str(frame.payload) + + # Proceed only if any NMNE keyword is present in the frame payload + if any(keyword in frame_str for keyword in self.nmne_config.nmne_capture_keywords): + # Start with the root of the NMNE capture structure + current_level = self.nmne + + # Update NMNE structure based on enabled settings + if self.nmne_config.capture_by_direction: + # Set or get the dictionary for the current direction + current_level = current_level.setdefault("direction", {}) + current_level = current_level.setdefault(direction, {}) + + if self.nmne_config.capture_by_ip_address: + # Set or get the dictionary for the current IP address + current_level = current_level.setdefault("ip_address", {}) + current_level = current_level.setdefault(ip_address, {}) + + if self.nmne_config.capture_by_protocol: + # Set or get the dictionary for the current protocol + current_level = current_level.setdefault("protocol", {}) + current_level = current_level.setdefault(protocol, {}) + + if self.nmne_config.capture_by_port: + # Set or get the dictionary for the current port + current_level = current_level.setdefault("port", {}) + current_level = current_level.setdefault(port, {}) + + # Ensure 'KEYWORD' level is present in the structure + keyword_level = current_level.setdefault("keywords", {}) + + # Increment the count for detected keywords in the payload + if self.nmne_config.capture_by_keyword: + for keyword in self.nmne_config.nmne_capture_keywords: + if keyword in frame_str: + # Update the count for each keyword found + keyword_level[keyword] = keyword_level.get(keyword, 0) + 1 + else: + # Increment a generic counter if keyword capturing is not enabled + keyword_level["*"] = keyword_level.get("*", 0) + 1 + + def _capture_traffic(self, frame: Frame, inbound: bool = True): + """ + Capture traffic statistics at the Network Interface. + + :param frame: The network frame containing the traffic data. + :type frame: Frame + :param inbound: Flag indicating if the traffic is inbound or outbound. Defaults to True. + :type inbound: bool + """ + # Determine the direction of the traffic + direction = "inbound" if inbound else "outbound" + + # Initialize protocol and port variables + protocol = None + port = None + + # Identify the protocol and port from the frame + if frame.tcp: + protocol = PROTOCOL_LOOKUP["TCP"] + port = frame.tcp.dst_port + elif frame.udp: + protocol = PROTOCOL_LOOKUP["UDP"] + port = frame.udp.dst_port + elif frame.icmp: + protocol = PROTOCOL_LOOKUP["ICMP"] + + # Ensure the protocol is in the capture dict + if protocol not in self.traffic: + self.traffic[protocol] = {} + + # Handle non-ICMP protocols that use ports + if protocol != PROTOCOL_LOOKUP["ICMP"]: + if port not in self.traffic[protocol]: + self.traffic[protocol][port] = {"inbound": 0, "outbound": 0} + self.traffic[protocol][port][direction] += frame.size_Mbits + else: + # Handle ICMP protocol separately (ICMP does not use ports) + if not self.traffic[protocol]: + self.traffic[protocol] = {"inbound": 0, "outbound": 0} + self.traffic[protocol][direction] += frame.size_Mbits + + @abstractmethod + def send_frame(self, frame: Frame) -> bool: + """ + Attempts to send a network frame through the interface. + + :param frame: The network frame to be sent. + :return: A boolean indicating whether the frame was successfully sent. + """ + self._capture_nmne(frame, inbound=False) + self._capture_traffic(frame, inbound=False) + + @abstractmethod + def receive_frame(self, frame: Frame) -> bool: + """ + Receives a network frame on the interface. + + :param frame: The network frame being received. + :return: A boolean indicating whether the frame was successfully received. + """ + self._capture_nmne(frame, inbound=True) + self._capture_traffic(frame, inbound=True) + + def __str__(self) -> str: + """ + String representation of the NIC. + + :return: A string combining the port number and the mac address + """ + return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}" + + def __hash__(self) -> int: + return hash(self.uuid) + + def apply_timestep(self, timestep: int) -> None: + """ + Apply a timestep evolution to this component. + + This just clears the nmne count back to 0. + """ + super().apply_timestep(timestep=timestep) + + def pre_timestep(self, timestep: int) -> None: + """Apply pre-timestep logic.""" + super().pre_timestep(timestep) + self.traffic = {} + + class _EnabledValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the NetworkInterface is enabled. + + This is useful because most actions should be being resolved if the NetworkInterface is disabled. + """ + + network_interface: NetworkInterface + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the NetworkInterface is enabled or not.""" + return self.network_interface.enabled + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return ( + f"Cannot perform request on NetworkInterface " + f"'{self.network_interface.mac_address}' because it is not enabled." + ) + + class _DisabledValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the NetworkInterface is disabled. + + This is useful because some actions should be being resolved if the NetworkInterface is disabled. + """ + + network_interface: NetworkInterface + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the NetworkInterface is disabled or not.""" + return not self.network_interface.enabled + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return ( + f"Cannot perform request on NetworkInterface " + f"'{self.network_interface.mac_address}' because it is not disabled." + ) + + +class WiredNetworkInterface(NetworkInterface, ABC): + """ + Represents a wired network interface in a network device. + + This abstract base class serves as a foundational blueprint for wired network interfaces, offering core + functionalities and enforcing the implementation of key operational methods such as enabling and disabling the + interface. It encapsulates common attributes and behaviors intrinsic to wired interfaces, including the + management of physical or logical connections to network links and the provision of methods for connecting to and + disconnecting from these links. + + Inherits from: + - NetworkInterface: Provides basic network interface properties and methods. + + + Subclasses of this class are expected to provide concrete implementations for the abstract methods defined here, + tailoring the functionality to the specific requirements of the wired interface types they represent. + """ + + _connected_link: Optional[Link] = None + "The network link to which the network interface is connected." + + def enable(self) -> bool: + """Attempt to enable the network interface.""" + if self.enabled: + return True + + if not self._connected_node: + _LOGGER.warning(f"Interface {self} cannot be enabled as it is not connected to a Node") + return False + + if self._connected_node.operating_state != NodeOperatingState.ON: + self._connected_node.sys_log.warning( + f"Interface {self} cannot be enabled as the connected Node is not powered on" + ) + return False + + if not self._connected_link: + self._connected_node.sys_log.warning(f"Interface {self} cannot be enabled as there is no Link connected.") + return False + + self.enabled = True + self._connected_node.sys_log.info(f"Network Interface {self} enabled") + self.pcap = PacketCapture( + hostname=self._connected_node.hostname, port_num=self.port_num, port_name=self.port_name + ) + if self._connected_link: + self._connected_link.endpoint_up() + return True + + def disable(self) -> bool: + """Disable the network interface.""" + if not self.enabled: + return True + self.enabled = False + if self._connected_node: + self._connected_node.sys_log.info(f"Network Interface {self} disabled") + else: + _LOGGER.debug(f"Interface {self} disabled") + if self._connected_link: + self._connected_link.endpoint_down() + return True + + def connect_link(self, link: Link): + """ + Connect this network interface to a specified link. + + This method establishes a connection between the network interface and a network link if the network interface + is not already connected. If the network interface is already connected to a link, it logs an error and does + not change the existing connection. + + :param link: The Link instance to connect to this network interface. + """ + if self._connected_link: + _LOGGER.warning(f"Cannot connect Link to network interface {self} as it already has a connection") + return + + if self._connected_link == link: + _LOGGER.warning(f"Cannot connect Link to network interface {self} as it is already connected") + return + + self._connected_link = link + self.enable() + + def disconnect_link(self): + """ + Disconnect the network interface from its connected Link, if any. + + This method removes the association between the network interface and its connected Link. It updates the + connected Link's endpoints to reflect the disconnection. + """ + if self._connected_link.endpoint_a == self: + self._connected_link.endpoint_a = None + if self._connected_link.endpoint_b == self: + self._connected_link.endpoint_b = None + self._connected_link = None + + def send_frame(self, frame: Frame) -> bool: + """ + Attempt to send a network frame through the connected Link. + + This method sends a frame if the NIC is enabled and connected to a link. It captures the frame using PCAP + (if available) and transmits it through the connected link. Returns True if the frame is successfully sent, + False otherwise (e.g., if the Network Interface is disabled). + + :param frame: The network frame to be sent. + :return: True if the frame is sent, False if the Network Interface is disabled or not connected to a link. + """ + if not self.enabled: + return False + if not self._connected_link.can_transmit_frame(frame): + # Drop frame for now. Queuing will happen here (probably) if it's done in the future. + self._connected_node.sys_log.info(f"{self}: Frame dropped as Link is at capacity") + return False + super().send_frame(frame) + frame.set_sent_timestamp() + self.pcap.capture_outbound(frame) + self._connected_link.transmit_frame(sender_nic=self, frame=frame) + return True + + @abstractmethod + def receive_frame(self, frame: Frame) -> bool: + """ + Receives a network frame on the network interface. + + :param frame: The network frame being received. + :return: A boolean indicating whether the frame was successfully received. + """ + return super().receive_frame(frame) + + +class Layer3Interface(BaseModel, ABC): + """ + Represents a Layer 3 (Network Layer) interface in a network device. + + This class serves as a base for network interfaces that operate at Layer 3 of the OSI model, providing IP + connectivity and subnetting capabilities. It's not meant to be instantiated directly but to be subclassed by + specific types of network interfaces that require IP addressing capabilities. + + :ivar IPV4Address ip_address: The IP address assigned to the interface. This address enables the interface to + participate in IP-based networking, allowing it to send and receive IP packets. + :ivar IPv4Address subnet_mask: The subnet mask assigned to the interface. This mask helps in determining the + network segment that the interface belongs to and is used in IP routing decisions. + """ + + ip_address: IPV4Address + "The IP address assigned to the interface for communication on an IP-based network." + + subnet_mask: IPV4Address + "The subnet mask assigned to the interface, defining the network portion and the host portion of the IP address." + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + """ + state = { + "ip_address": str(self.ip_address), + "subnet_mask": str(self.subnet_mask), + } + + return state + + @property + def ip_network(self) -> IPv4Network: + """ + Calculate and return the IPv4Network derived from the NIC's IP address and subnet mask. + + This property constructs an IPv4Network object which represents the whole network that the NIC's IP address + belongs to, based on its subnet mask. It's useful for determining the network range and broadcast address. + + :return: An IPv4Network instance representing the network of this NIC. + """ + return IPv4Network(f"{self.ip_address}/{self.subnet_mask}", strict=False) + + +class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): + """ + Represents an IP wired network interface. + + This interface operates at both the data link layer (Layer 2) and the network layer (Layer 3) of the OSI model, + specifically tailored for IP-based communication. This abstract class serves as a template for creating specific + wired network interfaces that support Internet Protocol (IP) functionalities. + + As this class is an amalgamation of its parent classes without additional attributes or methods, it is recommended + to refer to the documentation of `WiredNetworkInterface` and `Layer3Interface` for detailed information on the + supported operations and functionalities. + + The class inherits from: + - `WiredNetworkInterface`: Provides the functionalities and characteristics of a wired connection, such as + physical link establishment and data transmission over a cable. + - `Layer3Interface`: Enables network layer capabilities, including IP address assignment, routing, and + potentially, Layer 3 protocols like IPsec. + + As an abstract class, `IPWiredNetworkInterface` does not implement specific methods but mandates that any derived + class provides implementations for the functionalities of both `WiredNetworkInterface` and `Layer3Interface`. + This structure is ideal for representing network interfaces in devices that require wired connections and are + capable of IP routing and addressing, such as routers, switches, as well as end-host devices like computers and + servers. + + Derived classes should define specific behaviors and properties of an IP-capable wired network interface, + customizing it for their specific use cases. + """ + + _connected_link: Optional[Link] = None + "The network link to which the network interface is connected." + + def model_post_init(self, __context: Any) -> None: + """ + Performs post-initialisation checks to ensure the model's IP configuration is valid. + + This method is invoked after the initialisation of a network model object to validate its network settings, + particularly to ensure that the assigned IP address is not a network address. This validation is crucial for + maintaining the integrity of network simulations and avoiding configuration errors that could lead to + unrealistic or incorrect behavior. + + :param __context: Contextual information or parameters passed to the method, used for further initializing or + validating the model post-creation. + :raises ValueError: If the IP address is the same as the network address, indicating an incorrect configuration. + """ + if self.ip_network.network_address == self.ip_address: + raise ValueError(f"{self.ip_address}/{self.subnet_mask} must not be a network address") + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + # Get the state from the WiredNetworkInterface + state = WiredNetworkInterface.describe_state(self) + + # Update the state with information from Layer3Interface + state.update(Layer3Interface.describe_state(self)) + + return state + + def enable(self) -> bool: + """ + Enables this wired network interface and attempts to send a "hello" message to the default gateway. + + This method activates the network interface, making it operational for network communications. After enabling, + it tries to initiate a default gateway "hello" process, typically to establish initial connectivity and resolve + the default gateway's MAC address. This step is crucial for ensuring the interface can successfully send data + to and receive data from the network. + + The method safely handles cases where the connected node might not have a default gateway set or the + `default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption. + """ + super().enable() + try: + self._connected_node.default_gateway_hello() + except AttributeError: + pass + return True + + @abstractmethod + def receive_frame(self, frame: Frame) -> bool: + """ + Receives a network frame on the network interface. + + :param frame: The network frame being received. + :return: A boolean indicating whether the frame was successfully received. + """ + return super().receive_frame(frame) + + +class Link(SimComponent): + """ + Represents a network link between NIC<-->NIC, NIC<-->SwitchPort, or SwitchPort<-->SwitchPort. + + :param endpoint_a: The first NIC or SwitchPort connected to the Link. + :param endpoint_b: The second NIC or SwitchPort connected to the Link. + :param bandwidth: The bandwidth of the Link in Mbps. + """ + + endpoint_a: WiredNetworkInterface + "The first WiredNetworkInterface connected to the Link." + endpoint_b: WiredNetworkInterface + "The second WiredNetworkInterface connected to the Link." + bandwidth: float + "The bandwidth of the Link in Mbps." + current_load: float = 0.0 + "The current load on the link in Mbps." + + def __init__(self, **kwargs): + """ + Ensure that endpoint_a and endpoint_b are not the same NIC. + + Connect the link to the NICs after creation. + + :raises ValueError: If endpoint_a and endpoint_b are the same NIC. + """ + if kwargs["endpoint_a"] == kwargs["endpoint_b"]: + msg = "endpoint_a and endpoint_b cannot be the same NIC or SwitchPort" + _LOGGER.error(msg) + raise ValueError(msg) + super().__init__(**kwargs) + self.endpoint_a.connect_link(self) + self.endpoint_b.connect_link(self) + self.endpoint_up() + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "endpoint_a": self.endpoint_a.uuid, # TODO: consider if using UUID is the best way to do this + "endpoint_b": self.endpoint_b.uuid, # TODO: consider if using UUID is the best way to do this + "bandwidth": self.bandwidth, + "current_load": self.current_load, + } + ) + return state + + @property + def current_load_percent(self) -> str: + """Get the current load formatted as a percentage string.""" + return f"{self.current_load / self.bandwidth:.5f}%" + + def endpoint_up(self): + """Let the Link know and endpoint has been brought up.""" + if self.is_up: + _LOGGER.debug(f"Link {self} up") + + def endpoint_down(self): + """Let the Link know and endpoint has been brought down.""" + if not self.is_up: + self.current_load = 0.0 + _LOGGER.debug(f"Link {self} down") + + @property + def is_up(self) -> bool: + """ + Informs whether the link is up. + + This is based upon both NIC endpoints being enabled. + """ + return self.endpoint_a.enabled and self.endpoint_b.enabled + + def can_transmit_frame(self, frame: Frame) -> bool: + """ + Determines whether a frame can be transmitted considering the current Link load and the Link's bandwidth. + + This method assesses if the transmission of a given frame is possible without exceeding the Link's total + bandwidth capacity. It checks if the current load of the Link plus the size of the frame (expressed in Mbps) + would remain within the defined bandwidth limits. The transmission is only feasible if the Link is active + ('up') and the total load including the new frame does not surpass the bandwidth limit. + + :param frame: The frame intended for transmission, which contains its size in Mbps. + :return: True if the frame can be transmitted without exceeding the bandwidth limit, False otherwise. + """ + if self.is_up: + frame_size_Mbits = frame.size_Mbits # noqa - Leaving it as Mbits as this is how they're expressed + return self.current_load + frame.size_Mbits <= self.bandwidth + return False + + def transmit_frame(self, sender_nic: WiredNetworkInterface, frame: Frame) -> bool: + """ + Send a network frame from one NIC or SwitchPort to another connected NIC or SwitchPort. + + :param sender_nic: The NIC or SwitchPort sending the frame. + :param frame: The network frame to be sent. + :return: True if the Frame can be sent, otherwise False. + """ + receiver = self.endpoint_a + if receiver == sender_nic: + receiver = self.endpoint_b + frame_size = frame.size_Mbits + + if receiver.receive_frame(frame): + # Frame transmitted successfully + # Load the frame size on the link + self.current_load += frame_size + _LOGGER.debug( + f"Added {frame_size:.3f} Mbits to {self}, current load {self.current_load:.3f} Mbits " + f"({self.current_load_percent})" + ) + return True + return False + + def __str__(self) -> str: + return f"{self.endpoint_a}<-->{self.endpoint_b}" + + def apply_timestep(self, timestep: int) -> None: + """Apply a timestep to the simulation.""" + super().apply_timestep(timestep) + + def pre_timestep(self, timestep: int) -> None: + """Apply pre-timestep logic.""" + super().pre_timestep(timestep) + self.current_load = 0.0 + + +class User(SimComponent): + """ + Represents a user in the PrimAITE system. + + :ivar username: The username of the user + :ivar password: The password of the user + :ivar disabled: Boolean flag indicating whether the user is disabled + :ivar is_admin: Boolean flag indicating whether the user has admin privileges + """ + + username: str + """The username of the user""" + + password: str + """The password of the user""" + + disabled: bool = False + """Boolean flag indicating whether the user is disabled""" + + is_admin: bool = False + """Boolean flag indicating whether the user has admin privileges""" + + num_of_logins: int = 0 + """Counts the number of the User has logged in""" + + def describe_state(self) -> Dict: + """ + Returns a dictionary representing the current state of the user. + + :return: A dict containing the state of the user + """ + return self.model_dump() + + +class UserManager(Service): + """ + Manages users within the PrimAITE system, handling creation, authentication, and administration. + + :param users: A dictionary of all users by their usernames + :param admins: A dictionary of admin users by their usernames + :param disabled_admins: A dictionary of currently disabled admin users by their usernames + """ + + users: Dict[str, User] = {} + + def __init__(self, **kwargs): + """ + Initializes a UserManager instanc. + + :param username: The username for the default admin user + :param password: The password for the default admin user + """ + kwargs["name"] = "UserManager" + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] + super().__init__(**kwargs) + + self.start() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + # todo add doc about requeest schemas + rm.add_request( + "change_password", + RequestType( + func=lambda request, context: RequestResponse.from_bool( + self.change_user_password(username=request[0], current_password=request[1], new_password=request[2]) + ) + ), + ) + return rm + + def describe_state(self) -> Dict: + """ + Returns the state of the UserManager along with the number of users and admins. + + :return: A dict containing detailed state information + """ + state = super().describe_state() + state.update({"total_users": len(self.users), "total_admins": len(self.admins) + len(self.disabled_admins)}) + state["users"] = {k: v.describe_state() for k, v in self.users.items()} + return state + + def show(self, markdown: bool = False): + """ + Display the Users. + + :param markdown: Whether to display the table in Markdown format or not. Default is `False`. + """ + table = PrettyTable(["Username", "Admin", "Disabled"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.sys_log.hostname} User Manager" + for user in self.users.values(): + table.add_row([user.username, user.is_admin, user.disabled]) + print(table.get_string(sortby="Username")) + + @property + def non_admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all enabled non-admin users. + + :return: A dictionary with usernames as keys and User objects as values for non-admin, non-disabled users. + """ + return {k: v for k, v in self.users.items() if not v.is_admin and not v.disabled} + + @property + def disabled_non_admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all disabled non-admin users. + + :return: A dictionary with usernames as keys and User objects as values for non-admin, disabled users. + """ + return {k: v for k, v in self.users.items() if not v.is_admin and v.disabled} + + @property + def admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all enabled admin users. + + :return: A dictionary with usernames as keys and User objects as values for admin, non-disabled users. + """ + return {k: v for k, v in self.users.items() if v.is_admin and not v.disabled} + + @property + def disabled_admins(self) -> Dict[str, User]: + """ + Returns a dictionary of all disabled admin users. + + :return: A dictionary with usernames as keys and User objects as values for admin, disabled users. + """ + return {k: v for k, v in self.users.items() if v.is_admin and v.disabled} + + def install(self) -> None: + """Setup default user during first-time installation.""" + self.add_user(username="admin", password="admin", is_admin=True, bypass_can_perform_action=True) + + def _is_last_admin(self, username: str) -> bool: + return username in self.admins and len(self.admins) == 1 + + def add_user( + self, username: str, password: str, is_admin: bool = False, bypass_can_perform_action: bool = False + ) -> bool: + """ + Adds a new user to the system. + + :param username: The username for the new user + :param password: The password for the new user + :param is_admin: Flag indicating if the new user is an admin + :return: True if user was successfully added, False otherwise + """ + if not bypass_can_perform_action and not self._can_perform_action(): + return False + if username in self.users: + self.sys_log.info(f"{self.name}: Failed to create new user {username} as this user name already exists") + return False + user = User(username=username, password=password, is_admin=is_admin) + self.users[username] = user + self.sys_log.info(f"{self.name}: Added new {'admin' if is_admin else 'user'}: {username}") + return True + + def authenticate_user(self, username: str, password: str) -> Optional[User]: + """ + Authenticates a user's login attempt. + + :param username: The username of the user trying to log in + :param password: The password provided by the user + :return: The User object if authentication is successful, None otherwise + """ + if not self._can_perform_action(): + return None + user = self.users.get(username) + if user and not user.disabled and user.password == password: + self.sys_log.info(f"{self.name}: User authenticated: {username}") + return user + self.sys_log.info(f"{self.name}: Authentication failed for: {username}") + return None + + def change_user_password(self, username: str, current_password: str, new_password: str) -> bool: + """ + Changes a user's password. + + :param username: The username of the user changing their password + :param current_password: The current password of the user + :param new_password: The new password for the user + :return: True if the password was changed successfully, False otherwise + """ + if not self._can_perform_action(): + return False + user = self.users.get(username) + if user and user.password == current_password: + user.password = new_password + self.sys_log.info(f"{self.name}: Password changed for {username}") + self._user_session_manager._logout_user(user=user) + return True + self.sys_log.info(f"{self.name}: Password change failed for {username}") + return False + + def disable_user(self, username: str) -> bool: + """ + Disables a user account, preventing them from logging in. + + :param username: The username of the user to disable + :return: True if the user was disabled successfully, False otherwise + """ + if not self._can_perform_action(): + return False + if username in self.users and not self.users[username].disabled: + if self._is_last_admin(username): + self.sys_log.info(f"{self.name}: Cannot disable User {username} as they are the only enabled admin") + return False + self.users[username].disabled = True + self.sys_log.info(f"{self.name}: User disabled: {username}") + return True + self.sys_log.info(f"{self.name}: Failed to disable user: {username}") + return False + + def enable_user(self, username: str) -> bool: + """ + Enables a previously disabled user account. + + :param username: The username of the user to enable + :return: True if the user was enabled successfully, False otherwise + """ + if username in self.users and self.users[username].disabled: + self.users[username].disabled = False + self.sys_log.info(f"{self.name}: User enabled: {username}") + return True + self.sys_log.info(f"{self.name}: Failed to enable user: {username}") + return False + + @property + def _user_session_manager(self) -> "UserSessionManager": + return self.software_manager.software["UserSessionManager"] # noqa + + +class UserSession(SimComponent): + """ + Represents a user session on the Node. + + This class manages the state of a user session, including the user, session start, last active step, + and end step. It also indicates whether the session is local. + + :ivar user: The user associated with this session. + :ivar start_step: The timestep when the session was started. + :ivar last_active_step: The last timestep when the session was active. + :ivar end_step: The timestep when the session ended, if applicable. + :ivar local: Indicates if the session is local. Defaults to True. + """ + + user: User + """The user associated with this session.""" + + start_step: int + """The timestep when the session was started.""" + + last_active_step: int + """The last timestep when the session was active.""" + + end_step: Optional[int] = None + """The timestep when the session ended, if applicable.""" + + local: bool = True + """Indicates if the session is a local session or a remote session. Defaults to True as a local session.""" + + @classmethod + def create(cls, user: User, timestep: int) -> UserSession: + """ + Creates a new instance of UserSession. + + This class method initialises a user session with the given user and timestep. + + :param user: The user associated with this session. + :param timestep: The timestep when the session is created. + :return: An instance of UserSession. + """ + user.num_of_logins += 1 + return UserSession(user=user, start_step=timestep, last_active_step=timestep) + + def describe_state(self) -> Dict: + """ + Describes the current state of the user session. + + :return: A dictionary representing the state of the user session. + """ + return self.model_dump() + + +class RemoteUserSession(UserSession): + """ + Represents a remote user session on the Node. + + This class extends the UserSession class to include additional attributes and methods specific to remote sessions. + + :ivar remote_ip_address: The IP address of the remote user. + :ivar local: Indicates that this is not a local session. Always set to False. + """ + + remote_ip_address: IPV4Address + """The IP address of the remote user.""" + + local: bool = False + """Indicates that this is not a local session. Always set to False.""" + + @classmethod + def create(cls, user: User, timestep: int, remote_ip_address: IPV4Address) -> RemoteUserSession: # noqa + """ + Creates a new instance of RemoteUserSession. + + This class method initialises a remote user session with the given user, timestep, and remote IP address. + + :param user: The user associated with this session. + :param timestep: The timestep when the session is created. + :param remote_ip_address: The IP address of the remote user. + :return: An instance of RemoteUserSession. + """ + return RemoteUserSession( + user=user, start_step=timestep, last_active_step=timestep, remote_ip_address=remote_ip_address + ) + + def describe_state(self) -> Dict: + """ + Describes the current state of the remote user session. + + This method extends the base describe_state method to include the remote IP address. + + :return: A dictionary representing the state of the remote user session. + """ + state = super().describe_state() + state["remote_ip_address"] = str(self.remote_ip_address) + return state + + +class UserSessionManager(Service): + """ + Manages user sessions on a Node, including local and remote sessions. + + This class handles authentication, session management, and session timeouts for users interacting with the Node. + """ + + local_session: Optional[UserSession] = None + """The current local user session, if any.""" + + remote_sessions: Dict[str, RemoteUserSession] = {} + """A dictionary of active remote user sessions.""" + + historic_sessions: List[UserSession] = Field(default_factory=list) + """A list of historic user sessions.""" + + local_session_timeout_steps: int = 30 + """The number of steps before a local session times out due to inactivity.""" + + remote_session_timeout_steps: int = 30 + """The number of steps before a remote session times out due to inactivity.""" + + max_remote_sessions: int = 3 + """The maximum number of concurrent remote sessions allowed.""" + + current_timestep: int = 0 + """The current timestep in the simulation.""" + + def __init__(self, **kwargs): + """ + Initializes a UserSessionManager instance. + + :param username: The username for the default admin user + :param password: The password for the default admin user + """ + kwargs["name"] = "UserSessionManager" + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] + super().__init__(**kwargs) + self.start() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _remote_login(request: RequestFormat, context: Dict) -> RequestResponse: + """Request should take the form [username, password, remote_ip_address].""" + username, password, remote_ip_address = request + response = RequestResponse.from_bool(self.remote_login(username, password, remote_ip_address)) + response.data = {"remote_hostname": self.parent.hostname, "username": username} + return response + + rm.add_request("remote_login", RequestType(func=_remote_login)) + + rm.add_request( + "remote_logout", + RequestType( + func=lambda request, context: RequestResponse.from_bool( + self.remote_logout(remote_session_id=request[0]) + ) + ), + ) + return rm + + def show(self, markdown: bool = False, include_session_id: bool = False, include_historic: bool = False): + """ + Displays a table of the user sessions on the Node. + + :param markdown: Whether to display the table in markdown format. + :param include_session_id: Whether to include session IDs in the table. + :param include_historic: Whether to include historic sessions in the table. + """ + headers = ["Session ID", "Username", "Type", "Remote IP", "Start Step", "Step Last Active", "End Step"] + + if not include_session_id: + headers = headers[1:] + + table = PrettyTable(headers) + + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.parent.hostname} User Sessions" + + def _add_session_to_table(user_session: UserSession): + """ + Adds a user session to the table for display. + + This helper function determines whether the session is local or remote and formats the session data + accordingly. It then adds the session data to the table. + + :param user_session: The user session to add to the table. + """ + session_type = "local" + remote_ip = "" + if isinstance(user_session, RemoteUserSession): + session_type = "remote" + remote_ip = str(user_session.remote_ip_address) + data = [ + user_session.uuid, + user_session.user.username, + session_type, + remote_ip, + user_session.start_step, + user_session.last_active_step, + user_session.end_step if user_session.end_step else "", + ] + if not include_session_id: + data = data[1:] + table.add_row(data) + + if self.local_session is not None: + _add_session_to_table(self.local_session) + + for user_session in self.remote_sessions.values(): + _add_session_to_table(user_session) + + if include_historic: + for user_session in self.historic_sessions: + _add_session_to_table(user_session) + + print(table.get_string(sortby="Step Last Active", reversesort=True)) + + def describe_state(self) -> Dict: + """ + Describes the current state of the UserSessionManager. + + :return: A dictionary representing the current state. + """ + state = super().describe_state() + state["current_local_user"] = None if not self.local_session else self.local_session.user.username + state["active_remote_sessions"] = list(self.remote_sessions.keys()) + return state + + @property + def _user_manager(self) -> UserManager: + """ + Returns the UserManager instance. + + :return: The UserManager instance. + """ + return self.software_manager.software["UserManager"] # noqa + + def pre_timestep(self, timestep: int) -> None: + """Apply any pre-timestep logic that helps make sure we have the correct observations.""" + self.current_timestep = timestep + inactive_sessions: list = [] + if self.local_session: + if self.local_session.last_active_step + self.local_session_timeout_steps <= timestep: + inactive_sessions.append(self.local_session) + + for session in self.remote_sessions: + remote_session = self.remote_sessions[session] + if remote_session.last_active_step + self.remote_session_timeout_steps <= timestep: + inactive_sessions.append(remote_session) + + for sessions in inactive_sessions: + self._timeout_session(sessions) + + def _timeout_session(self, session: UserSession) -> None: + """ + Handles session timeout logic. + + :param session: The session to be timed out. + """ + session.end_step = self.current_timestep + session_identity = session.user.username + if session.local: + self.local_session = None + session_type = "Local" + else: + self.remote_sessions.pop(session.uuid) + session_type = "Remote" + session_identity = f"{session_identity} {session.remote_ip_address}" + self.parent.terminal._connections.pop(session.uuid) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload={"type": "user_timeout", "connection_id": session.uuid}, + dest_port=PORT_LOOKUP["SSH"], + dest_ip_address=session.remote_ip_address, + ) + + self.sys_log.info(f"{self.name}: {session_type} {session_identity} session timeout due to inactivity") + + @property + def remote_session_limit_reached(self) -> bool: + """ + Checks if the maximum number of remote sessions has been reached. + + :return: True if the limit is reached, otherwise False. + """ + return len(self.remote_sessions) >= self.max_remote_sessions + + def validate_remote_session_uuid(self, remote_session_id: str) -> bool: + """ + Validates if a given remote session ID exists. + + :param remote_session_id: The remote session ID to validate. + :return: True if the session ID exists, otherwise False. + """ + return remote_session_id in self.remote_sessions + + def _login( + self, username: str, password: str, local: bool = True, remote_ip_address: Optional[IPv4Address] = None + ) -> Optional[str]: + """ + Logs a user in either locally or remotely. + + :param username: The username of the account. + :param password: The password of the account. + :param local: Whether the login is local or remote. + :param remote_ip_address: The remote IP address for remote login. + :return: The session ID if login is successful, otherwise None. + """ + if not self._can_perform_action(): + return None + + user = self._user_manager.authenticate_user(username=username, password=password) + + if not user: + self.sys_log.info(f"{self.name}: Incorrect username or password") + return None + + session_id = None + if local: + create_new_session = True + if self.local_session: + if self.local_session.user != user: + # logout the current user + self.local_logout() + else: + # not required as existing logged-in user attempting to re-login + create_new_session = False + + if create_new_session: + self.local_session = UserSession.create(user=user, timestep=self.current_timestep) + + session_id = self.local_session.uuid + else: + if not self.remote_session_limit_reached: + remote_session = RemoteUserSession.create( + user=user, timestep=self.current_timestep, remote_ip_address=remote_ip_address + ) + session_id = remote_session.uuid + self.remote_sessions[session_id] = remote_session + self.sys_log.info(f"{self.name}: User {user.username} logged in") + return session_id + + def local_login(self, username: str, password: str) -> Optional[str]: + """ + Logs a user in locally. + + :param username: The username of the account. + :param password: The password of the account. + :return: The session ID if login is successful, otherwise None. + """ + return self._login(username=username, password=password, local=True) + + @validate_call() + def remote_login(self, username: str, password: str, remote_ip_address: IPV4Address) -> Optional[str]: + """ + Logs a user in remotely. + + :param username: The username of the account. + :param password: The password of the account. + :param remote_ip_address: The remote IP address for the remote login. + :return: The session ID if login is successful, otherwise None. + """ + return self._login(username=username, password=password, local=False, remote_ip_address=remote_ip_address) + + def _logout(self, local: bool = True, remote_session_id: Optional[str] = None) -> bool: + """ + Logs a user out either locally or remotely. + + :param local: Whether the logout is local or remote. + :param remote_session_id: The remote session ID for remote logout. + :return: True if logout successful, otherwise False. + """ + if not self._can_perform_action(): + return False + session = None + if local and self.local_session: + session = self.local_session + session.end_step = self.current_timestep + self.local_session = None + + if not local and remote_session_id: + self.parent.terminal._disconnect(remote_session_id) + session = self.remote_sessions.pop(remote_session_id) + if session: + self.historic_sessions.append(session) + self.sys_log.info(f"{self.name}: User {session.user.username} logged out") + return True + return False + + def local_logout(self) -> bool: + """ + Logs out the current local user. + + :return: True if logout successful, otherwise False. + """ + return self._logout(local=True) + + def remote_logout(self, remote_session_id: str) -> bool: + """ + Logs out a remote user by session ID. + + :param remote_session_id: The remote session ID. + :return: True if logout successful, otherwise False. + """ + return self._logout(local=False, remote_session_id=remote_session_id) + + def _logout_user(self, user: Union[str, User]) -> bool: + """End a user session by username or user object.""" + if isinstance(user, str): + user = self._user_manager.users[user] # grab user object from username + for sess_id, session in self.remote_sessions.items(): + if session.user is user: + self._logout(local=False, remote_session_id=sess_id) + return True + if self.local_user_logged_in and self.local_session.user is user: + self.local_logout() + return True + return False + + @property + def local_user_logged_in(self) -> bool: + """ + Checks if a local user is currently logged in. + + :return: True if a local user is logged in, otherwise False. + """ + return self.local_session is not None + + +class Node(SimComponent): + """ + A basic Node class that represents a node on the network. + + This class manages the state of the node, including the NICs (Network Interface Cards), accounts, applications, + services, processes, file system, and various managers like ARP, ICMP, SessionManager, and SoftwareManager. + + :param hostname: The node hostname on the network. + :param operating_state: The node operating state, either ON or OFF. + """ + + hostname: str + "The node hostname on the network." + default_gateway: Optional[IPV4Address] = None + "The default gateway IP address for forwarding network traffic to other networks." + operating_state: NodeOperatingState = NodeOperatingState.OFF + "The hardware state of the node." + network_interfaces: Dict[str, NetworkInterface] = {} + "The Network Interfaces on the node." + network_interface: Dict[int, NetworkInterface] = {} + "The Network Interfaces on the node by port id." + dns_server: Optional[IPv4Address] = None + "List of IP addresses of DNS servers used for name resolution." + + accounts: Dict[str, Account] = {} + "All accounts on the node." + applications: Dict[str, Application] = {} + "All applications on the node." + services: Dict[str, Service] = {} + "All services on the node." + processes: Dict[str, Process] = {} + "All processes on the node." + file_system: FileSystem + "The nodes file system." + root: Path + "Root directory for simulation output." + sys_log: SysLog + session_manager: SessionManager + software_manager: SoftwareManager + + revealed_to_red: bool = False + "Informs whether the node has been revealed to a red agent." + + start_up_duration: int = 3 + "Time steps needed for the node to start up." + + start_up_countdown: int = 0 + "Time steps needed until node is booted up." + + shut_down_duration: int = 3 + "Time steps needed for the node to shut down." + + shut_down_countdown: int = 0 + "Time steps needed until node is shut down." + + is_resetting: bool = False + "If true, the node will try turning itself off then back on again." + + node_scan_duration: int = 10 + "How many timesteps until the whole node is scanned. Default 10 time steps." + + node_scan_countdown: int = 0 + "Time steps until scan is complete" + + red_scan_countdown: int = 0 + "Time steps until reveal to red scan is complete." + + SYSTEM_SOFTWARE: ClassVar[Dict[str, Type[Software]]] = {} + "Base system software that must be preinstalled." + + _registry: ClassVar[Dict[str, Type["Node"]]] = {} + """Registry of application types. Automatically populated when subclasses are defined.""" + + _identifier: ClassVar[str] = "unknown" + """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" + + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: + """ + Register a node type. + + :param identifier: Uniquely specifies an node class by name. Used for finding items by config. + :type identifier: str + :raises ValueError: When attempting to register an node with a name that is already allocated. + """ + if identifier == "default": + return + identifier = identifier.lower() + super().__init_subclass__(**kwargs) + if identifier in cls._registry: + raise ValueError(f"Tried to define new node {identifier}, but this name is already reserved.") + cls._registry[identifier] = cls + cls._identifier = identifier + + def __init__(self, **kwargs): + """ + Initialize the Node with various components and managers. + + This method initialises the ARP cache, ICMP handler, session manager, and software manager if they are not + provided. + """ + if not kwargs.get("sys_log"): + kwargs["sys_log"] = SysLog(kwargs["hostname"]) + if not kwargs.get("session_manager"): + kwargs["session_manager"] = SessionManager(sys_log=kwargs.get("sys_log")) + if not kwargs.get("root"): + kwargs["root"] = SIM_OUTPUT.path / kwargs["hostname"] + if not kwargs.get("file_system"): + kwargs["file_system"] = FileSystem(sys_log=kwargs["sys_log"], sim_root=kwargs["root"] / "fs") + if not kwargs.get("software_manager"): + kwargs["software_manager"] = SoftwareManager( + parent_node=self, + sys_log=kwargs.get("sys_log"), + session_manager=kwargs.get("session_manager"), + file_system=kwargs.get("file_system"), + dns_server=kwargs.get("dns_server"), + ) + super().__init__(**kwargs) + self._install_system_software() + self.session_manager.node = self + self.session_manager.software_manager = self.software_manager + + @property + def user_manager(self) -> Optional[UserManager]: + """The Nodes User Manager.""" + return self.software_manager.software.get("UserManager") # noqa + + @property + def user_session_manager(self) -> Optional[UserSessionManager]: + """The Nodes User Session Manager.""" + return self.software_manager.software.get("UserSessionManager") # noqa + + @property + def terminal(self) -> Optional[Terminal]: + """The Nodes Terminal.""" + return self.software_manager.software.get("Terminal") + + def local_login(self, username: str, password: str) -> Optional[str]: + """ + Attempt to log in to the node uas a local user. + + This method attempts to authenticate a local user with the given username and password. If successful, it + returns a session token. If authentication fails, it returns None. + + :param username: The username of the account attempting to log in. + :param password: The password of the account attempting to log in. + :return: A session token if the login is successful, otherwise None. + """ + return self.user_session_manager.local_login(username, password) + + def local_logout(self) -> None: + """ + Log out the current local user from the node. + + This method ends the current local user's session and invalidates the session token. + """ + return self.user_session_manager.local_logout() + + def ip_is_network_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool: + """ + Checks if a given IP address belongs to any of the nodes interfaces. + + :param ip_address: The IP address to check. + :param enabled_only: If True, only considers enabled network interfaces. + :return: True if the IP address is assigned to one of the nodes interfaces; False otherwise. + """ + for network_interface in self.network_interface.values(): + if not hasattr(network_interface, "ip_address"): + continue + if network_interface.ip_address == ip_address: + if enabled_only: + return network_interface.enabled + else: + return True + return False + + def setup_for_episode(self, episode: int): + """Reset the original state of the SimComponent.""" + super().setup_for_episode(episode=episode) + + # Reset File System + self.file_system.setup_for_episode(episode=episode) + + # Reset all Nics + for network_interface in self.network_interfaces.values(): + network_interface.setup_for_episode(episode=episode) + + for software in self.software_manager.software.values(): + software.setup_for_episode(episode=episode) + + if episode and self.sys_log: + self.sys_log.current_episode = episode + self.sys_log.setup_logger() + + class _NodeIsOnValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the node is on. + + This is useful because no actions should be being resolved if the node is off. + """ + + node: Node + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the node is on or off.""" + return self.node.operating_state == NodeOperatingState.ON + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return f"Cannot perform request on node '{self.node.hostname}' because it is not powered on." + + class _NodeIsOffValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the node is off. + + This is useful because some actions require the node to be in an off state. + """ + + node: Node + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the node is on or off.""" + return self.node.operating_state == NodeOperatingState.OFF + + @property + def fail_message(self) -> str: + """Message that is reported when a request is rejected by this validator.""" + return f"Cannot perform request on node '{self.node.hostname}' because it is not turned off." + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + + def _install_application(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Allows agents to install applications to the node. + + :param request: list containing the application name as the only element + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: Request response with a success code if the application was installed. + :rtype: RequestResponse + """ + application_name = request[0] + if self.software_manager.software.get(application_name): + self.sys_log.warning(f"Can't install {application_name}. It's already installed.") + return RequestResponse(status="success", data={"reason": "already installed"}) + application_class = Application._registry[application_name] + self.software_manager.install(application_class) + application_instance = self.software_manager.software.get(application_name) + self.applications[application_instance.uuid] = application_instance + _LOGGER.debug(f"Added application {application_instance.name} to node {self.hostname}") + self._application_request_manager.add_request( + application_name, RequestType(func=application_instance._request_manager) + ) + application_instance.install() + if application_name in self.software_manager.software: + return RequestResponse.from_bool(True) + else: + return RequestResponse.from_bool(False) + + def _uninstall_application(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Uninstall and completely remove application from this node. + + This method is useful for allowing agents to take this action. + + :param request: list containing the application name as the only element + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: Request response with a success code if the application was uninstalled. + :rtype: RequestResponse + """ + application_name = request[0] + if application_name not in self.software_manager.software: + self.sys_log.warning(f"Can't uninstall {application_name}. It's not installed.") + return RequestResponse.from_bool(False) + + application_instance = self.software_manager.software.get(application_name) + self.software_manager.uninstall(application_instance.name) + if application_instance.name not in self.software_manager.software: + return RequestResponse.from_bool(True) + else: + return RequestResponse.from_bool(False) + + _node_is_on = Node._NodeIsOnValidator(node=self) + _node_is_off = Node._NodeIsOffValidator(node=self) + + rm = super()._init_request_manager() + # since there are potentially many services, create an request manager that can map service name + self._service_request_manager = RequestManager() + rm.add_request("service", RequestType(func=self._service_request_manager, validator=_node_is_on)) + self._nic_request_manager = RequestManager() + rm.add_request("network_interface", RequestType(func=self._nic_request_manager, validator=_node_is_on)) + + rm.add_request("file_system", RequestType(func=self.file_system._request_manager, validator=_node_is_on)) + + # currently we don't have any applications nor processes, so these will be empty + self._process_request_manager = RequestManager() + rm.add_request("process", RequestType(func=self._process_request_manager, validator=_node_is_on)) + self._application_request_manager = RequestManager() + rm.add_request("application", RequestType(func=self._application_request_manager, validator=_node_is_on)) + + rm.add_request( + "scan", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.reveal_to_red()), validator=_node_is_on + ), + ) + + rm.add_request( + "shutdown", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.power_off()), validator=_node_is_on + ), + ) + rm.add_request( + "startup", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.power_on()), validator=_node_is_off + ), + ) + rm.add_request( + "reset", + RequestType(func=lambda request, context: RequestResponse.from_bool(self.reset()), validator=_node_is_on), + ) # TODO implement node reset + rm.add_request( + "logon", RequestType(func=lambda request, context: RequestResponse.from_bool(False), validator=_node_is_on) + ) # TODO implement logon request + rm.add_request( + "logoff", RequestType(func=lambda request, context: RequestResponse.from_bool(False), validator=_node_is_on) + ) # TODO implement logoff request + + self._os_request_manager = RequestManager() + self._os_request_manager.add_request( + "scan", + RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()), validator=_node_is_on), + ) + rm.add_request("os", RequestType(func=self._os_request_manager, validator=_node_is_on)) + + self._software_request_manager = RequestManager() + rm.add_request("software_manager", RequestType(func=self._software_request_manager, validator=_node_is_on)) + self._application_manager = RequestManager() + self._software_request_manager.add_request( + name="application", request_type=RequestType(func=self._application_manager) + ) + + self._application_manager.add_request(name="install", request_type=RequestType(func=_install_application)) + self._application_manager.add_request(name="uninstall", request_type=RequestType(func=_uninstall_application)) + + return rm + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "hostname": self.hostname, + "operating_state": self.operating_state.value, + "NICs": { + eth_num: network_interface.describe_state() + for eth_num, network_interface in self.network_interface.items() + }, + "file_system": self.file_system.describe_state(), + "applications": {app.name: app.describe_state() for app in self.applications.values()}, + "services": {svc.name: svc.describe_state() for svc in self.services.values()}, + "process": {proc.name: proc.describe_state() for proc in self.processes.values()}, + "revealed_to_red": self.revealed_to_red, + } + ) + return state + + def show(self, markdown: bool = False): + """Show function that calls both show NIC and show open ports.""" + self.show_nic(markdown) + self.show_open_ports(markdown) + + def show_open_ports(self, markdown: bool = False): + """Prints a table of the open ports on the Node.""" + table = PrettyTable(["Port"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.hostname} Open Ports" + for port in self.software_manager.get_open_ports(): + if port > 0: + table.add_row([port]) + print(table.get_string(sortby="Port")) + + @property + def has_enabled_network_interface(self) -> bool: + """ + Checks if the node has at least one enabled network interface. + + Iterates through all network interfaces associated with the node to determine if at least one is enabled. This + property is essential for determining the node's ability to communicate within the network. + + :return: True if there is at least one enabled network interface; otherwise, False. + """ + for network_interface in self.network_interfaces.values(): + if network_interface.enabled: + return True + return False + + def show_nic(self, markdown: bool = False): + """Prints a table of the NICs on the Node.""" + table = PrettyTable(["Port", "Type", "MAC Address", "Address", "Speed", "Status", "NMNE"]) + if markdown: + table.set_style(MARKDOWN) + table.align = "l" + table.title = f"{self.hostname} Network Interface Cards" + for port, network_interface in self.network_interface.items(): + ip_address = "" + if hasattr(network_interface, "ip_address"): + ip_address = f"{network_interface.ip_address}/{network_interface.ip_network.prefixlen}" + table.add_row( + [ + port, + network_interface.__class__.__name__, + network_interface.mac_address, + ip_address, + network_interface.speed, + "Enabled" if network_interface.enabled else "Disabled", + network_interface.nmne if network_interface.nmne_config.capture_nmne else "Disabled", + ] + ) + print(table) + + def apply_timestep(self, timestep: int): + """ + Apply a single timestep of simulation dynamics to this node. + + In this instance, if any multi-timestep processes are currently occurring + (such as starting up or shutting down), then they are brought one step closer to + being finished. + + :param timestep: The current timestep number. (Amount of time since simulation episode began) + :type timestep: int + """ + super().apply_timestep(timestep=timestep) + + for network_interface in self.network_interfaces.values(): + network_interface.apply_timestep(timestep=timestep) + + # count down to boot up + if self.start_up_countdown > 0: + self.start_up_countdown -= 1 + else: + if self.operating_state == NodeOperatingState.BOOTING: + self.operating_state = NodeOperatingState.ON + self.sys_log.info(f"{self.hostname}: Turned on") + for network_interface in self.network_interfaces.values(): + network_interface.enable() + + self._start_up_actions() + + # count down to shut down + if self.shut_down_countdown > 0: + self.shut_down_countdown -= 1 + else: + if self.operating_state == NodeOperatingState.SHUTTING_DOWN: + self.operating_state = NodeOperatingState.OFF + self.sys_log.info(f"{self.hostname}: Turned off") + self._shut_down_actions() + + # if resetting turn back on + if self.is_resetting: + self.is_resetting = False + self.power_on() + + # time steps which require the node to be on + if self.operating_state == NodeOperatingState.ON: + # node scanning + if self.node_scan_countdown > 0: + self.node_scan_countdown -= 1 + + if self.node_scan_countdown == 0: + # scan everything! + for process_id in self.processes: + self.processes[process_id].scan() + + # scan services + for service_id in self.services: + self.services[service_id].scan() + + # scan applications + for application_id in self.applications: + self.applications[application_id].scan() + + # scan file system + self.file_system.scan(instant_scan=True) + + if self.red_scan_countdown > 0: + self.red_scan_countdown -= 1 + + if self.red_scan_countdown == 0: + # scan processes + for process_id in self.processes: + self.processes[process_id].reveal_to_red() + + # scan services + for service_id in self.services: + self.services[service_id].reveal_to_red() + + # scan applications + for application_id in self.applications: + self.applications[application_id].reveal_to_red() + + # scan file system + self.file_system.reveal_to_red(instant_scan=True) + + for process_id in self.processes: + self.processes[process_id].apply_timestep(timestep=timestep) + + for service_id in self.services: + self.services[service_id].apply_timestep(timestep=timestep) + + for application_id in self.applications: + self.applications[application_id].apply_timestep(timestep=timestep) + + self.file_system.apply_timestep(timestep=timestep) + + def pre_timestep(self, timestep: int) -> None: + """Apply pre-timestep logic.""" + super().pre_timestep(timestep) + for network_interface in self.network_interfaces.values(): + network_interface.pre_timestep(timestep=timestep) + + for process_id in self.processes: + self.processes[process_id].pre_timestep(timestep=timestep) + + for service_id in self.services: + self.services[service_id].pre_timestep(timestep=timestep) + + for application_id in self.applications: + self.applications[application_id].pre_timestep(timestep=timestep) + + self.file_system.pre_timestep(timestep=timestep) + + def scan(self) -> bool: + """ + Scan the node and all the items within it. + + Scans the: + - Processes + - Services + - Applications + - Folders + - Files + + to the red agent. + """ + self.node_scan_countdown = self.node_scan_duration + return True + + def reveal_to_red(self) -> bool: + """ + Reveals the node and all the items within it to the red agent. + + Set all the: + - Processes + - Services + - Applications + - Folders + - Files + + `revealed_to_red` to `True`. + """ + self.red_scan_countdown = self.node_scan_duration + return True + + def power_on(self) -> bool: + """Power on the Node, enabling its NICs if it is in the OFF state.""" + if self.start_up_duration <= 0: + self.operating_state = NodeOperatingState.ON + self._start_up_actions() + self.sys_log.info("Power on") + for network_interface in self.network_interfaces.values(): + network_interface.enable() + return True + if self.operating_state == NodeOperatingState.OFF: + self.operating_state = NodeOperatingState.BOOTING + self.start_up_countdown = self.start_up_duration + return True + + return False + + def power_off(self) -> bool: + """Power off the Node, disabling its NICs if it is in the ON state.""" + if self.shut_down_duration <= 0: + self._shut_down_actions() + self.operating_state = NodeOperatingState.OFF + self.sys_log.info("Power off") + return True + if self.operating_state == NodeOperatingState.ON: + for network_interface in self.network_interfaces.values(): + network_interface.disable() + self.operating_state = NodeOperatingState.SHUTTING_DOWN + self.shut_down_countdown = self.shut_down_duration + return True + return False + + def reset(self) -> bool: + """ + Resets the node. + + Powers off the node and sets is_resetting to True. + Applying more timesteps will eventually turn the node back on. + """ + if self.operating_state.ON: + self.is_resetting = True + self.sys_log.info("Resetting") + self.power_off() + return True + return False + + def connect_nic(self, network_interface: NetworkInterface, port_name: Optional[str] = None): + """ + Connect a Network Interface to the node. + + :param network_interface: The NIC to connect. + :raise NetworkError: If the NIC is already connected. + """ + if network_interface.uuid not in self.network_interface: + self.network_interfaces[network_interface.uuid] = network_interface + new_nic_num = len(self.network_interfaces) + self.network_interface[new_nic_num] = network_interface + network_interface._connected_node = self + network_interface.port_num = new_nic_num + if port_name: + network_interface.port_name = port_name + network_interface.parent = self + self.sys_log.info(f"Connected Network Interface {network_interface}") + if self.operating_state == NodeOperatingState.ON: + network_interface.enable() + self._nic_request_manager.add_request(new_nic_num, RequestType(func=network_interface._request_manager)) + else: + msg = f"Cannot connect NIC {network_interface} as it is already connected" + self.sys_log.logger.warning(msg) + raise NetworkError(msg) + + def disconnect_nic(self, network_interface: Union[NetworkInterface, str]): + """ + Disconnect a NIC (Network Interface Card) from the node. + + :param network_interface: The NIC to Disconnect, or its UUID. + :raise NetworkError: If the NIC is not connected. + """ + if isinstance(network_interface, str): + network_interface = self.network_interfaces.get(network_interface) + if network_interface or network_interface.uuid in self.network_interfaces: + network_interface_num = -1 + for port, _network_interface in self.network_interface.items(): + if network_interface == _network_interface: + self.network_interface.pop(port) + network_interface_num = port + break + self.network_interfaces.pop(network_interface.uuid) + network_interface.parent = None + network_interface.disable() + self.sys_log.info(f"Disconnected Network Interface {network_interface}") + if network_interface_num != -1: + self._nic_request_manager.remove_request(network_interface_num) + else: + msg = f"Cannot disconnect Network Interface {network_interface} as it is not connected" + self.sys_log.logger.warning(msg) + raise NetworkError(msg) + + def ping(self, target_ip_address: Union[IPv4Address, str], pings: int = 4) -> bool: + """ + Ping an IP address, performing a standard ICMP echo request/response. + + :param target_ip_address: The target IP address to ping. + :param pings: The number of pings to attempt, default is 4. + :return: True if the ping is successful, otherwise False. + """ + if not isinstance(target_ip_address, IPv4Address): + target_ip_address = IPv4Address(target_ip_address) + if self.software_manager.icmp: + return self.software_manager.icmp.ping(target_ip_address, pings) + return False + + @abstractmethod + def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): + """ + Receive a Frame from the connected NIC and process it. + + This is an abstract implementation of receive_frame with some very basic functionality (ARP population). All + Node subclasses should have their own implementation of receive_frame that first calls super().receive_frame( + ) before implementing its own internal receive_frame logic. + + :param frame: The Frame being received. + :param from_network_interface: The Network Interface that received the frame. + """ + if self.operating_state == NodeOperatingState.ON: + if frame.ip: + if self.software_manager.arp: + self.software_manager.arp.add_arp_cache_entry( + ip_address=frame.ip.src_ip_address, + mac_address=frame.ethernet.src_mac_addr, + network_interface=from_network_interface, + ) + else: + return + + def _shut_down_actions(self): + """Actions to perform when the node is shut down.""" + # Turn off all the services in the node + for service_id in self.services: + self.services[service_id].stop() + + # Turn off all the applications in the node + for app_id in self.applications: + self.applications[app_id].close() + + # Turn off all processes in the node + # for process_id in self.processes: + # self.processes[process_id] + + def _start_up_actions(self): + """Actions to perform when the node is starting up.""" + # Turn on all the services in the node + for service_id in self.services: + self.services[service_id].start() + + # Turn on all the applications in the node + for app_id in self.applications: + self.applications[app_id].run() + + # Turn off all processes in the node + # for process_id in self.processes: + # self.processes[process_id] + + def _install_system_software(self) -> None: + """Preinstall required software.""" + for _, software_class in self.SYSTEM_SOFTWARE.items(): + self.software_manager.install(software_class) + + def __contains__(self, item: Any) -> bool: + if isinstance(item, Service): + return item.uuid in self.services + elif isinstance(item, Application): + return item.uuid in self.applications + return None diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 836b79af..9c7b91ce 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1 +1,338 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from ipaddress import IPv4Address +from typing import Dict, Optional + +from primaite import getLogger +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.file_system.file_system import File +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import Port, PORT_LOOKUP + +_LOGGER = getLogger(__name__) + + +class FTPClient(FTPServiceABC): + """ + A class for simulating an FTP client service. + + This class inherits from the `Service` class and provides methods to emulate FTP + RFC 959: https://datatracker.ietf.org/doc/html/rfc959 + """ + + def __init__(self, **kwargs): + kwargs["name"] = "FTPClient" + kwargs["port"] = PORT_LOOKUP["FTP"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] + super().__init__(**kwargs) + self.start() + + def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ + rm = super()._init_request_manager() + + def _send_data_request(request: RequestFormat, context: Dict) -> RequestResponse: + """ + Request for sending data via the ftp_client using the request options parameters. + + :param request: Request with one element containing a dict of parameters for the send method. + :type request: RequestFormat + :param context: additional context for resolving this action, currently unused + :type context: dict + :return: RequestResponse object with a success code reflecting whether the configuration could be applied. + :rtype: RequestResponse + """ + dest_ip = request[-1].get("dest_ip_address") + dest_ip = None if dest_ip is None else IPv4Address(dest_ip) + + # Missing FTP Options results is an automatic failure. + src_folder = request[-1].get("src_folder_name", None) + src_file_name = request[-1].get("src_file_name", None) + dest_folder = request[-1].get("dest_folder_name", None) + dest_file_name = request[-1].get("dest_file_name", None) + + if not self.file_system.access_file(folder_name=src_folder, file_name=src_file_name): + self.sys_log.debug( + f"{self.name}: Received a FTP Request to transfer file: {src_file_name} to Remote IP: {dest_ip}." + ) + return RequestResponse( + status="failure", + data={ + "reason": "Unable to locate given file on local file system. Perhaps given options are invalid?" + }, + ) + + return RequestResponse.from_bool( + self.send_file( + dest_ip_address=dest_ip, + src_folder_name=src_folder, + src_file_name=src_file_name, + dest_folder_name=dest_folder, + dest_file_name=dest_file_name, + ) + ) + + rm.add_request("send", request_type=RequestType(func=_send_data_request)), + return rm + + def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ + # if client service is down, return error + if not self._can_perform_action(): + payload.status_code = FTPStatusCode.ERROR + return payload + + self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}") + + # process client specific commands, otherwise call super + return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) + + def _connect_to_server( + self, + dest_ip_address: Optional[IPv4Address] = None, + dest_port: Optional[Port] = PORT_LOOKUP["FTP"], + session_id: Optional[str] = None, + is_reattempt: Optional[bool] = False, + ) -> bool: + """ + Connects the client to a given FTP server. + + :param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional. + :type: dest_ip_address: Optional[IPv4Address] + :param: dest_port: Port of the FTP server the client needs to connect to. Optional. + :type: dest_port: Optional[Port] + :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False. + :type: is_reattempt: Optional[bool] + """ + # make sure the service is running before attempting + if not self._can_perform_action(): + return False + + # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now + # create FTP packet + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=PORT_LOOKUP["FTP"]) + + if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id): + if payload.status_code == FTPStatusCode.OK: + self.sys_log.info( + f"{self.name}: Successfully connected to FTP Server " + f"{dest_ip_address} via port {payload.ftp_command_args}" + ) + self.add_connection(connection_id="server_connection", session_id=session_id) + return True + else: + if is_reattempt: + # reattempt failed + self.sys_log.warning( + f"{self.name}: Unable to connect to FTP Server " + f"{dest_ip_address} via port {payload.ftp_command_args}" + ) + return False + else: + # try again + self._connect_to_server( + dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, is_reattempt=True + ) + else: + self.sys_log.warning(f"{self.name}: Unable to send FTPPacket") + return False + + def _disconnect_from_server( + self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = PORT_LOOKUP["FTP"] + ) -> bool: + """ + Connects the client from a given FTP server. + + :param: dest_ip_address: IP address of the FTP server the client needs to disconnect from. Optional. + :type: dest_ip_address: Optional[IPv4Address] + :param: dest_port: Port of the FTP server the client needs to disconnect from. Optional. + :type: dest_port: Optional[Port] + :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. + :type: is_reattempt: Optional[bool] + """ + # send a disconnect request payload to FTP server + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port + ) + return payload.status_code == FTPStatusCode.OK + + def send_file( + self, + dest_ip_address: IPv4Address, + src_folder_name: str, + src_file_name: str, + dest_folder_name: str, + dest_file_name: str, + dest_port: Optional[Port] = PORT_LOOKUP["FTP"], + session_id: Optional[str] = None, + ) -> bool: + """ + Send a file to a target IP address. + + The function checks if the file exists in the FTP Client host. + The STOR command is then sent to the FTP Server. + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: IPv4Address + + :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. + :type: src_folder_name: str + + :param: src_file_name: The name of the file to send to the FTP Server. + :type: src_file_name: str + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. + :type: dest_port: Optional[Port] + + :param: session_id: The id of the session + :type: session_id: Optional[str] + """ + # check if the file to transfer exists on the client + file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) + if not file_to_transfer: + self.sys_log.warning(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}") + return False + + # check if FTP is currently connected to IP + self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) + + if not len(self.connections): + return False + else: + self.sys_log.info(f"Sending file {src_folder_name}/{src_file_name} to {str(dest_ip_address)}") + # send STOR request + if self._send_data( + file=file_to_transfer, + dest_folder_name=dest_folder_name, + dest_file_name=dest_file_name, + dest_ip_address=dest_ip_address, + dest_port=dest_port, + ): + return self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port) + + return False + + def request_file( + self, + dest_ip_address: IPv4Address, + src_folder_name: str, + src_file_name: str, + dest_folder_name: str, + dest_file_name: str, + dest_port: Optional[Port] = PORT_LOOKUP["FTP"], + ) -> bool: + """ + Request a file from a target IP address. + + Sends a RETR command to the FTP Server. + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: IPv4Address + + :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. + :type: src_folder_name: str + + :param: src_file_name: The name of the file to send to the FTP Server. + :type: src_file_name: str + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. + :type: dest_port: Optional[int] + """ + # check if FTP is currently connected to IP + self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) + + if not len(self.connections): + return False + else: + # send retrieve request + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.RETR, + ftp_command_args={ + "src_folder_name": src_folder_name, + "src_file_name": src_file_name, + "dest_file_name": dest_file_name, + "dest_folder_name": dest_folder_name, + }, + ) + self.sys_log.info(f"Requesting file {src_folder_name}/{src_file_name} from {str(dest_ip_address)}") + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port + ) + + # the payload should have ok status code + if payload.status_code == FTPStatusCode.OK: + self.sys_log.info(f"{self.name}: File {src_folder_name}/{src_file_name} found in FTP server.") + return True + else: + self.sys_log.error(f"{self.name}: File {src_folder_name}/{src_file_name} does not exist in FTP server") + return False + + def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: + """ + Receives a payload from the SessionManager. + + :param: payload: FTPPacket payload. + :type: payload: FTPPacket + + :param: session_id: ID of the session. Optional. + :type: session_id: Optional[str] + """ + if not isinstance(payload, FTPPacket): + self.sys_log.warning(f"{self.name}: Payload is not an FTP packet") + self.sys_log.debug(f"{self.name}: {payload}") + return False + + """ + Ignore ftp payload if status code is None. + + This helps prevent an FTP request loop - FTP client and servers can exist on + the same node. + """ + if not self._can_perform_action(): + return False + + if payload.status_code is None: + self.sys_log.error(f"FTP Server could not be found - Error Code: {FTPStatusCode.NOT_FOUND.value}") + return False + + # if PORT succeeded, add the connection as an active connection list + if payload.ftp_command is FTPCommand.PORT and payload.status_code is FTPStatusCode.OK: + self.add_connection(connection_id=session_id, session_id=session_id) + + # if QUIT succeeded, remove the session from active connection list + if payload.ftp_command is FTPCommand.QUIT and payload.status_code is FTPStatusCode.OK: + self.terminate_connection(connection_id=session_id) + + self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}") + + self._process_ftp_command(payload=payload, session_id=session_id) + return True diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 836b79af..e26e77f6 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -1 +1,545 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from __future__ import annotations + +from abc import abstractmethod +from datetime import datetime +from ipaddress import IPv4Address +from typing import Any, Dict, List, Optional, Union +from uuid import uuid4 + +from pydantic import BaseModel + +from primaite.interface.request import RequestFormat, RequestResponse +from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.protocols.ssh import ( + SSHConnectionMessage, + SSHPacket, + SSHTransportMessage, + SSHUserCredentials, +) +from primaite.simulator.system.core.software_manager import SoftwareManager +from primaite.simulator.system.services.service import Service, ServiceOperatingState +from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.port import PORT_LOOKUP + + +# TODO 2824: Since remote terminal connections and remote user sessions are the same thing, we could refactor +# the terminal to leverage the user session manager's list. This way we avoid potential bugs and code ducplication +class TerminalClientConnection(BaseModel): + """ + TerminalClientConnection Class. + + This class is used to record current User Connections to the Terminal class. + """ + + parent_terminal: Terminal + """The parent Node that this connection was created on.""" + + ssh_session_id: str = None + """Session ID that connection is linked to, used for sending commands via session manager.""" + + connection_uuid: str = None + """Connection UUID""" + + connection_request_id: str = None + """Connection request ID""" + + time: datetime = None + """Timestamp connection was created.""" + + ip_address: IPv4Address + """Source IP of Connection""" + + is_active: bool = True + """Flag to state whether the connection is active or not""" + + def __str__(self) -> str: + return f"{self.__class__.__name__}(connection_id: '{self.connection_uuid}, ip_address: {self.ip_address}')" + + def __repr__(self) -> str: + return self.__str__() + + def __getitem__(self, key: Any) -> Any: + return getattr(self, key) + + @property + def client(self) -> Optional[Terminal]: + """The Terminal that holds this connection.""" + return self.parent_terminal + + def disconnect(self) -> bool: + """Disconnect the session.""" + return self.parent_terminal._disconnect(connection_uuid=self.connection_uuid) + + @abstractmethod + def execute(self, command: Any) -> bool: + """Execute a given command.""" + pass + + +class LocalTerminalConnection(TerminalClientConnection): + """ + LocalTerminalConnectionClass. + + This class represents a local terminal when connected. + """ + + ip_address: str = "Local Connection" + + def execute(self, command: Any) -> Optional[RequestResponse]: + """Execute a given command on local Terminal.""" + if self.parent_terminal.operating_state != ServiceOperatingState.RUNNING: + self.parent_terminal.sys_log.warning("Cannot process command as system not running") + return None + if not self.is_active: + self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") + return None + return self.parent_terminal.execute(command) + + +class RemoteTerminalConnection(TerminalClientConnection): + """ + RemoteTerminalConnection Class. + + This class acts as broker between the terminal and remote. + + """ + + def execute(self, command: Any) -> bool: + """Execute a given command on the remote Terminal.""" + if self.parent_terminal.operating_state != ServiceOperatingState.RUNNING: + self.parent_terminal.sys_log.warning("Cannot process command as system not running") + return False + if not self.is_active: + self.parent_terminal.sys_log.warning("Connection inactive, cannot execute") + return False + # Send command to remote terminal to process. + + transport_message: SSHTransportMessage = SSHTransportMessage.SSH_MSG_SERVICE_REQUEST + connection_message: SSHConnectionMessage = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA + + payload: SSHPacket = SSHPacket( + transport_message=transport_message, + connection_message=connection_message, + connection_request_uuid=self.connection_request_id, + connection_uuid=self.connection_uuid, + ssh_command=command, + ) + + return self.parent_terminal.send(payload=payload, session_id=self.ssh_session_id) + + +class Terminal(Service): + """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" + + _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} + """Dictionary of connect requests made to remote nodes.""" + + def __init__(self, **kwargs): + kwargs["name"] = "Terminal" + kwargs["port"] = PORT_LOOKUP["SSH"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] + super().__init__(**kwargs) + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + return state + + def show(self, markdown: bool = False): + """ + Display the remote connections to this terminal instance in tabular format. + + :param markdown: Whether to display the table in Markdown format or not. Default is `False`. + """ + self.show_connections(markdown=markdown) + + def _init_request_manager(self) -> RequestManager: + """Initialise Request manager.""" + rm = super()._init_request_manager() + + def _remote_login(request: RequestFormat, context: Dict) -> RequestResponse: + login = self._send_remote_login(username=request[0], password=request[1], ip_address=request[2]) + if login: + return RequestResponse( + status="success", + data={ + "ip_address": str(login.ip_address), + "username": request[0], + }, + ) + else: + return RequestResponse(status="failure", data={}) + + rm.add_request( + "ssh_to_remote", + request_type=RequestType(func=_remote_login), + ) + + def _remote_logoff(request: RequestFormat, context: Dict) -> RequestResponse: + """Logoff from remote connection.""" + ip_address = IPv4Address(request[0]) + remote_connection = self._get_connection_from_ip(ip_address=ip_address) + if remote_connection: + outcome = self._disconnect(remote_connection.connection_uuid) + if outcome: + return RequestResponse(status="success", data={}) + + return RequestResponse(status="failure", data={}) + + rm.add_request("remote_logoff", request_type=RequestType(func=_remote_logoff)) + + def remote_execute_request(request: RequestFormat, context: Dict) -> RequestResponse: + """Execute an instruction.""" + ip_address: IPv4Address = IPv4Address(request[0]) + command: str = request[1]["command"] + remote_connection = self._get_connection_from_ip(ip_address=ip_address) + if remote_connection: + outcome = remote_connection.execute(command) + if outcome: + return RequestResponse( + status="success", + data={}, + ) + else: + return RequestResponse( + status="failure", + data={}, + ) + + rm.add_request( + "send_remote_command", + request_type=RequestType(func=remote_execute_request), + ) + + return rm + + def execute(self, command: List[Any]) -> Optional[RequestResponse]: + """Execute a passed ssh command via the request manager.""" + return self.parent.apply_request(command) + + def _get_connection_from_ip(self, ip_address: IPv4Address) -> Optional[RemoteTerminalConnection]: + """Find Remote Terminal Connection from a given IP.""" + for connection in self._connections.values(): + if connection.ip_address == ip_address: + return connection + + def _create_local_connection(self, connection_uuid: str, session_id: str) -> TerminalClientConnection: + """Create a new connection object and amend to list of active connections. + + :param connection_uuid: Connection ID of the new local connection + :param session_id: Session ID of the new local connection + :return: TerminalClientConnection object + """ + new_connection = LocalTerminalConnection( + parent_terminal=self, + connection_uuid=connection_uuid, + ssh_session_id=session_id, + time=datetime.now(), + ) + self._connections[connection_uuid] = new_connection + self._client_connection_requests[connection_uuid] = new_connection + + return new_connection + + def login( + self, username: str, password: str, ip_address: Optional[IPv4Address] = None + ) -> Optional[TerminalClientConnection]: + """Login to the terminal. Will attempt a remote login if ip_address is given, else local. + + :param: username: Username used to connect to the remote node. + :type: username: str + + :param: password: Password used to connect to the remote node + :type: password: str + + :param: ip_address: Target Node IP address for login attempt. If None, login is assumed local. + :type: ip_address: Optional[IPv4Address] + """ + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.warning(f"{self.name}: Cannot login as service is not running.") + return None + if ip_address: + # Assuming that if IP is passed we are connecting to remote + return self._send_remote_login(username=username, password=password, ip_address=ip_address) + else: + return self._process_local_login(username=username, password=password) + + def _process_local_login(self, username: str, password: str) -> Optional[TerminalClientConnection]: + """Local session login to terminal. + + :param username: Username for login. + :param password: Password for login. + :return: boolean, True if successful, else False + """ + # TODO: Un-comment this when UserSessionManager is merged. + connection_uuid = self.parent.user_session_manager.local_login(username=username, password=password) + if connection_uuid: + self.sys_log.info(f"{self.name}: Login request authorised, connection uuid: {connection_uuid}") + # Add new local session to list of connections and return + return self._create_local_connection(connection_uuid=connection_uuid, session_id="Local_Connection") + else: + self.sys_log.warning(f"{self.name}: Login failed, incorrect Username or Password") + return None + + def _validate_client_connection_request(self, connection_id: str) -> bool: + """Check that client_connection_id is valid.""" + return connection_id in self._client_connection_requests + + def _check_client_connection(self, connection_id: str) -> bool: + """Check that client_connection_id is valid.""" + if not self.parent.user_session_manager.validate_remote_session_uuid(connection_id): + self._disconnect(connection_id) + return False + return connection_id in self._connections + + def _send_remote_login( + self, + username: str, + password: str, + ip_address: IPv4Address, + connection_request_id: Optional[str] = None, + is_reattempt: bool = False, + ) -> Optional[RemoteTerminalConnection]: + """Send a remote login attempt and connect to Node. + + :param: username: Username used to connect to the remote node. + :type: username: str + :param: password: Password used to connect to the remote node + :type: password: str + :param: ip_address: Target Node IP address for login attempt. + :type: ip_address: IPv4Address + :param: connection_request_id: Connection Request ID, if not provided, a new one is generated + :type: connection_request_id: Optional[str] + :param: is_reattempt: True if the request has been reattempted. Default False. + :type: is_reattempt: Optional[bool] + :return: RemoteTerminalConnection: Connection Object for sending further commands if successful, else False. + """ + connection_request_id = connection_request_id or str(uuid4()) + if is_reattempt: + valid_connection_request = self._validate_client_connection_request(connection_id=connection_request_id) + if valid_connection_request: + remote_terminal_connection = self._client_connection_requests.pop(connection_request_id) + if isinstance(remote_terminal_connection, RemoteTerminalConnection): + self.sys_log.info(f"{self.name}: Remote Connection to {ip_address} authorised.") + return remote_terminal_connection + else: + self.sys_log.warning(f"{self.name}: Connection request {connection_request_id} declined") + return None + else: + self.sys_log.warning(f"{self.name}: Remote connection to {ip_address} declined.") + return None + + self.sys_log.info( + f"{self.name}: Sending Remote login attempt to {ip_address}. Connection_id is {connection_request_id}" + ) + transport_message: SSHTransportMessage = SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST + connection_message: SSHConnectionMessage = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA + user_details: SSHUserCredentials = SSHUserCredentials(username=username, password=password) + + payload_contents = { + "type": "login_request", + "username": username, + "password": password, + "connection_request_id": connection_request_id, + } + + payload: SSHPacket = SSHPacket( + payload=payload_contents, + transport_message=transport_message, + connection_message=connection_message, + user_account=user_details, + connection_request_uuid=connection_request_id, + ) + + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=ip_address, dest_port=self.port + ) + return self._send_remote_login( + username=username, + password=password, + ip_address=ip_address, + is_reattempt=True, + connection_request_id=connection_request_id, + ) + + def _create_remote_connection( + self, connection_id: str, connection_request_id: str, session_id: str, source_ip: str + ) -> None: + """Create a new TerminalClientConnection Object. + + :param: connection_request_id: Connection Request ID + :type: connection_request_id: str + + :param: session_id: Session ID of connection. + :type: session_id: str + """ + client_connection = RemoteTerminalConnection( + parent_terminal=self, + ssh_session_id=session_id, + connection_uuid=connection_id, + ip_address=source_ip, + connection_request_id=connection_request_id, + time=datetime.now(), + ) + self._connections[connection_id] = client_connection + self._client_connection_requests[connection_request_id] = client_connection + + def receive(self, session_id: str, payload: Union[SSHPacket, Dict], **kwargs) -> bool: + """ + Receive a payload from the Software Manager. + + :param payload: A payload to receive. + :param session_id: The session id the payload relates to. + :return: True. + """ + source_ip = kwargs["frame"].ip.src_ip_address + self.sys_log.info(f"{self.name}: Received payload: {payload}. Source: {source_ip}") + if isinstance(payload, SSHPacket): + if payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST: + # validate & add connection + # TODO: uncomment this as part of 2781 + username = payload.user_account.username + password = payload.user_account.password + connection_id = self.parent.user_session_manager.remote_login( + username=username, password=password, remote_ip_address=source_ip + ) + if connection_id: + connection_request_id = payload.connection_request_uuid + self.sys_log.info(f"{self.name}: Connection authorised, session_id: {session_id}") + self._create_remote_connection( + connection_id=connection_id, + connection_request_id=connection_request_id, + session_id=session_id, + source_ip=source_ip, + ) + + transport_message = SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS + connection_message = SSHConnectionMessage.SSH_MSG_CHANNEL_DATA + + payload_contents = { + "type": "login_success", + "username": username, + "password": password, + "connection_request_id": connection_request_id, + "connection_id": connection_id, + } + payload: SSHPacket = SSHPacket( + payload=payload_contents, + transport_message=transport_message, + connection_message=connection_message, + connection_request_uuid=connection_request_id, + connection_uuid=connection_id, + ) + + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_port=self.port, session_id=session_id + ) + elif payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS: + self.sys_log.info(f"{self.name}: Login Successful") + self._create_remote_connection( + connection_id=payload.connection_uuid, + connection_request_id=payload.connection_request_uuid, + session_id=session_id, + source_ip=source_ip, + ) + + elif payload.transport_message == SSHTransportMessage.SSH_MSG_SERVICE_REQUEST: + # Requesting a command to be executed + self.sys_log.info(f"{self.name}: Received command to execute") + command = payload.ssh_command + valid_connection = self._check_client_connection(payload.connection_uuid) + if valid_connection: + remote_session = self.software_manager.node.user_session_manager.remote_sessions.get( + payload.connection_uuid + ) + remote_session.last_active_step = self.software_manager.node.user_session_manager.current_timestep + self.execute(command) + return True + else: + self.sys_log.error( + f"{self.name}: Connection UUID:{payload.connection_uuid} is not valid. Rejecting Command." + ) + + if isinstance(payload, dict) and payload.get("type"): + if payload["type"] == "disconnect": + connection_id = payload["connection_id"] + valid_id = self._check_client_connection(connection_id) + if valid_id: + self.sys_log.info(f"{self.name}: Received disconnect command for {connection_id=} from remote.") + self._disconnect(payload["connection_id"]) + self.parent.user_session_manager.remote_logout(remote_session_id=connection_id) + else: + self.sys_log.error(f"{self.name}: No Active connection held for received connection ID.") + + if payload["type"] == "user_timeout": + connection_id = payload["connection_id"] + valid_id = connection_id in self._connections + if valid_id: + connection = self._connections.pop(connection_id) + connection.is_active = False + self.sys_log.info(f"{self.name}: Connection {connection_id} disconnected due to inactivity.") + else: + self.sys_log.error(f"{self.name}: Connection {connection_id} is invalid.") + + return True + + def _disconnect(self, connection_uuid: str) -> bool: + """Disconnect connection. + + :param connection_uuid: Connection ID that we want to disconnect. + :return True if successful, False otherwise. + """ + # TODO: Handle the possibility of attempting to disconnect + if not self._connections: + self.sys_log.warning(f"{self.name}: No remote connection present") + return False + + connection = self._connections.pop(connection_uuid, None) + if not connection: + return False + connection.is_active = False + + if isinstance(connection, RemoteTerminalConnection): + # Send disconnect command via software manager + session_id = connection.ssh_session_id + + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload={"type": "disconnect", "connection_id": connection_uuid}, + dest_port=self.port, + session_id=session_id, + ) + self.sys_log.info(f"{self.name}: Disconnected {connection_uuid}") + return True + + elif isinstance(connection, LocalTerminalConnection): + self.parent.user_session_manager.local_logout() + return True + + def send( + self, payload: SSHPacket, dest_ip_address: Optional[IPv4Address] = None, session_id: Optional[str] = None + ) -> bool: + """ + Send a payload out from the Terminal. + + :param payload: The payload to be sent. + :param dest_up_address: The IP address of the payload destination. + """ + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.warning(f"{self.name}: Cannot send commands when Operating state is {self.operating_state}!") + return False + + self.sys_log.debug(f"{self.name}: Sending payload: {payload}") + return super().send( + payload=payload, dest_ip_address=dest_ip_address, dest_port=self.port, session_id=session_id + ) diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index 836b79af..fc7aa69c 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -1 +1,109 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from typing import Tuple + +import pytest + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.system.applications.application import Application, ApplicationOperatingState + + +@pytest.fixture(scope="function") +def populated_node(application_class) -> Tuple[Application, Computer]: + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + shut_down_duration=0, + ) + computer.power_on() + computer.software_manager.install(application_class) + + app = computer.software_manager.software.get("DummyApplication") + app.run() + + return app, computer + + +def test_application_on_offline_node(application_class): + """Test to check that the application cannot be interacted with when node it is on is off.""" + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + shut_down_duration=0, + ) + computer.software_manager.install(application_class) + + app: Application = computer.software_manager.software.get("DummyApplication") + + computer.power_off() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + app.run() + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_server_turns_off_application(populated_node): + """Check that the application is turned off when the server is turned off""" + app, computer = populated_node + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_application_cannot_be_turned_on_when_computer_is_off(populated_node): + """Check that the application cannot be started when the computer is off.""" + app, computer = populated_node + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + app.run() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_computer_runs_applications(populated_node): + """Check that turning on the computer will turn on applications.""" + app, computer = populated_node + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + computer.power_on() + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + computer.power_on() + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING From 2108b914e332f1c9d1628a481dcead6a43336032 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 2 Jan 2025 17:41:24 +0000 Subject: [PATCH 100/224] #2869 - New Year, new changes. Actioning review comments and some changes following self-review and catchup --- .pre-commit-config.yaml | 12 +- .../how_to_guides/extensible_agents.rst | 45 ++++++- .../agent/{scripted_agents => }/interface.py | 124 ++++++------------ src/primaite/game/agent/rewards.py | 2 +- .../game/agent/scripted_agents/__init__.py | 2 +- .../agent/scripted_agents/abstract_tap.py | 4 +- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 40 +++--- src/primaite/game/game.py | 5 +- src/primaite/session/environment.py | 2 +- src/primaite/session/ray_envs.py | 2 +- tests/conftest.py | 2 +- ...software_installation_and_configuration.py | 2 +- .../test_application_request_permission.py | 2 +- .../actions/test_c2_suite_actions.py | 2 +- .../actions/test_file_request_permission.py | 2 +- .../actions/test_folder_request_permission.py | 2 +- .../actions/test_nic_request_permission.py | 2 +- .../actions/test_node_request_permission.py | 2 +- .../test_service_request_permission.py | 2 +- .../actions/test_terminal_actions.py | 2 +- .../observations/test_nic_observations.py | 2 +- .../game_layer/test_RNG_seed.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_rewards.py | 2 +- .../test_c2_suite_integration.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 2 +- .../_system/_services/test_terminal.py | 2 +- 28 files changed, 130 insertions(+), 144 deletions(-) rename src/primaite/game/agent/{scripted_agents => }/interface.py (69%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3088dc1d..df3bb504 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - - repo: local - hooks: - - id: ensure-copyright-clause - name: ensure copyright clause - entry: python copyright_clause_pre_commit_hook.py - language: python + # - repo: local + # hooks: + # - id: ensure-copyright-clause + # name: ensure copyright clause + # entry: python copyright_clause_pre_commit_hook.py + # language: python - repo: http://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 6ccb80cd..b7c17b83 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -7,13 +7,13 @@ Extensible Agents ***************** -Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents. +Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents. Developing Agents for PrimAITE ============================== -Agents within PrimAITE, follow the shown inheritance structure, and +Agents within PrimAITE, follow the shown inheritance structure below. # TODO: Turn this into an inheritance diagram @@ -32,7 +32,6 @@ AbstractAgent | | | | - RandomAgent | - | | - ProxyAgent | | - ControlledAgent @@ -41,6 +40,8 @@ AbstractAgent #. **ConfigSchema**: Configurable items within a new agent within PrimAITE should contain a ``ConfigSchema`` which holds all configurable variables of the agent. This should not include parameters related to its *state*. + Agent generation will fail if incorrect parameters are passed to the ConfigSchema, for the chosen Agent. + .. code-block:: python @@ -49,7 +50,7 @@ AbstractAgent config: "ExampleAgent.ConfigSchema" """Agent configuration""" - num_executions: int + num_executions: int = 0 """Number of action executions by agent""" class ConfigSchema(AbstractAgent.ConfigSchema): @@ -60,9 +61,43 @@ AbstractAgent action_interval: int """Number of steps between agent actions""" + + .. code-block:: YAML + + - ref: example_green_agent + team: GREEN + type: ExampleAgent + observation_space: null + action_space: + action_list: + - type: do_nothing + action_map: + 0: + action: do_nothing + options: {} + options: + nodes: + - node_name: client_1 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + + #. **identifier**: - All agent classes should have a ``identifier`` attribute, a unique snake_case string, for when they are added to the base ``AbstractAgent`` registry. + All agent classes should have a ``identifier`` attribute, a unique snake_case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. Changes to YAML file ==================== diff --git a/src/primaite/game/agent/scripted_agents/interface.py b/src/primaite/game/agent/interface.py similarity index 69% rename from src/primaite/game/agent/scripted_agents/interface.py rename to src/primaite/game/agent/interface.py index e6c2d6b3..c953d0a5 100644 --- a/src/primaite/game/agent/scripted_agents/interface.py +++ b/src/primaite/game/agent/interface.py @@ -17,7 +17,7 @@ from primaite.interface.request import RequestFormat, RequestResponse if TYPE_CHECKING: pass -__all__ = ("AgentHistoryItem", "AgentStartSettings", "AbstractAgent", "AbstractScriptedAgent", "ProxyAgent") +__all__ = ("AgentHistoryItem", "AbstractAgent", "AbstractScriptedAgent", "ProxyAgent") class AgentHistoryItem(BaseModel): @@ -43,63 +43,18 @@ class AgentHistoryItem(BaseModel): reward_info: Dict[str, Any] = {} -class AgentStartSettings(BaseModel): - """Configuration values for when an agent starts performing actions.""" - - start_step: int = 5 - "The timestep at which an agent begins performing it's actions" - frequency: int = 5 - "The number of timesteps to wait between performing actions" - variance: int = 0 - "The amount the frequency can randomly change to" - - @model_validator(mode="after") - def check_variance_lt_frequency(self) -> "AgentStartSettings": - """ - Make sure variance is equal to or lower than frequency. - - This is because the calculation for the next execution time is now + (frequency +- variance). If variance were - greater than frequency, sometimes the bracketed term would be negative and the attack would never happen again. - """ - if self.variance > self.frequency: - raise ValueError( - f"Agent start settings error: variance must be lower than frequency " - f"{self.variance=}, {self.frequency=}" - ) - return self - - -class AgentSettings(BaseModel): - """Settings for configuring the operation of an agent.""" - - start_settings: Optional[AgentStartSettings] = None - "Configuration for when an agent begins performing it's actions." - flatten_obs: bool = True - "Whether to flatten the observation space before passing it to the agent. True by default." - action_masking: bool = False - "Whether to return action masks at each step." - - @classmethod - def from_config(cls, config: Optional[Dict]) -> "AgentSettings": - """Construct agent settings from a config dictionary. - - :param config: A dict of options for the agent settings. - :type config: Dict - :return: The agent settings. - :rtype: AgentSettings - """ - if config is None: - return cls() - - return cls(**config) - - class AbstractAgent(BaseModel): """Base class for scripted and RL agents.""" _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} + _logger: AgentLog = AgentLog(agent_name="Abstract_Agent") config: "AbstractAgent.ConfigSchema" + history: List[AgentHistoryItem] = [] + action_manager: ActionManager + observation_manager: ObservationManager + reward_function: RewardFunction + class ConfigSchema(BaseModel): """ @@ -118,13 +73,34 @@ class AbstractAgent(BaseModel): """ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - agent_name: ClassVar[str] = "Abstract_Agent" # TODO: Make this a ClassVar[str] like verb in actions? - history: List[AgentHistoryItem] = [] - _logger: AgentLog = AgentLog(agent_name=agent_name) - action_manager: ActionManager - observation_manager: ObservationManager - reward_function: RewardFunction - agent_settings: Optional[AgentSettings] = None + agent_name: str = "Abstract_Agent" + flatten_obs: bool = True + "Whether to flatten the observation space before passing it to the agent. True by default." + action_masking: bool = False + "Whether to return action masks at each step." + start_step: int = 5 + "The timestep at which an agent begins performing it's actions" + frequency: int = 5 + "The number of timesteps to wait between performing actions" + variance: int = 0 + "The amount the frequency can randomly change to" + + + @model_validator(mode="after") + def check_variance_lt_frequency(self) -> "AbstractAgent.ConfigSchema": + """ + Make sure variance is equal to or lower than frequency. + + This is because the calculation for the next execution time is now + (frequency +- variance). If variance were + greater than frequency, sometimes the bracketed term would be negative and the attack would never happen again. + """ + if self.variance > self.frequency: + raise ValueError( + f"Agent start settings error: variance must be lower than frequency " + f"{self.variance=}, {self.frequency=}" + ) + return self + def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: if identifier in cls._registry: @@ -132,35 +108,11 @@ class AbstractAgent(BaseModel): cls._registry[identifier] = cls super().__init_subclass__(**kwargs) - @property - def logger(self) -> AgentLog: - """Return the AgentLog.""" - return self.config._logger @property def flatten_obs(self) -> bool: """Return agent flatten_obs param.""" - return self.config.agent_settings.flatten_obs - - @property - def history(self) -> List[AgentHistoryItem]: - """Return the agent history.""" - return self.config.history - - @property - def observation_manager(self) -> ObservationManager: - """Returns the agents observation manager.""" - return self.config.observation_manager - - @property - def action_manager(self) -> ActionManager: - """Returns the agents action manager.""" - return self.config.action_manager - - @property - def reward_function(self) -> RewardFunction: - """Returns the agents reward function.""" - return self.config.reward_function + return self.config.flatten_obs @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": @@ -217,7 +169,7 @@ class AbstractAgent(BaseModel): self, timestep: int, action: str, parameters: Dict[str, Any], request: RequestFormat, response: RequestResponse ) -> None: """Process the response from the most recent action.""" - self.config.history.append( + self.history.append( AgentHistoryItem( timestep=timestep, action=action, parameters=parameters, request=request, response=response ) @@ -225,7 +177,7 @@ class AbstractAgent(BaseModel): def save_reward_to_history(self) -> None: """Update the most recent history item with the reward value.""" - self.config.history[-1].reward = self.reward_function.current_reward + self.history[-1].reward = self.reward_function.current_reward class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 3c83731f..1de34b40 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -36,7 +36,7 @@ from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE if TYPE_CHECKING: - from primaite.game.agent.scripted_agents.interface import AgentHistoryItem + from primaite.game.agent.interface import AgentHistoryItem _LOGGER = getLogger(__name__) WhereType = Optional[Iterable[Union[str, int]]] diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py index 6237d430..e64a37c4 100644 --- a/src/primaite/game/agent/scripted_agents/__init__.py +++ b/src/primaite/game/agent/scripted_agents/__init__.py @@ -1,9 +1,9 @@ # © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.game.agent import interface from primaite.game.agent.scripted_agents import ( abstract_tap, data_manipulation_bot, - interface, probabilistic_agent, random_agent, ) diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index 725d3525..d7ecb959 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -7,7 +7,7 @@ from typing import Dict, Optional, Tuple from gymnasium.core import ObsType -from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent +from primaite.game.agent.interface import AbstractScriptedAgent __all__ = "AbstractTAPAgent" @@ -50,4 +50,4 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): num_nodes = len(self.config.action_manager.node_names) starting_node_idx = random.randint(0, num_nodes - 1) self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] - self.logger.debug(f"Selected Starting node ID: {self.starting_node_name}") + self.logger.debug(f"Selected starting node: {self.starting_node_name}") diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 533f0628..9e0cfbea 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -6,7 +6,7 @@ import numpy as np import pydantic from gymnasium.core import ObsType -from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent, AgentSettings +from primaite.game.agent.interface import AbstractScriptedAgent, AgentSettings __all__ = "ProbabilisticAgent" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index fadaa66c..2f417730 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -4,7 +4,7 @@ from typing import Dict, Tuple from gymnasium.core import ObsType -from primaite.game.agent.scripted_agents.interface import AbstractScriptedAgent +from primaite.game.agent.interface import AbstractScriptedAgent __all__ = ("RandomAgent", "PeriodicAgent") @@ -37,23 +37,9 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" + agent_name: str = "Periodic_Agent" + """Name of the agent.""" - agent_name: str = "Periodic_Agent" - """Name of the agent.""" - - # TODO: This is available in config.agent_settings.start_settings.start_step - start_step: int = 20 - "The timestep at which an agent begins performing it's actions." - start_variance: int = 5 - "Deviation around the start step." - - # TODO: This is available in config.agent_settings.start_settings.frequency - frequency: int = 5 - "The number of timesteps to wait between performing actions." - - # TODO: This is available in config.agent_settings.start_settings.variance - variance: int = 0 - "The amount the frequency can randomly change to." max_executions: int = 999999 "Maximum number of times the agent can execute its action." num_executions: int = 0 @@ -62,6 +48,22 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" + @property + def start_step(self) -> int: + """Return the timestep at which an agent begins performing it's actions.""" + return self.config.agent_settings.start_settings.start_step + + @property + def start_variance(self) -> int: + """Returns the deviation around the start step.""" + return self.config.agent_settings.start_settings.variance + + @property + def frequency(self) -> int: + """Returns the number of timesteps to wait between performing actions.""" + return self.config.agent_settings.start_settings.frequency + + def _set_next_execution_timestep(self, timestep: int, variance: int) -> None: """Set the next execution timestep with a configured random variance. @@ -75,9 +77,9 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" - if timestep == self.next_execution_timestep and self.num_executions < self.config.max_executions: + if timestep == self.next_execution_timestep and self.num_executions < self.max_executions: self.num_executions += 1 - self._set_next_execution_timestep(timestep + self.frequency, self.variance) + self._set_next_execution_timestep(timestep + self.frequency, self.start_variance) self.target_node = self.action_manager.node_names[0] return "node_application_execute", {"node_name": self.target_node, "application_name": 0} diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 6cf4a75a..501cbbdb 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -10,7 +10,7 @@ from primaite import DEFAULT_BANDWIDTH, getLogger from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction, SharedReward -from primaite.game.agent.scripted_agents.interface import AbstractAgent, ProxyAgent +from primaite.game.agent.interface import AbstractAgent, ProxyAgent from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.creation import NetworkNodeAdder @@ -555,9 +555,6 @@ class PrimaiteGame: # new_agent_cfg.update{} if agent_type in AbstractAgent._registry: - print(agent_type) - print(agent_config) - print(AbstractAgent._registry) new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) # If blue agent is created, add to game.rl_agents if agent_type == "ProxyAgent": diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index ab7b68f0..c66663e3 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -10,7 +10,7 @@ import numpy as np from gymnasium.core import ActType, ObsType from primaite import getLogger -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler from primaite.session.io import PrimaiteIO diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 5d15ffa2..8df4fc24 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -7,7 +7,7 @@ from gymnasium import spaces from gymnasium.core import ActType, ObsType from ray.rllib.env.multi_agent_env import MultiAgentEnv -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import _LOGGER, PrimaiteGymEnv from primaite.session.episode_schedule import build_scheduler, EpisodeScheduler diff --git a/tests/conftest.py b/tests/conftest.py index 319e306d..157fb95e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ from primaite import getLogger, PRIMAITE_PATHS from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction -from primaite.game.agent.scripted_agents.interface import AbstractAgent +from primaite.game.agent.interface import AbstractAgent from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system import FileSystem diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index bea18fb0..5182c809 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -7,7 +7,7 @@ import yaml from primaite.config.load import data_manipulation_config_path from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.game.game import PrimaiteGame, SERVICE_TYPES_MAPPING from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index 24e0d67e..36a7ae57 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 9d77536e..187fb1fe 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.base import UserManager diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 39b3fc8f..1c143aed 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index 19d549f5..e5e0806a 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -4,7 +4,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index 53629332..d796b75e 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index baf79007..fdf04ad5 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index d0099f6d..3054c73b 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index fa103805..a70cea72 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -3,7 +3,7 @@ from typing import Tuple import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.base import UserManager from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index eb5aca3a..36049c63 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -7,7 +7,7 @@ import yaml from gymnasium import spaces from primaite.game.agent.observations.nic_observations import NICObservation -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index a7a2b6c3..0c6d567d 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -5,7 +5,7 @@ import pytest import yaml from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.scripted_agents.interface import AgentHistoryItem +from primaite.game.agent.interface import AgentHistoryItem from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 53d8edd9..e03a7d26 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -17,7 +17,7 @@ from typing import Tuple import pytest import yaml -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 66c2d5a0..35ca5431 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -3,7 +3,7 @@ import pytest import yaml from primaite.game.agent.rewards import ActionPenalty, GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty -from primaite.game.agent.scripted_agents.interface import AgentHistoryItem +from primaite.game.agent.interface import AgentHistoryItem from primaite.game.game import PrimaiteGame from primaite.interface.request import RequestResponse from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 3352b975..2cbd4d11 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -5,7 +5,7 @@ from typing import Tuple import pytest import yaml -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index bfcc544d..9a58f395 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -5,7 +5,7 @@ from primaite.game.agent.rewards import ( WebpageUnavailablePenalty, WebServer404Penalty, ) -from primaite.game.agent.scripted_agents.interface import AgentHistoryItem +from primaite.game.agent.interface import AgentHistoryItem from primaite.interface.request import RequestResponse diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index fb081f12..9b6a4bf3 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -4,7 +4,7 @@ from uuid import uuid4 import pytest -from primaite.game.agent.scripted_agents.interface import ProxyAgent +from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer From c9752f0dc5e94a7dd4f0b58004e03096648888d1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 3 Jan 2025 11:22:17 +0000 Subject: [PATCH 101/224] #2913 - minor comment cleanup --- docs/source/how_to_guides/extensible_rewards.rst | 2 +- src/primaite/game/agent/rewards.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/how_to_guides/extensible_rewards.rst b/docs/source/how_to_guides/extensible_rewards.rst index 4dd24110..a01b9d8f 100644 --- a/docs/source/how_to_guides/extensible_rewards.rst +++ b/docs/source/how_to_guides/extensible_rewards.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _about: diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index c5850d6e..a4c7c546 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -223,7 +223,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WEBPAGE_UNAVAILABLE_ """Penalises the agent when the web browser fails to fetch a webpage.""" config: "WebpageUnavailablePenalty.ConfigSchema" - reward: float = 0.0 # XXX: Private attribute? + reward: float = 0.0 location_in_state: List[str] = [""] # Calculate in __init__()? class ConfigSchema(AbstractReward.ConfigSchema): From c481847b01266e1cd93aa02ce805c3ff95bbd169 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 3 Jan 2025 13:39:58 +0000 Subject: [PATCH 102/224] #2888 - Software: align identifiers, tidy up schemas --- .../simulator/network/hardware/base.py | 22 +++---- .../system/applications/application.py | 22 +++---- .../system/applications/database_client.py | 15 ++--- .../simulator/system/applications/nmap.py | 14 ++-- .../red_applications/c2/abstract_c2.py | 66 +++++++++---------- .../red_applications/c2/c2_beacon.py | 39 +++++------ .../red_applications/c2/c2_server.py | 27 ++++---- .../red_applications/data_manipulation_bot.py | 9 +++ .../applications/red_applications/dos_bot.py | 6 +- .../red_applications/ransomware_script.py | 13 ++-- .../system/applications/web_browser.py | 14 ++-- .../simulator/system/services/arp/arp.py | 9 +-- .../services/database/database_service.py | 14 ++-- .../system/services/dns/dns_client.py | 18 ++--- .../system/services/dns/dns_server.py | 13 ++-- .../system/services/ftp/ftp_client.py | 6 +- .../system/services/ftp/ftp_server.py | 6 +- .../simulator/system/services/icmp/icmp.py | 10 +-- .../system/services/ntp/ntp_client.py | 14 ++-- .../system/services/ntp/ntp_server.py | 8 ++- .../system/services/terminal/terminal.py | 14 ++-- .../system/services/web_server/web_server.py | 12 ++-- tests/conftest.py | 13 ++-- .../applications/extended_application.py | 14 ++-- .../extensions/services/extended_service.py | 14 ++-- .../network/test_broadcast.py | 14 +++- .../system/test_service_listening_on_ports.py | 12 ++-- .../_red_applications/test_c2_suite.py | 28 ++++---- .../_simulator/_system/test_software.py | 8 +-- 29 files changed, 252 insertions(+), 222 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 96b1d9a7..a7278489 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -833,14 +833,14 @@ class UserManager(Service, identifier="UserManager"): :param disabled_admins: A dictionary of currently disabled admin users by their usernames """ - config: "UserManager.ConfigSchema" = None - - users: Dict[str, User] = {} - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for UserManager.""" - type: str = "USER_MANAGER" + type: str = "UserManager" + + config: "UserManager.ConfigSchema" = Field(default_factory=lambda: UserManager.ConfigSchema()) + + users: Dict[str, User] = {} def __init__(self, **kwargs): """ @@ -1144,7 +1144,12 @@ class UserSessionManager(Service, identifier="UserSessionManager"): This class handles authentication, session management, and session timeouts for users interacting with the Node. """ - config: "UserSessionManager.ConfigSchema" = None + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for UserSessionManager.""" + + type: str = "UserSessionManager" + + config: "UserSessionManager.ConfigSchema" = Field(default_factory=lambda: UserSessionManager.ConfigSchema()) local_session: Optional[UserSession] = None """The current local user session, if any.""" @@ -1167,11 +1172,6 @@ class UserSessionManager(Service, identifier="UserSessionManager"): current_timestep: int = 0 """The current timestep in the simulation.""" - class ConfigSchema(Service.ConfigSchema): - """ConfigSchema for UserSessionManager.""" - - type: str = "USER_SESSION_MANAGER" - def __init__(self, **kwargs): """ Initializes a UserSessionManager instance. diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 29753cff..e0cac6b4 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from enum import Enum from typing import Any, ClassVar, Dict, Optional, Set, Type -from pydantic import BaseModel +from pydantic import BaseModel, Field from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType @@ -23,14 +23,19 @@ class ApplicationOperatingState(Enum): "The application is being installed or updated." -class Application(IOSoftware): +class Application(IOSoftware, ABC): """ Represents an Application in the simulation environment. Applications are user-facing programs that may perform input/output operations. """ - config: "Application.ConfigSchema" = None + class ConfigSchema(BaseModel, ABC): + """Config Schema for Application class.""" + + type: str + + config: ConfigSchema = Field(default_factory=lambda: Application.ConfigSchema()) operating_state: ApplicationOperatingState = ApplicationOperatingState.CLOSED "The current operating state of the Application." @@ -48,20 +53,15 @@ class Application(IOSoftware): _registry: ClassVar[Dict[str, Type["Application"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" - class ConfigSchema(BaseModel, ABC): - """Config Schema for Application class.""" - - type: str - - def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: + def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: """ Register an application type. :param identifier: Uniquely specifies an application class by name. Used for finding items by config. - :type identifier: str + :type identifier: Optional[str] :raises ValueError: When attempting to register an application with a name that is already allocated. """ - if identifier == "default": + if identifier is None: return super().__init_subclass__(**kwargs) if identifier in cls._registry: diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index d04f8298..facc4016 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Optional, Union from uuid import uuid4 from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel +from pydantic import BaseModel, Field from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType @@ -67,10 +67,14 @@ class DatabaseClient(Application, identifier="DatabaseClient"): Extends the Application class to provide functionality for connecting, querying, and disconnecting from a Database Service. It mainly operates over TCP protocol. - """ - config: "DatabaseClient.ConfigSchema" = None + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for DatabaseClient.""" + + type: str = "DatabaseClient" + + config: ConfigSchema = Field(default_factory=lambda: DatabaseClient.ConfigSchema()) server_ip_address: Optional[IPv4Address] = None """The IPv4 address of the Database Service server, defaults to None.""" @@ -90,11 +94,6 @@ class DatabaseClient(Application, identifier="DatabaseClient"): native_connection: Optional[DatabaseClientConnection] = None """Native Client Connection for using the client directly (similar to psql in a terminal).""" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for DatabaseClient.""" - - type: str = "DATABASE_CLIENT" - def __init__(self, **kwargs): kwargs["name"] = "DatabaseClient" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 676515cc..3eeda4b6 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -3,7 +3,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, Final, List, Optional, Set, Tuple, Union from prettytable import PrettyTable -from pydantic import validate_call +from pydantic import Field, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -52,7 +52,12 @@ class NMAP(Application, identifier="NMAP"): as ping scans to discover active hosts and port scans to detect open ports on those hosts. """ - config: "NMAP.ConfigSchema" = None + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for NMAP.""" + + type: str = "NMAP" + + config: "NMAP.ConfigSchema" = Field(default_factory=lambda: NMAP.ConfigSchema()) _active_port_scans: Dict[str, PortScanPayload] = {} _port_scan_responses: Dict[str, PortScanPayload] = {} @@ -64,11 +69,6 @@ class NMAP(Application, identifier="NMAP"): (False, False): "Port", } - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for NMAP.""" - - type: str = "NMAP" - def __init__(self, **kwargs): kwargs["name"] = "NMAP" kwargs["port"] = PORT_LOOKUP["NONE"] diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 960f8592..a379769d 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -4,7 +4,7 @@ from enum import Enum from ipaddress import IPv4Address from typing import Dict, Optional, Union -from pydantic import BaseModel, Field, validate_call +from pydantic import Field, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.file_system.file_system import FileSystem, Folder @@ -48,7 +48,7 @@ class C2Payload(Enum): """C2 Output Command. Used by the C2 Beacon to send the results of an Input command to the c2 server.""" -class AbstractC2(Application, identifier="AbstractC2"): +class AbstractC2(Application): """ An abstract command and control (c2) application. @@ -63,7 +63,19 @@ class AbstractC2(Application, identifier="AbstractC2"): Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ - config: "AbstractC2.ConfigSchema" = None + class ConfigSchema(Application.ConfigSchema): + """Configuration for AbstractC2.""" + + keep_alive_frequency: int = Field(default=5, ge=1) + """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" + + masquerade_protocol: IPProtocol = Field(default=PROTOCOL_LOOKUP["TCP"]) + """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" + + masquerade_port: Port = Field(default=PORT_LOOKUP["HTTP"]) + """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + + config: ConfigSchema = Field(default_factory=lambda: AbstractC2.ConfigSchema()) c2_connection_active: bool = False """Indicates if the c2 server and c2 beacon are currently connected.""" @@ -77,24 +89,6 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_inactivity: int = 0 """Indicates how many timesteps since the last time the c2 application received a keep alive.""" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for AbstractC2.""" - - type: str = "ABSTRACT_C2" - - class _C2Opts(BaseModel): - """A Pydantic Schema for the different C2 configuration options.""" - - keep_alive_frequency: int = Field(default=5, ge=1) - """The frequency at which ``Keep Alive`` packets are sent to the C2 Server from the C2 Beacon.""" - - masquerade_protocol: IPProtocol = Field(default=PROTOCOL_LOOKUP["TCP"]) - """The currently chosen protocol that the C2 traffic is masquerading as. Defaults as TCP.""" - - masquerade_port: Port = Field(default=PORT_LOOKUP["HTTP"]) - """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" - - c2_config: _C2Opts = _C2Opts() """ Holds the current configuration settings of the C2 Suite. @@ -129,9 +123,9 @@ class AbstractC2(Application, identifier="AbstractC2"): :rtype: C2Packet """ constructed_packet = C2Packet( - masquerade_protocol=self.c2_config.masquerade_protocol, - masquerade_port=self.c2_config.masquerade_port, - keep_alive_frequency=self.c2_config.keep_alive_frequency, + masquerade_protocol=self.config.masquerade_protocol, + masquerade_port=self.config.masquerade_port, + keep_alive_frequency=self.config.keep_alive_frequency, payload_type=c2_payload, command=c2_command, payload=command_options, @@ -337,8 +331,8 @@ class AbstractC2(Application, identifier="AbstractC2"): if self.send( payload=keep_alive_packet, dest_ip_address=self.c2_remote_connection, - dest_port=self.c2_config.masquerade_port, - ip_protocol=self.c2_config.masquerade_protocol, + dest_port=self.config.masquerade_port, + ip_protocol=self.config.masquerade_protocol, session_id=session_id, ): # Setting the keep_alive_sent guard condition to True. This is used to prevent packet storms. @@ -347,8 +341,8 @@ class AbstractC2(Application, identifier="AbstractC2"): self.sys_log.info(f"{self.name}: Keep Alive sent to {self.c2_remote_connection}") self.sys_log.debug( f"{self.name}: Keep Alive sent to {self.c2_remote_connection} " - f"Masquerade Port: {self.c2_config.masquerade_port} " - f"Masquerade Protocol: {self.c2_config.masquerade_protocol} " + f"Masquerade Port: {self.config.masquerade_port} " + f"Masquerade Protocol: {self.config.masquerade_protocol} " ) return True else: @@ -383,15 +377,15 @@ class AbstractC2(Application, identifier="AbstractC2"): # Updating the C2 Configuration attribute. - self.c2_config.masquerade_port = payload.masquerade_port - self.c2_config.masquerade_protocol = payload.masquerade_protocol - self.c2_config.keep_alive_frequency = payload.keep_alive_frequency + self.config.masquerade_port = payload.masquerade_port + self.config.masquerade_protocol = payload.masquerade_protocol + self.config.keep_alive_frequency = payload.keep_alive_frequency self.sys_log.debug( f"{self.name}: C2 Config Resolved Config from Keep Alive:" - f"Masquerade Port: {self.c2_config.masquerade_port}" - f"Masquerade Protocol: {self.c2_config.masquerade_protocol}" - f"Keep Alive Frequency: {self.c2_config.keep_alive_frequency}" + f"Masquerade Port: {self.config.masquerade_port}" + f"Masquerade Protocol: {self.config.masquerade_protocol}" + f"Keep Alive Frequency: {self.config.keep_alive_frequency}" ) # This statement is intended to catch on the C2 Application that is listening for connection. @@ -417,8 +411,8 @@ class AbstractC2(Application, identifier="AbstractC2"): self.keep_alive_inactivity = 0 self.keep_alive_frequency = 5 self.c2_remote_connection = None - self.c2_config.masquerade_port = PORT_LOOKUP["HTTP"] - self.c2_config.masquerade_protocol = PROTOCOL_LOOKUP["TCP"] + self.config.masquerade_port = PORT_LOOKUP["HTTP"] + self.config.masquerade_protocol = PROTOCOL_LOOKUP["TCP"] @abstractmethod def _confirm_remote_connection(self, timestep: int) -> bool: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index abb620cd..014a4096 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -3,12 +3,11 @@ from ipaddress import IPv4Address from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable -from pydantic import validate_call +from pydantic import Field, validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet -from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts, RansomwareOpts, TerminalOpts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript @@ -36,7 +35,12 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ - config: "C2Beacon.ConfigSchema" = None + class ConfigSchema(AbstractC2.ConfigSchema): + """ConfigSchema for C2Beacon.""" + + type: str = "C2Beacon" + + config: ConfigSchema = Field(default_factory=lambda: C2Beacon.ConfigSchema()) keep_alive_attempted: bool = False """Indicates if a keep alive has been attempted to be sent this timestep. Used to prevent packet storms.""" @@ -44,11 +48,6 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): terminal_session: TerminalClientConnection = None "The currently in use terminal session." - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for C2Beacon.""" - - type: str = "C2_BEACON" - @property def _host_terminal(self) -> Optional[Terminal]: """Return the Terminal that is installed on the same machine as the C2 Beacon.""" @@ -154,7 +153,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): masquerade_port | What port should the C2 traffic use? (TCP or UDP) These configuration options are used to reassign the fields in the inherited inner class - ``c2_config``. + ``config``. If a connection is already in progress then this method also sends a keep alive to the C2 Server in order for the C2 Server to sync with the new configuration settings. @@ -170,9 +169,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :return: Returns True if the configuration was successful, False otherwise. """ self.c2_remote_connection = IPv4Address(c2_server_ip_address) - self.c2_config.keep_alive_frequency = keep_alive_frequency - self.c2_config.masquerade_port = masquerade_port - self.c2_config.masquerade_protocol = masquerade_protocol + self.config.keep_alive_frequency = keep_alive_frequency + self.config.masquerade_port = masquerade_port + self.config.masquerade_protocol = masquerade_protocol self.sys_log.info( f"{self.name}: Configured {self.name} with remote C2 server connection: {c2_server_ip_address=}." ) @@ -271,14 +270,12 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): if self.send( payload=output_packet, dest_ip_address=self.c2_remote_connection, - dest_port=self.c2_config.masquerade_port, - ip_protocol=self.c2_config.masquerade_protocol, + dest_port=self.config.masquerade_port, + ip_protocol=self.config.masquerade_protocol, session_id=session_id, ): self.sys_log.info(f"{self.name}: Command output sent to {self.c2_remote_connection}") - self.sys_log.debug( - f"{self.name}: on {self.c2_config.masquerade_port} via {self.c2_config.masquerade_protocol}" - ) + self.sys_log.debug(f"{self.name}: on {self.config.masquerade_port} via {self.config.masquerade_protocol}") return True else: self.sys_log.warning( @@ -570,7 +567,7 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): :rtype bool: """ self.keep_alive_attempted = False # Resetting keep alive sent. - if self.keep_alive_inactivity == self.c2_config.keep_alive_frequency: + if self.keep_alive_inactivity == self.config.keep_alive_frequency: self.sys_log.info( f"{self.name}: Attempting to Send Keep Alive to {self.c2_remote_connection} at timestep {timestep}." ) @@ -635,9 +632,9 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.c2_connection_active, self.c2_remote_connection, self.keep_alive_inactivity, - self.c2_config.keep_alive_frequency, - self.c2_config.masquerade_protocol, - self.c2_config.masquerade_port, + self.config.keep_alive_frequency, + self.config.masquerade_protocol, + self.config.masquerade_port, ] ) print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 7308e8bc..9d2097e9 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -2,12 +2,11 @@ from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable -from pydantic import validate_call +from pydantic import Field, validate_call from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.masquerade import C2Packet -from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.red_applications.c2 import ( CommandOpts, ExfilOpts, @@ -35,16 +34,16 @@ class C2Server(AbstractC2, identifier="C2Server"): Please refer to the Command-and-Control notebook for an in-depth example of the C2 Suite. """ - config: "C2Server.ConfigSchema" = None + class ConfigSchema(AbstractC2.ConfigSchema): + """ConfigSchema for C2Server.""" + + type: str = "C2Server" + + config: ConfigSchema = Field(default_factory=lambda: C2Server.ConfigSchema()) current_command_output: RequestResponse = None """The Request Response by the last command send. This attribute is updated by the method _handle_command_output.""" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for C2Server.""" - - type: str = "C2_SERVER" - def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. @@ -259,8 +258,8 @@ class C2Server(AbstractC2, identifier="C2Server"): payload=command_packet, dest_ip_address=self.c2_remote_connection, session_id=self.c2_session.uuid, - dest_port=self.c2_config.masquerade_port, - ip_protocol=self.c2_config.masquerade_protocol, + dest_port=self.config.masquerade_port, + ip_protocol=self.config.masquerade_protocol, ): self.sys_log.info(f"{self.name}: Successfully sent {given_command}.") self.sys_log.info(f"{self.name}: Awaiting command response {given_command}.") @@ -342,11 +341,11 @@ class C2Server(AbstractC2, identifier="C2Server"): :return: Returns False if the C2 beacon is considered dead. Otherwise True. :rtype bool: """ - if self.keep_alive_inactivity > self.c2_config.keep_alive_frequency: + if self.keep_alive_inactivity > self.config.keep_alive_frequency: self.sys_log.info(f"{self.name}: C2 Beacon connection considered dead due to inactivity.") self.sys_log.debug( f"{self.name}: Did not receive expected keep alive connection from {self.c2_remote_connection}" - f"{self.name}: Expected at timestep: {timestep} due to frequency: {self.c2_config.keep_alive_frequency}" + f"{self.name}: Expected at timestep: {timestep} due to frequency: {self.config.keep_alive_frequency}" f"{self.name}: Last Keep Alive received at {(timestep - self.keep_alive_inactivity)}" ) self._reset_c2_connection() @@ -397,8 +396,8 @@ class C2Server(AbstractC2, identifier="C2Server"): [ self.c2_connection_active, self.c2_remote_connection, - self.c2_config.masquerade_protocol, - self.c2_config.masquerade_port, + self.config.masquerade_protocol, + self.config.masquerade_port, ] ) print(table) diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 0423087e..1978afb9 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -3,6 +3,8 @@ from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestResponse @@ -40,6 +42,13 @@ class DataManipulationAttackStage(IntEnum): class DataManipulationBot(Application, identifier="DataManipulationBot"): """A bot that simulates a script which performs a SQL injection attack.""" + class ConfigSchema(Application.ConfigSchema): + """Configuration schema for DataManipulationBot.""" + + type: str = "DataManipulationBot" + + config: "DataManipulationBot.ConfigSchema" = Field(default_factory=lambda: DataManipulationBot.ConfigSchema()) + payload: Optional[str] = None port_scan_p_of_success: float = 0.1 data_manipulation_p_of_success: float = 0.1 diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index 0c337c53..e284ba92 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -3,6 +3,8 @@ from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestFormat, RequestResponse @@ -33,7 +35,7 @@ class DoSAttackStage(IntEnum): class DoSBot(DatabaseClient, identifier="DoSBot"): """A bot that simulates a Denial of Service attack.""" - config: "DoSBot.ConfigSchema" = None + config: "DoSBot.ConfigSchema" = Field(default_factory=lambda: DoSBot.ConfigSchema()) target_ip_address: Optional[IPv4Address] = None """IP address of the target service.""" @@ -59,7 +61,7 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for DoSBot.""" - type: str = "DOS_BOT" + type: str = "DoSBot" def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 3e6ed624..b72dc8e5 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable +from pydantic import Field from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType @@ -18,7 +19,12 @@ class RansomwareScript(Application, identifier="RansomwareScript"): :ivar payload: The attack stage query payload. (Default ENCRYPT) """ - config: "RansomwareScript.ConfigSchema" = None + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for RansomwareScript.""" + + type: str = "RansomwareScript" + + config: "RansomwareScript.ConfigSchema" = Field(default_factory=lambda: RansomwareScript.ConfigSchema()) server_ip_address: Optional[IPv4Address] = None """IP address of node which hosts the database.""" @@ -27,11 +33,6 @@ class RansomwareScript(Application, identifier="RansomwareScript"): payload: Optional[str] = "ENCRYPT" "Payload String for the payload stage" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for RansomwareScript.""" - - type: str = "RANSOMWARE_SCRIPT" - def __init__(self, **kwargs): kwargs["name"] = "RansomwareScript" kwargs["port"] = PORT_LOOKUP["NONE"] diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 1bfe0e1a..52a566f2 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -4,7 +4,7 @@ from ipaddress import IPv4Address from typing import Dict, List, Optional from urllib.parse import urlparse -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from primaite import getLogger from primaite.interface.request import RequestResponse @@ -30,7 +30,12 @@ class WebBrowser(Application, identifier="WebBrowser"): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ - config: "WebBrowser.ConfigSchema" = None + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for WebBrowser.""" + + type: str = "WebBrowser" + + config: "WebBrowser.ConfigSchema" = Field(default_factory=lambda: WebBrowser.ConfigSchema()) target_url: Optional[str] = None @@ -43,11 +48,6 @@ class WebBrowser(Application, identifier="WebBrowser"): history: List["BrowserHistoryItem"] = [] """Keep a log of visited websites and information about the visit, such as response code.""" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for WebBrowser.""" - - type: str = "WEB_BROWSER" - def __init__(self, **kwargs): kwargs["name"] = "WebBrowser" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 4f59bc15..bbeec301 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -5,6 +5,7 @@ from abc import abstractmethod from typing import Any, Dict, Optional, Union from prettytable import MARKDOWN, PrettyTable +from pydantic import Field from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.arp import ARPEntry, ARPPacket @@ -22,15 +23,15 @@ class ARP(Service, identifier="ARP"): sends ARP requests and replies, and processes incoming ARP packets. """ - config: "ARP.ConfigSchema" = None - - arp: Dict[IPV4Address, ARPEntry] = {} - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for ARP.""" type: str = "ARP" + config: "ARP.ConfigSchema" = Field(default_factory=lambda: ARP.ConfigSchema()) + + arp: Dict[IPV4Address, ARPEntry] = {} + def __init__(self, **kwargs): kwargs["name"] = "ARP" kwargs["port"] = PORT_LOOKUP["ARP"] diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 68d75665..f16b4125 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -3,6 +3,8 @@ from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 +from pydantic import Field + from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus @@ -24,7 +26,12 @@ class DatabaseService(Service, identifier="DatabaseService"): This class inherits from the `Service` class and provides methods to simulate a SQL database. """ - config: "DatabaseService.ConfigSchema" = None + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for DatabaseService.""" + + type: str = "DatabaseService" + + config: "DatabaseService.ConfigSchema" = Field(default_factory=lambda: DatabaseService.ConfigSchema()) password: Optional[str] = None """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" @@ -38,11 +45,6 @@ class DatabaseService(Service, identifier="DatabaseService"): latest_backup_file_name: str = None """File name of latest backup.""" - class ConfigSchema(Service.ConfigSchema): - """ConfigSchema for DatabaseService.""" - - type: str = "DATABASE_SERVICE" - def __init__(self, **kwargs): kwargs["name"] = "DatabaseService" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index eb54ec71..0756eb05 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -2,6 +2,8 @@ from ipaddress import IPv4Address from typing import Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest from primaite.simulator.system.core.software_manager import SoftwareManager @@ -12,19 +14,19 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class DNSClient(Service): +class DNSClient(Service, identifier="DNSClient"): """Represents a DNS Client as a Service.""" - config: "DNSClient.ConfigSchema" = None - dns_cache: Dict[str, IPv4Address] = {} - "A dict of known mappings between domain/URLs names and IPv4 addresses." - dns_server: Optional[IPv4Address] = None - "The DNS Server the client sends requests to." - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DNSClient.""" - type: str = "DNS_CLIENT" + type: str = "DNSClient" + + config: "DNSClient.ConfigSchema" = Field(default_factory=lambda: DNSClient.ConfigSchema()) + dns_cache: Dict[str, IPv4Address] = {} + "A dict of known mappings between domain/URLs names and IPv4 addresses." + dns_server: Optional[IPv4Address] = None + "The DNS Server the client sends requests to." def __init__(self, **kwargs): kwargs["name"] = "DNSClient" diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index da302b6c..46008ddf 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from typing import Any, Dict, Optional from prettytable import MARKDOWN, PrettyTable +from pydantic import Field from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket @@ -16,15 +17,15 @@ _LOGGER = getLogger(__name__) class DNSServer(Service, identifier="DNSServer"): """Represents a DNS Server as a Service.""" - config: "DNSServer.ConfigSchema" = None - - dns_table: Dict[str, IPv4Address] = {} - "A dict of mappings between domain names and IPv4 addresses." - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DNSServer.""" - type: str = "DNS_SERVER" + type: str = "DNSServer" + + config: "DNSServer.ConfigSchema" = Field(default_factory=lambda: DNSServer.ConfigSchema()) + + dns_table: Dict[str, IPv4Address] = {} + "A dict of mappings between domain names and IPv4 addresses." def __init__(self, **kwargs): kwargs["name"] = "DNSServer" diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 033d4602..16cefdd6 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -2,6 +2,8 @@ from ipaddress import IPv4Address from typing import Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType @@ -24,12 +26,12 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPClient.ConfigSchema" = None + config: "FTPClient.ConfigSchema" = Field(default_factory=lambda: FTPClient.ConfigSchema()) class ConfigSchema(Service.ConfigSchema): """ConfigSchema for FTPClient.""" - type: str = "FTP_CLIENT" + type: str = "FTPClient" def __init__(self, **kwargs): kwargs["name"] = "FTPClient" diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 205ace21..054bfe15 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,6 +1,8 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Optional +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC @@ -19,7 +21,7 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPServer.ConfigSchema" = None + config: "FTPServer.ConfigSchema" = Field(default_factory=lambda: FTPServer.ConfigSchema()) server_password: Optional[str] = None """Password needed to connect to FTP server. Default is None.""" @@ -27,7 +29,7 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for FTPServer.""" - type: str = "FTP_Server" + type: str = "FTPServer" def __init__(self, **kwargs): kwargs["name"] = "FTPServer" diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 6d5355e7..7f626945 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -3,6 +3,8 @@ import secrets from ipaddress import IPv4Address from typing import Any, Dict, Optional, Tuple, Union +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType @@ -22,15 +24,15 @@ class ICMP(Service, identifier="ICMP"): network diagnostics, notably the ping command. """ - config: "ICMP.ConfigSchema" = None - - request_replies: Dict = {} - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for ICMP.""" type: str = "ICMP" + config: "ICMP.ConfigSchema" = Field(default_factory=lambda: ICMP.ConfigSchema()) + + request_replies: Dict = {} + def __init__(self, **kwargs): kwargs["name"] = "ICMP" kwargs["port"] = PORT_LOOKUP["NONE"] diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 6fc1f6fa..fb470faf 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -3,6 +3,8 @@ from datetime import datetime from ipaddress import IPv4Address from typing import Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket from primaite.simulator.system.services.service import Service, ServiceOperatingState @@ -15,17 +17,17 @@ _LOGGER = getLogger(__name__) class NTPClient(Service, identifier="NTPClient"): """Represents a NTP client as a service.""" - config: "NTPClient.ConfigSchema" = None + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for NTPClient.""" + + type: str = "NTPClient" + + config: "NTPClient.ConfigSchema" = Field(default_factory=lambda: NTPClient.ConfigSchema()) ntp_server: Optional[IPv4Address] = None "The NTP server the client sends requests to." time: Optional[datetime] = None - class ConfigSchema(Service.ConfigSchema): - """ConfigSchema for NTPClient.""" - - type: str = "NTP_CLIENT" - def __init__(self, **kwargs): kwargs["name"] = "NTPClient" kwargs["port"] = PORT_LOOKUP["NTP"] diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index a07d5f5c..7af33893 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -2,6 +2,8 @@ from datetime import datetime from typing import Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket from primaite.simulator.system.services.service import Service @@ -14,12 +16,12 @@ _LOGGER = getLogger(__name__) class NTPServer(Service, identifier="NTPServer"): """Represents a NTP server as a service.""" - config: "NTPServer.ConfigSchema" = None - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for NTPServer.""" - type: str = "NTP_SERVER" + type: str = "NTPServer" + + config: "NTPServer.ConfigSchema" = Field(default_factory=lambda: NTPServer.ConfigSchema()) def __init__(self, **kwargs): kwargs["name"] = "NTPServer" diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index c07af73e..f576d5ee 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address from typing import Any, Dict, List, Optional, Union from uuid import uuid4 -from pydantic import BaseModel +from pydantic import BaseModel, Field from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType @@ -132,15 +132,15 @@ class RemoteTerminalConnection(TerminalClientConnection): class Terminal(Service, identifier="Terminal"): """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" - config: "Terminal.ConfigSchema" = None - - _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} - """Dictionary of connect requests made to remote nodes.""" - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for Terminal.""" - type: str = "TERMINAL" + type: str = "Terminal" + + config: "Terminal.ConfigSchema" = Field(default_factory=lambda: Terminal.ConfigSchema()) + + _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} + """Dictionary of connect requests made to remote nodes.""" def __init__(self, **kwargs): kwargs["name"] = "Terminal" diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 70731df9..51724002 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -3,6 +3,8 @@ from ipaddress import IPv4Address from typing import Any, Dict, List, Optional from urllib.parse import urlparse +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.protocols.http import ( HttpRequestMethod, @@ -22,14 +24,14 @@ _LOGGER = getLogger(__name__) class WebServer(Service, identifier="WebServer"): """Class used to represent a Web Server Service in simulation.""" - config: "WebServer.ConfigSchema" = None - - response_codes_this_timestep: List[HttpStatusCode] = [] - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for WebServer.""" - type: str = "WEB_SERVER" + type: str = "WebServer" + + config: "WebServer.ConfigSchema" = Field(default_factory=lambda: WebServer.ConfigSchema()) + + response_codes_this_timestep: List[HttpStatusCode] = [] def describe_state(self) -> Dict: """ diff --git a/tests/conftest.py b/tests/conftest.py index 2ef4904a..d1440bd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Tuple import pytest import yaml +from pydantic import Field from ray import init as rayinit from primaite import getLogger, PRIMAITE_PATHS @@ -40,12 +41,12 @@ _LOGGER = getLogger(__name__) class DummyService(Service, identifier="DummyService"): """Test Service class""" - config: "DummyService.ConfigSchema" = None - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DummyService.""" - type: str = "DUMMY_SERVICE" + type: str = "DummyService" + + config: "DummyService.ConfigSchema" = Field(default_factory=lambda: DummyService.ConfigSchema()) def describe_state(self) -> Dict: return super().describe_state() @@ -63,12 +64,12 @@ class DummyService(Service, identifier="DummyService"): class DummyApplication(Application, identifier="DummyApplication"): """Test Application class""" - config: "DummyApplication.ConfigSchema" = None - class ConfigSchema(Application.ConfigSchema): """ConfigSchema for DummyApplication.""" - type: str = "DUMMY_APPLICATION" + type: str = "DummyApplication" + + config: "DummyApplication.ConfigSchema" = Field(default_factory=lambda: DummyApplication.ConfigSchema()) def __init__(self, **kwargs): kwargs["name"] = "DummyApplication" diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index f2d071b1..13fa3d1b 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -4,7 +4,7 @@ from ipaddress import IPv4Address from typing import Dict, List, Optional from urllib.parse import urlparse -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from primaite import getLogger from primaite.interface.request import RequestResponse @@ -31,7 +31,12 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ - config: "ExtendedApplication.ConfigSchema" = None + class ConfigSchema(Application.ConfigSchema): + """ConfigSchema for ExtendedApplication.""" + + type: str = "ExtendedApplication" + + config: "ExtendedApplication.ConfigSchema" = Field(default_factory=lambda: ExtendedApplication.ConfigSchema()) target_url: Optional[str] = None @@ -44,11 +49,6 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): history: List["BrowserHistoryItem"] = [] """Keep a log of visited websites and information about the visit, such as response code.""" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for ExtendedApplication.""" - - type: str = "EXTENDED_APPLICATION" - def __init__(self, **kwargs): kwargs["name"] = "ExtendedApplication" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index 5ec157b2..ba247369 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -3,6 +3,8 @@ from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 +from pydantic import Field + from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus @@ -24,7 +26,12 @@ class ExtendedService(Service, identifier="ExtendedService"): This class inherits from the `Service` class and provides methods to simulate a SQL database. """ - config: "ExtendedService.ConfigSchema" = None + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for ExtendedService.""" + + type: str = "ExtendedService" + + config: "ExtendedService.ConfigSchema" = Field(default_factory=lambda: ExtendedService.ConfigSchema()) password: Optional[str] = None """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" @@ -38,11 +45,6 @@ class ExtendedService(Service, identifier="ExtendedService"): latest_backup_file_name: str = None """File name of latest backup.""" - class ConfigSchema(Service.ConfigSchema): - """ConfigSchema for ExtendedService.""" - - type: str = "EXTENDED_SERVICE" - def __init__(self, **kwargs): kwargs["name"] = "ExtendedService" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index 37553727..ed40334f 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Tuple import pytest +from pydantic import Field from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -17,12 +18,12 @@ from primaite.utils.validation.port import PORT_LOOKUP class BroadcastTestService(Service, identifier="BroadcastTestService"): """A service for sending broadcast and unicast messages over a network.""" - config: "BroadcastTestService.ConfigSchema" = None - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for BroadcastTestService.""" - type: str = "BROADCAST_TEST_SERVICE" + type: str = "BroadcastTestService" + + config: "BroadcastTestService.ConfigSchema" = Field(default_factory=lambda: BroadcastTestService.ConfigSchema()) def __init__(self, **kwargs): # Set default service properties for broadcasting @@ -53,6 +54,13 @@ class BroadcastTestService(Service, identifier="BroadcastTestService"): class BroadcastTestClient(Application, identifier="BroadcastTestClient"): """A client application to receive broadcast and unicast messages.""" + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for BroadcastTestClient.""" + + type: str = "BroadcastTestClient" + + config: ConfigSchema = Field(default_factory=lambda: BroadcastTestClient.ConfigSchema()) + payloads_received: List = [] def __init__(self, **kwargs): 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 bdfd56f0..a57bd539 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -15,18 +15,18 @@ from tests import TEST_ASSETS_ROOT class _DatabaseListener(Service, identifier="_DatabaseListener"): - config: "_DatabaseListener.ConfigSchema" = None + class ConfigSchema(Service.ConfigSchema): + """ConfigSchema for _DatabaseListener.""" + + type: str = "_DatabaseListener" + + config: "_DatabaseListener.ConfigSchema" = Field(default_factory=lambda: _DatabaseListener.ConfigSchema()) name: str = "DatabaseListener" protocol: str = PROTOCOL_LOOKUP["TCP"] port: int = PORT_LOOKUP["NONE"] listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} payloads_received: List[Any] = Field(default_factory=list) - class ConfigSchema(Service.ConfigSchema): - """ConfigSchema for _DatabaseListener.""" - - type: str = "_DATABASE_LISTENER" - def receive(self, payload: Any, session_id: str, **kwargs) -> bool: self.payloads_received.append(payload) self.sys_log.info(f"{self.name}: received payload {payload}") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 4ff387ce..17f8445a 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -128,13 +128,13 @@ def test_c2_handle_switching_port(basic_c2_network): assert c2_server.c2_connection_active is True # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. - assert c2_beacon.c2_config.keep_alive_frequency is 2 - assert c2_beacon.c2_config.masquerade_port is PORT_LOOKUP["HTTP"] - assert c2_beacon.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] + assert c2_beacon.config.keep_alive_frequency is 2 + assert c2_beacon.config.masquerade_port is PORT_LOOKUP["HTTP"] + assert c2_beacon.config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] - assert c2_server.c2_config.keep_alive_frequency is 2 - assert c2_server.c2_config.masquerade_port is PORT_LOOKUP["HTTP"] - assert c2_server.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] + assert c2_server.config.keep_alive_frequency is 2 + assert c2_server.config.masquerade_port is PORT_LOOKUP["HTTP"] + assert c2_server.config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] # Configuring the C2 Beacon. c2_beacon.configure( @@ -150,11 +150,11 @@ def test_c2_handle_switching_port(basic_c2_network): # Assert to confirm that both the C2 server and the C2 beacon # Have reconfigured their C2 settings. - assert c2_beacon.c2_config.masquerade_port is PORT_LOOKUP["FTP"] - assert c2_beacon.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] + assert c2_beacon.config.masquerade_port is PORT_LOOKUP["FTP"] + assert c2_beacon.config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] - assert c2_server.c2_config.masquerade_port is PORT_LOOKUP["FTP"] - assert c2_server.c2_config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] + assert c2_server.config.masquerade_port is PORT_LOOKUP["FTP"] + assert c2_server.config.masquerade_protocol is PROTOCOL_LOOKUP["TCP"] def test_c2_handle_switching_frequency(basic_c2_network): @@ -174,8 +174,8 @@ def test_c2_handle_switching_frequency(basic_c2_network): assert c2_server.c2_connection_active is True # Assert to confirm that both the C2 server and the C2 beacon are configured correctly. - assert c2_beacon.c2_config.keep_alive_frequency is 2 - assert c2_server.c2_config.keep_alive_frequency is 2 + assert c2_beacon.config.keep_alive_frequency is 2 + assert c2_server.config.keep_alive_frequency is 2 # Configuring the C2 Beacon. c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=10) @@ -186,8 +186,8 @@ def test_c2_handle_switching_frequency(basic_c2_network): # Assert to confirm that both the C2 server and the C2 beacon # Have reconfigured their C2 settings. - assert c2_beacon.c2_config.keep_alive_frequency is 10 - assert c2_server.c2_config.keep_alive_frequency is 10 + assert c2_beacon.config.keep_alive_frequency is 10 + assert c2_server.config.keep_alive_frequency is 10 # Now skipping 9 time steps to confirm keep alive inactivity for i in range(9): diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 46860836..bdf9cfee 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -2,6 +2,7 @@ from typing import Dict import pytest +from pydantic import Field from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.service import Service @@ -11,13 +12,12 @@ from primaite.utils.validation.port import PORT_LOOKUP class TestSoftware(Service, identifier="TestSoftware"): - - config: "TestSoftware.ConfigSchema" = None - class ConfigSchema(Service.ConfigSchema): """ConfigSChema for TestSoftware.""" - type: str = "TEST_SOFTWARE" + type: str = "TestSoftware" + + config: "TestSoftware.ConfigSchema" = Field(default_factory=lambda: TestSoftware.ConfigSchema()) def describe_state(self) -> Dict: pass From 505eab6ed91f520d672775c18021706bd8c3a578 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 3 Jan 2025 14:02:36 +0000 Subject: [PATCH 103/224] #2869 - Changes following review discussion --- src/primaite/game/agent/interface.py | 25 +++++++++++++------------ src/primaite/game/game.py | 22 +++++----------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 14416241..b980d748 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -49,13 +49,12 @@ class AbstractAgent(BaseModel): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} _logger: AgentLog = AgentLog(agent_name="Abstract_Agent") - config: "AbstractAgent.ConfigSchema" history: List[AgentHistoryItem] = [] + config: "AbstractAgent.ConfigSchema" action_manager: ActionManager observation_manager: ObservationManager reward_function: RewardFunction - class ConfigSchema(BaseModel): """ Configuration Schema for AbstractAgents. @@ -85,14 +84,14 @@ class AbstractAgent(BaseModel): variance: int = 0 "The amount the frequency can randomly change to" - @model_validator(mode="after") def check_variance_lt_frequency(self) -> "AbstractAgent.ConfigSchema": """ Make sure variance is equal to or lower than frequency. - This is because the calculation for the next execution time is now + (frequency +- variance). If variance were - greater than frequency, sometimes the bracketed term would be negative and the attack would never happen again. + This is because the calculation for the next execution time is now + (frequency +- variance). + If variance were greater than frequency, sometimes the bracketed term would be negative + and the attack would never happen again. """ if self.variance > self.frequency: raise ValueError( @@ -101,14 +100,12 @@ class AbstractAgent(BaseModel): ) return self - def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: if identifier in cls._registry: raise ValueError(f"Cannot create a new agent under reserved name {identifier}") cls._registry[identifier] = cls super().__init_subclass__(**kwargs) - @property def flatten_obs(self) -> bool: """Return agent flatten_obs param.""" @@ -117,7 +114,12 @@ class AbstractAgent(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": """Creates an agent component from a configuration dictionary.""" - obj = cls(config=cls.ConfigSchema(**config)) + obj = cls( + config=cls.ConfigSchema(**config["agent_settings"]), + action_manager=ActionManager.from_config(**config["action_manager"]), + observation_manager=ObservationManager.from_config(**config["observation_space"]), + reward_function=RewardFunction.from_config(**config["reward_function"]), + ) return obj def update_observation(self, state: Dict) -> ObsType: @@ -206,9 +208,8 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): """Configuration Schema for Proxy Agent.""" agent_name: str = "Proxy_Agent" - agent_settings: AgentSettings = None - flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False - action_masking: bool = agent_settings.action_masking if agent_settings else False + flatten_obs: bool = False + action_masking: bool = False def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ @@ -221,7 +222,7 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): :return: Action to be taken in CAOS format. :rtype: Tuple[str, Dict] """ - return self.config.action_manager.get_action(self.most_recent_action) + return self.action_manager.get_action(self.most_recent_action) def store_action(self, action: ActType): """ diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 781db2c5..e83f59a6 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -7,10 +7,8 @@ import numpy as np from pydantic import BaseModel, ConfigDict from primaite import DEFAULT_BANDWIDTH, getLogger -from primaite.game.agent.actions import ActionManager -from primaite.game.agent.observations.observation_manager import ObservationManager -from primaite.game.agent.rewards import RewardFunction, SharedReward from primaite.game.agent.interface import AbstractAgent, ProxyAgent +from primaite.game.agent.rewards import SharedReward from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.creation import NetworkNodeAdder @@ -532,24 +530,14 @@ class PrimaiteGame: action_space_cfg = agent_cfg["action_space"] observation_space_cfg = agent_cfg["observation_space"] reward_function_cfg = agent_cfg["reward_function"] - - # CREATE OBSERVATION SPACE - obs_space = ObservationManager.from_config(observation_space_cfg) - - # CREATE ACTION SPACE - action_space = ActionManager.from_config(game, action_space_cfg) - - # CREATE REWARD FUNCTION - reward_function = RewardFunction.from_config(reward_function_cfg) + agent_settings = agent_cfg["agent_settings"] # CREATE AGENT - - agent_settings = agent_cfg["agent_settings"] agent_config = { "agent_name": agent_ref, - "action_manager": action_space, - "observation_manager": obs_space, - "reward_function": reward_function, + "action_manager": action_space_cfg, + "observation_manager": observation_space_cfg, + "reward_function": reward_function_cfg, "agent_settings": agent_settings, } From b11678a128183dbd1badaf663c2f57446f6258f3 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 3 Jan 2025 14:40:00 +0000 Subject: [PATCH 104/224] #2912 - Actioning Review Comments --- benchmark/benchmark.py | 2 +- benchmark/primaite_benchmark.py | 2 +- benchmark/report.py | 2 +- benchmark/utils.py | 2 +- docs/_templates/custom-class-template.rst | 2 +- docs/_templates/custom-module-template.rst | 2 +- docs/api.rst | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/source/action_masking.rst | 2 +- docs/source/config.rst | 2 +- docs/source/configuration/agents.rst | 2 +- docs/source/configuration/game.rst | 2 +- docs/source/configuration/io_settings.rst | 2 +- docs/source/configuration/simulation.rst | 2 +- .../simulation/nodes/common/common.rst | 2 +- .../common/common_host_node_attributes.rst | 2 +- .../common/common_network_node_attributes.rst | 2 +- .../nodes/common/common_node_attributes.rst | 2 +- .../nodes/common/node_type_list.rst | 2 +- .../simulation/nodes/computer.rst | 2 +- .../simulation/nodes/firewall.rst | 2 +- .../simulation/nodes/network_examples.rst | 2 +- .../configuration/simulation/nodes/router.rst | 2 +- .../configuration/simulation/nodes/server.rst | 2 +- .../configuration/simulation/nodes/switch.rst | 2 +- .../simulation/software/applications.rst | 2 +- .../simulation/software/services.rst | 2 +- docs/source/customising_scenarios.rst | 2 +- docs/source/dependencies.rst | 2 +- docs/source/developer_tools.rst | 2 +- docs/source/environment.rst | 2 +- docs/source/example_notebooks.rst | 2 +- docs/source/game_layer.rst | 2 +- docs/source/getting_started.rst | 2 +- docs/source/glossary.rst | 2 +- .../how_to_guides/extensible_actions.rst | 2 +- docs/source/node_sets.rst | 2 +- docs/source/notebooks/executed_notebooks.rst | 2 +- docs/source/primaite-dependencies.rst | 2 +- docs/source/request_system.rst | 2 +- docs/source/rewards.rst | 2 +- docs/source/simulation.rst | 2 +- .../network/airspace.rst | 2 +- .../network/base_hardware.rst | 2 +- .../simulation_components/network/network.rst | 2 +- .../network/network_interfaces.rst | 2 +- .../network/nodes/firewall.rst | 2 +- .../network/nodes/host_node.rst | 2 +- .../network/nodes/network_node.rst | 2 +- .../network/nodes/router.rst | 2 +- .../network/nodes/switch.rst | 2 +- .../network/nodes/wireless_router.rst | 2 +- .../network/transport_to_data_link_layer.rst | 2 +- .../system/applications/c2_suite.rst | 2 +- .../applications/data_manipulation_bot.rst | 2 +- .../system/applications/database_client.rst | 2 +- .../system/applications/dos_bot.rst | 2 +- .../system/applications/nmap.rst | 2 +- .../system/applications/ransomware_script.rst | 2 +- .../system/applications/web_browser.rst | 2 +- .../system/common/common_configuration.rst | 2 +- .../system/common/db_payload_list.rst | 2 +- .../system/internal_frame_processing.rst | 2 +- .../system/list_of_applications.rst | 2 +- .../system/list_of_services.rst | 2 +- .../system/list_of_system_applications.rst | 2 +- .../system/list_of_system_services.rst | 2 +- .../simulation_components/system/pcap.rst | 2 +- .../system/services/database_service.rst | 2 +- .../system/services/dns_client.rst | 2 +- .../system/services/dns_server.rst | 2 +- .../system/services/ftp_client.rst | 2 +- .../system/services/ftp_server.rst | 2 +- .../system/services/ntp_client.rst | 2 +- .../system/services/ntp_server.rst | 2 +- .../system/services/terminal.rst | 2 +- .../system/services/web_server.rst | 2 +- .../system/session_and_software_manager.rst | 2 +- .../simulation_components/system/software.rst | 2 +- .../simulation_components/system/sys_log.rst | 2 +- docs/source/simulation_structure.rst | 2 +- docs/source/state_system.rst | 2 +- docs/source/varying_config_files.rst | 2 +- src/primaite/__init__.py | 2 +- src/primaite/_legacy/actions.py | 2 +- src/primaite/cli.py | 2 +- src/primaite/config/__init__.py | 2 +- src/primaite/config/load.py | 2 +- src/primaite/exceptions.py | 2 +- src/primaite/game/__init__.py | 2 +- src/primaite/game/agent/__init__.py | 2 +- src/primaite/game/agent/actions/__init__.py | 2 +- src/primaite/game/agent/actions/abstract.py | 2 +- src/primaite/game/agent/actions/acl.py | 2 +- .../game/agent/actions/application.py | 2 +- src/primaite/game/agent/actions/config.py | 2 +- src/primaite/game/agent/actions/file.py | 2 +- src/primaite/game/agent/actions/folder.py | 2 +- src/primaite/game/agent/actions/host_nic.py | 2 +- src/primaite/game/agent/actions/manager.py | 4 +-- src/primaite/game/agent/actions/network.py | 2 +- src/primaite/game/agent/actions/node.py | 2 +- src/primaite/game/agent/actions/service.py | 2 +- src/primaite/game/agent/actions/session.py | 2 +- src/primaite/game/agent/agent_log.py | 2 +- src/primaite/game/agent/interface.py | 2 +- .../game/agent/observations/__init__.py | 2 +- .../agent/observations/acl_observation.py | 2 +- .../observations/file_system_observations.py | 2 +- .../observations/firewall_observation.py | 2 +- .../agent/observations/host_observations.py | 2 +- .../agent/observations/link_observation.py | 2 +- .../agent/observations/nic_observations.py | 2 +- .../agent/observations/node_observations.py | 2 +- .../agent/observations/observation_manager.py | 2 +- .../game/agent/observations/observations.py | 2 +- .../agent/observations/router_observation.py | 2 +- .../observations/software_observation.py | 2 +- src/primaite/game/agent/rewards.py | 2 +- .../game/agent/scripted_agents/__init__.py | 2 +- .../scripted_agents/data_manipulation_bot.py | 2 +- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 2 +- .../game/agent/scripted_agents/tap001.py | 2 +- src/primaite/game/agent/utils.py | 2 +- src/primaite/game/game.py | 2 +- src/primaite/game/science.py | 2 +- src/primaite/interface/__init__.py | 2 +- src/primaite/interface/request.py | 2 +- src/primaite/session/__init__.py | 2 +- src/primaite/session/environment.py | 2 +- src/primaite/session/episode_schedule.py | 2 +- src/primaite/session/io.py | 2 +- src/primaite/session/ray_envs.py | 2 +- src/primaite/setup/__init__.py | 2 +- src/primaite/setup/reset_demo_notebooks.py | 2 +- src/primaite/setup/reset_example_configs.py | 2 +- src/primaite/simulator/__init__.py | 2 +- src/primaite/simulator/core.py | 2 +- src/primaite/simulator/domain/__init__.py | 2 +- src/primaite/simulator/domain/account.py | 2 +- src/primaite/simulator/domain/controller.py | 2 +- .../simulator/file_system/__init__.py | 2 +- src/primaite/simulator/file_system/file.py | 2 +- .../simulator/file_system/file_system.py | 2 +- .../file_system/file_system_item_abc.py | 2 +- .../simulator/file_system/file_type.py | 2 +- src/primaite/simulator/file_system/folder.py | 2 +- src/primaite/simulator/network/__init__.py | 2 +- src/primaite/simulator/network/airspace.py | 2 +- src/primaite/simulator/network/container.py | 2 +- src/primaite/simulator/network/creation.py | 2 +- .../simulator/network/hardware/__init__.py | 2 +- .../simulator/network/hardware/base.py | 2 +- .../hardware/network_interface/__init__.py | 2 +- .../network_interface/wireless/__init__.py | 2 +- .../wireless/wireless_access_point.py | 2 +- .../wireless/wireless_nic.py | 2 +- .../network/hardware/node_operating_state.py | 2 +- .../network/hardware/nodes/__init__.py | 2 +- .../network/hardware/nodes/host/__init__.py | 2 +- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../network/hardware/nodes/host/server.py | 2 +- .../hardware/nodes/network/__init__.py | 2 +- .../hardware/nodes/network/firewall.py | 2 +- .../hardware/nodes/network/network_node.py | 2 +- .../network/hardware/nodes/network/router.py | 2 +- .../network/hardware/nodes/network/switch.py | 2 +- .../hardware/nodes/network/wireless_router.py | 2 +- src/primaite/simulator/network/networks.py | 2 +- src/primaite/simulator/network/nmne.py | 2 +- .../simulator/network/protocols/__init__.py | 2 +- .../simulator/network/protocols/arp.py | 2 +- .../simulator/network/protocols/dns.py | 2 +- .../simulator/network/protocols/ftp.py | 2 +- .../simulator/network/protocols/http.py | 2 +- .../simulator/network/protocols/icmp.py | 2 +- .../simulator/network/protocols/masquerade.py | 2 +- .../simulator/network/protocols/ntp.py | 2 +- .../simulator/network/protocols/packet.py | 2 +- .../simulator/network/protocols/ssh.py | 2 +- .../network/transmission/__init__.py | 2 +- .../network/transmission/data_link_layer.py | 2 +- .../network/transmission/network_layer.py | 2 +- .../network/transmission/primaite_layer.py | 2 +- .../network/transmission/transport_layer.py | 2 +- src/primaite/simulator/network/utils.py | 2 +- src/primaite/simulator/sim_container.py | 2 +- src/primaite/simulator/system/__init__.py | 2 +- .../simulator/system/applications/__init__.py | 2 +- .../system/applications/application.py | 2 +- .../system/applications/database_client.py | 2 +- .../simulator/system/applications/nmap.py | 2 +- .../applications/red_applications/__init__.py | 2 +- .../red_applications/c2/__init__.py | 2 +- .../red_applications/c2/abstract_c2.py | 2 +- .../red_applications/c2/c2_beacon.py | 2 +- .../red_applications/c2/c2_server.py | 2 +- .../red_applications/data_manipulation_bot.py | 2 +- .../applications/red_applications/dos_bot.py | 2 +- .../red_applications/ransomware_script.py | 2 +- .../system/applications/web_browser.py | 2 +- .../simulator/system/core/__init__.py | 2 +- .../simulator/system/core/packet_capture.py | 2 +- .../simulator/system/core/session_manager.py | 2 +- .../simulator/system/core/software_manager.py | 2 +- src/primaite/simulator/system/core/sys_log.py | 2 +- .../simulator/system/processes/__init__.py | 2 +- .../simulator/system/processes/process.py | 2 +- .../simulator/system/services/__init__.py | 2 +- .../system/services/access/__init__.py | 2 +- .../system/services/access/user_manager.py | 2 +- .../services/access/user_session_manager.py | 2 +- .../simulator/system/services/arp/__init__.py | 2 +- .../simulator/system/services/arp/arp.py | 2 +- .../system/services/database/__init__.py | 2 +- .../services/database/database_service.py | 2 +- .../simulator/system/services/dns/__init__.py | 2 +- .../system/services/dns/dns_client.py | 2 +- .../system/services/dns/dns_server.py | 2 +- .../simulator/system/services/ftp/__init__.py | 2 +- .../system/services/ftp/ftp_client.py | 2 +- .../system/services/ftp/ftp_server.py | 2 +- .../system/services/ftp/ftp_service.py | 2 +- .../system/services/icmp/__init__.py | 2 +- .../simulator/system/services/icmp/icmp.py | 2 +- .../system/services/icmp/router_icmp.py | 2 +- .../simulator/system/services/ntp/__init__.py | 2 +- .../system/services/ntp/ntp_client.py | 2 +- .../system/services/ntp/ntp_server.py | 2 +- .../simulator/system/services/service.py | 2 +- .../system/services/terminal/__init__.py | 2 +- .../system/services/terminal/terminal.py | 2 +- .../system/services/web_server/__init__.py | 2 +- .../system/services/web_server/web_server.py | 2 +- src/primaite/simulator/system/software.py | 2 +- src/primaite/utils/__init__.py | 2 +- src/primaite/utils/cli/__init__.py | 2 +- src/primaite/utils/cli/dev_cli.py | 2 +- .../utils/cli/primaite_config_utils.py | 2 +- src/primaite/utils/converters.py | 2 +- src/primaite/utils/package_data.py | 2 +- src/primaite/utils/session_metadata_parser.py | 2 +- src/primaite/utils/session_output_reader.py | 2 +- src/primaite/utils/session_output_writer.py | 2 +- src/primaite/utils/validation/__init__.py | 2 +- src/primaite/utils/validation/ip_protocol.py | 2 +- src/primaite/utils/validation/ipv4_address.py | 2 +- src/primaite/utils/validation/port.py | 2 +- tests/__init__.py | 2 +- tests/conftest.py | 29 +------------------ tests/e2e_integration_tests/__init__.py | 2 +- .../action_masking/__init__.py | 2 +- .../test_agents_use_action_masks.py | 2 +- .../environments/__init__.py | 2 +- .../test_rllib_multi_agent_environment.py | 2 +- .../test_rllib_single_agent_environment.py | 2 +- .../environments/test_sb3_environment.py | 2 +- .../e2e_integration_tests/test_environment.py | 2 +- .../test_uc2_data_manipulation_scenario.py | 2 +- tests/integration_tests/__init__.py | 2 +- tests/integration_tests/cli/__init__.py | 2 +- tests/integration_tests/cli/test_dev_cli.py | 2 +- .../component_creation/__init__.py | 2 +- .../test_action_integration.py | 2 +- .../test_permission_system.py | 2 +- .../configuration_file_parsing/__init__.py | 2 +- .../nodes/__init__.py | 2 +- .../nodes/network/__init__.py | 2 +- .../nodes/network/test_firewall_config.py | 2 +- .../nodes/network/test_router_config.py | 2 +- .../nodes/test_node_config.py | 2 +- ...software_installation_and_configuration.py | 2 +- .../test_episode_scheduler.py | 2 +- .../test_game_options_config.py | 2 +- .../test_io_settings.py | 2 +- .../test_no_nodes_links_agents_config.py | 2 +- .../test_software_fix_duration.py | 2 +- .../applications/extended_application.py | 2 +- .../extensions/nodes/giga_switch.py | 2 +- .../extensions/nodes/super_computer.py | 2 +- .../extensions/services/extended_service.py | 2 +- .../extensions/test_extendable_config.py | 2 +- .../game_layer/actions/__init__.py | 2 +- .../test_application_request_permission.py | 2 +- .../actions/test_c2_suite_actions.py | 2 +- .../actions/test_configure_actions.py | 2 +- .../actions/test_file_request_permission.py | 2 +- .../actions/test_folder_request_permission.py | 2 +- .../actions/test_nic_request_permission.py | 2 +- .../actions/test_node_request_permission.py | 2 +- .../test_service_request_permission.py | 2 +- .../actions/test_terminal_actions.py | 2 +- .../game_layer/observations/__init__.py | 2 +- .../observations/test_acl_observations.py | 2 +- .../test_file_system_observations.py | 2 +- .../observations/test_firewall_observation.py | 2 +- .../observations/test_link_observations.py | 2 +- .../observations/test_nic_observations.py | 2 +- .../observations/test_node_observations.py | 2 +- .../observations/test_router_observation.py | 2 +- .../test_software_observations.py | 2 +- .../observations/test_user_observations.py | 2 +- .../game_layer/test_RNG_seed.py | 2 +- .../game_layer/test_action_mask.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_observations.py | 2 +- .../game_layer/test_rewards.py | 2 +- tests/integration_tests/network/__init__.py | 2 +- .../network/test_airspace_config.py | 2 +- ...ndwidth_load_checks_before_transmission.py | 2 +- .../network/test_broadcast.py | 2 +- .../network/test_capture_nmne.py | 2 +- .../network/test_firewall.py | 2 +- .../network/test_frame_transmission.py | 2 +- ...test_multi_lan_internet_example_network.py | 2 +- .../network/test_network_creation.py | 2 +- .../network/test_nic_link_connection.py | 2 +- .../integration_tests/network/test_routing.py | 2 +- .../network/test_switched_network.py | 2 +- .../test_users_creation_from_config.py | 2 +- .../network/test_wireless_router.py | 2 +- tests/integration_tests/system/__init__.py | 2 +- .../test_c2_suite_integration.py | 2 +- .../test_data_manipulation_bot_and_server.py | 2 +- .../test_dos_bot_and_server.py | 2 +- .../test_ransomware_script.py | 2 +- .../system/test_application_on_node.py | 2 +- tests/integration_tests/system/test_arp.py | 2 +- .../system/test_database_on_node.py | 2 +- .../system/test_dns_client_server.py | 2 +- .../system/test_ftp_client_server.py | 2 +- tests/integration_tests/system/test_nmap.py | 2 +- .../system/test_ntp_client_server.py | 2 +- .../system/test_service_listening_on_ports.py | 2 +- .../system/test_service_on_node.py | 2 +- .../test_user_session_manager_logins.py | 2 +- .../system/test_web_client_server.py | 2 +- .../test_web_client_server_and_database.py | 2 +- .../test_simulation/__init__.py | 2 +- .../test_simulation/test_request_response.py | 2 +- tests/mock_and_patch/__init__.py | 2 +- tests/mock_and_patch/get_session_path_mock.py | 2 +- tests/unit_tests/__init__.py | 2 +- tests/unit_tests/_primaite/__init__.py | 2 +- tests/unit_tests/_primaite/_game/__init__.py | 2 +- .../_primaite/_game/_agent/__init__.py | 2 +- .../_primaite/_game/_agent/test_actions.py | 2 +- .../_primaite/_game/_agent/test_agent_log.py | 2 +- .../_game/_agent/test_observations.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 2 +- .../_primaite/_interface/__init__.py | 2 +- .../_primaite/_interface/test_request.py | 2 +- .../unit_tests/_primaite/_session/__init__.py | 2 +- .../_session/test_episode_schedule.py | 2 +- .../_primaite/_simulator/__init__.py | 2 +- .../_primaite/_simulator/_domain/__init__.py | 2 +- .../_simulator/_domain/test_account.py | 2 +- .../_simulator/_domain/test_controller.py | 2 +- .../_simulator/_file_system/__init__.py | 2 +- .../_simulator/_file_system/test_file.py | 2 +- .../_file_system/test_file_actions.py | 2 +- .../_file_system/test_file_system.py | 2 +- .../_file_system/test_file_system_actions.py | 2 +- .../_simulator/_file_system/test_folder.py | 2 +- .../_file_system/test_folder_actions.py | 2 +- .../_primaite/_simulator/_network/__init__.py | 2 +- .../_simulator/_network/_hardware/__init__.py | 2 +- .../_network/_hardware/nodes/__init__.py | 2 +- .../_network/_hardware/nodes/test_acl.py | 2 +- .../_network/_hardware/nodes/test_router.py | 2 +- .../_network/_hardware/nodes/test_switch.py | 2 +- .../test_network_interface_actions.py | 2 +- .../_simulator/_network/_hardware/test_nic.py | 2 +- .../_network/_hardware/test_node_actions.py | 2 +- .../_network/_transmission/__init__.py | 2 +- .../_transmission/test_data_link_layer.py | 2 +- .../_transmission/test_network_layer.py | 2 +- .../_simulator/_network/test_container.py | 2 +- .../_simulator/_network/test_creation.py | 2 +- .../_simulator/_network/test_utils.py | 2 +- .../_primaite/_simulator/_system/__init__.py | 2 +- .../_system/_applications/__init__.py | 2 +- .../_red_applications/__init__.py | 2 +- .../_red_applications/test_c2_suite.py | 2 +- .../test_data_manipulation_bot.py | 2 +- .../_red_applications/test_dos_bot.py | 2 +- .../_applications/test_application_actions.py | 2 +- .../test_application_registry.py | 2 +- .../_applications/test_applications.py | 2 +- .../_applications/test_database_client.py | 2 +- .../_system/_applications/test_web_browser.py | 2 +- .../_simulator/_system/_services/__init__.py | 2 +- .../_system/_services/test_database.py | 2 +- .../_system/_services/test_dns_client.py | 2 +- .../_system/_services/test_dns_server.py | 2 +- .../_system/_services/test_ftp_client.py | 2 +- .../_system/_services/test_ftp_server.py | 2 +- .../_system/_services/test_service_actions.py | 2 +- .../_system/_services/test_services.py | 2 +- .../_system/_services/test_terminal.py | 2 +- .../_system/_services/test_web_server.py | 2 +- .../_simulator/_system/core/test_sys_log.py | 2 +- .../_simulator/_system/test_software.py | 2 +- .../_primaite/_simulator/test_core.py | 2 +- .../_simulator/test_sim_container.py | 2 +- tests/unit_tests/_primaite/_utils/__init__.py | 2 +- .../_primaite/_utils/_validation/__init__.py | 2 +- .../_utils/_validation/test_ip_protocol.py | 2 +- .../_primaite/_utils/_validation/test_port.py | 2 +- .../_utils/test_dict_enum_keys_conversion.py | 2 +- 414 files changed, 414 insertions(+), 443 deletions(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 4ad398b9..ddedebb7 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, Optional, Tuple from gymnasium.core import ObsType diff --git a/benchmark/primaite_benchmark.py b/benchmark/primaite_benchmark.py index 86ed22a9..70ea8900 100644 --- a/benchmark/primaite_benchmark.py +++ b/benchmark/primaite_benchmark.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import shutil from datetime import datetime diff --git a/benchmark/report.py b/benchmark/report.py index 4035ceca..c11528ab 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import sys from datetime import datetime diff --git a/benchmark/utils.py b/benchmark/utils.py index 2e92d80d..f17c64b7 100644 --- a/benchmark/utils.py +++ b/benchmark/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import platform from typing import Dict diff --git a/docs/_templates/custom-class-template.rst b/docs/_templates/custom-class-template.rst index 920158d5..71e992bc 100644 --- a/docs/_templates/custom-class-template.rst +++ b/docs/_templates/custom-class-template.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. Credit to https://github.com/JamesALeedham/Sphinx-Autosummary-Recursion for the custom templates. diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst index 98627e43..3a2ced35 100644 --- a/docs/_templates/custom-module-template.rst +++ b/docs/_templates/custom-module-template.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. Credit to https://github.com/JamesALeedham/Sphinx-Autosummary-Recursion for the custom templates. diff --git a/docs/api.rst b/docs/api.rst index 977f9e87..eb7e4719 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,7 +2,7 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. DO NOT DELETE THIS FILE! It contains the all-important `.. autosummary::` directive with `:recursive:` option, without diff --git a/docs/conf.py b/docs/conf.py index 318829fd..60739499 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: diff --git a/docs/index.rst b/docs/index.rst index 2ba43162..42cc1d6d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Welcome to PrimAITE's documentation ==================================== diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index 264ab254..dad6a484 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Action Masking ************** diff --git a/docs/source/config.rst b/docs/source/config.rst index eb0b9906..0fa4a4d5 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK PrimAITE |VERSION| Configuration ******************************** diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index dece94c5..d11f7892 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``agents`` diff --git a/docs/source/configuration/game.rst b/docs/source/configuration/game.rst index 2048708c..b3c139b2 100644 --- a/docs/source/configuration/game.rst +++ b/docs/source/configuration/game.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``game`` diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index 1c9585c9..ab3a978e 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``io_settings`` diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index fa1d774a..0b2067d8 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``simulation`` diff --git a/docs/source/configuration/simulation/nodes/common/common.rst b/docs/source/configuration/simulation/nodes/common/common.rst index a0f2eb13..c45eccf6 100644 --- a/docs/source/configuration/simulation/nodes/common/common.rst +++ b/docs/source/configuration/simulation/nodes/common/common.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Node Attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst index bb3b2a52..b717340e 100644 --- a/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _common_host_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst index d556e2dc..035c7e55 100644 --- a/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _common_network_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst index 6a95911f..542b817b 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _common_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/node_type_list.rst b/docs/source/configuration/simulation/nodes/common/node_type_list.rst index 1ec496d9..21181019 100644 --- a/docs/source/configuration/simulation/nodes/common/node_type_list.rst +++ b/docs/source/configuration/simulation/nodes/common/node_type_list.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``type`` -------- diff --git a/docs/source/configuration/simulation/nodes/computer.rst b/docs/source/configuration/simulation/nodes/computer.rst index 32e0b2b9..456d11a2 100644 --- a/docs/source/configuration/simulation/nodes/computer.rst +++ b/docs/source/configuration/simulation/nodes/computer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _computer_configuration: diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst index 775ffabd..84b5c99e 100644 --- a/docs/source/configuration/simulation/nodes/firewall.rst +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _firewall_configuration: diff --git a/docs/source/configuration/simulation/nodes/network_examples.rst b/docs/source/configuration/simulation/nodes/network_examples.rst index 2a34a206..80e934e5 100644 --- a/docs/source/configuration/simulation/nodes/network_examples.rst +++ b/docs/source/configuration/simulation/nodes/network_examples.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _network_examples: diff --git a/docs/source/configuration/simulation/nodes/router.rst b/docs/source/configuration/simulation/nodes/router.rst index ac9d6411..4b41784c 100644 --- a/docs/source/configuration/simulation/nodes/router.rst +++ b/docs/source/configuration/simulation/nodes/router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _router_configuration: diff --git a/docs/source/configuration/simulation/nodes/server.rst b/docs/source/configuration/simulation/nodes/server.rst index 92b33ca7..616efb38 100644 --- a/docs/source/configuration/simulation/nodes/server.rst +++ b/docs/source/configuration/simulation/nodes/server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _server_configuration: diff --git a/docs/source/configuration/simulation/nodes/switch.rst b/docs/source/configuration/simulation/nodes/switch.rst index 17cf76f9..d09f5ba7 100644 --- a/docs/source/configuration/simulation/nodes/switch.rst +++ b/docs/source/configuration/simulation/nodes/switch.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _switch_configuration: diff --git a/docs/source/configuration/simulation/software/applications.rst b/docs/source/configuration/simulation/software/applications.rst index 8c590d53..9973a167 100644 --- a/docs/source/configuration/simulation/software/applications.rst +++ b/docs/source/configuration/simulation/software/applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``applications`` ---------------- diff --git a/docs/source/configuration/simulation/software/services.rst b/docs/source/configuration/simulation/software/services.rst index fafdf2e8..ec6bbba9 100644 --- a/docs/source/configuration/simulation/software/services.rst +++ b/docs/source/configuration/simulation/software/services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``services`` ------------ diff --git a/docs/source/customising_scenarios.rst b/docs/source/customising_scenarios.rst index 092f306b..df7d4b1e 100644 --- a/docs/source/customising_scenarios.rst +++ b/docs/source/customising_scenarios.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Customising Agents ****************** diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst index 74f3cd14..e8be00d3 100644 --- a/docs/source/dependencies.rst +++ b/docs/source/dependencies.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. role:: raw-html(raw) :format: html diff --git a/docs/source/developer_tools.rst b/docs/source/developer_tools.rst index a66b7902..b3d81a27 100644 --- a/docs/source/developer_tools.rst +++ b/docs/source/developer_tools.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Developer Tools: diff --git a/docs/source/environment.rst b/docs/source/environment.rst index a282c09e..251b1090 100644 --- a/docs/source/environment.rst +++ b/docs/source/environment.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK RL Environments *************** diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index 920175c9..6caeae3d 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _example jupyter notebooks: diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 775c02b5..58a274d9 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK PrimAITE Game layer ******************* diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index ded92c60..427d1823 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _getting-started: diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 8fff0ea3..02c578d1 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Glossary ============= diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 6e44a905..1c44c2b2 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _about: diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst index 866f0139..3c247478 100644 --- a/docs/source/node_sets.rst +++ b/docs/source/node_sets.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _network_node_adder: diff --git a/docs/source/notebooks/executed_notebooks.rst b/docs/source/notebooks/executed_notebooks.rst index 3431d344..f4acfad6 100644 --- a/docs/source/notebooks/executed_notebooks.rst +++ b/docs/source/notebooks/executed_notebooks.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Executed Notebooks: diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst index 8367ee61..14a96349 100644 --- a/docs/source/primaite-dependencies.rst +++ b/docs/source/primaite-dependencies.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | Name | Version | License | Description | URL | diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index f2d2e68d..b89d0906 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Request System ************** diff --git a/docs/source/rewards.rst b/docs/source/rewards.rst index 0163284c..254237ee 100644 --- a/docs/source/rewards.rst +++ b/docs/source/rewards.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Rewards ####### diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index cc723e40..95807703 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Simulation diff --git a/docs/source/simulation_components/network/airspace.rst b/docs/source/simulation_components/network/airspace.rst index 06a884a7..a6967b91 100644 --- a/docs/source/simulation_components/network/airspace.rst +++ b/docs/source/simulation_components/network/airspace.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _airspace: diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index ce1e5c74..8b325ffc 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ############# Base Hardware diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index 4cc121a3..152b74b8 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _network: diff --git a/docs/source/simulation_components/network/network_interfaces.rst b/docs/source/simulation_components/network/network_interfaces.rst index c6b97a8e..663af7ba 100644 --- a/docs/source/simulation_components/network/network_interfaces.rst +++ b/docs/source/simulation_components/network/network_interfaces.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ################################# Network Interface Hierarchy Model diff --git a/docs/source/simulation_components/network/nodes/firewall.rst b/docs/source/simulation_components/network/nodes/firewall.rst index 1ef16d63..f2d7e61a 100644 --- a/docs/source/simulation_components/network/nodes/firewall.rst +++ b/docs/source/simulation_components/network/nodes/firewall.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ######## Firewall diff --git a/docs/source/simulation_components/network/nodes/host_node.rst b/docs/source/simulation_components/network/nodes/host_node.rst index b8aae098..2c1e75d0 100644 --- a/docs/source/simulation_components/network/nodes/host_node.rst +++ b/docs/source/simulation_components/network/nodes/host_node.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ######### diff --git a/docs/source/simulation_components/network/nodes/network_node.rst b/docs/source/simulation_components/network/nodes/network_node.rst index e1fa976c..4aebe09f 100644 --- a/docs/source/simulation_components/network/nodes/network_node.rst +++ b/docs/source/simulation_components/network/nodes/network_node.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ############ Network Node diff --git a/docs/source/simulation_components/network/nodes/router.rst b/docs/source/simulation_components/network/nodes/router.rst index 5d3de60f..fb582b23 100644 --- a/docs/source/simulation_components/network/nodes/router.rst +++ b/docs/source/simulation_components/network/nodes/router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ###### Router diff --git a/docs/source/simulation_components/network/nodes/switch.rst b/docs/source/simulation_components/network/nodes/switch.rst index 0ecbcbf3..e7143f0c 100644 --- a/docs/source/simulation_components/network/nodes/switch.rst +++ b/docs/source/simulation_components/network/nodes/switch.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ###### Switch diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index c0c245b2..d7207846 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ###### Wireless Router diff --git a/docs/source/simulation_components/network/transport_to_data_link_layer.rst b/docs/source/simulation_components/network/transport_to_data_link_layer.rst index 02bfdcdc..54118c90 100644 --- a/docs/source/simulation_components/network/transport_to_data_link_layer.rst +++ b/docs/source/simulation_components/network/transport_to_data_link_layer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Transport Layer to Data Link Layer ================================== diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index d045949a..3dd2b4fc 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _C2_Suite: diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 1a387514..91c33ede 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DataManipulationBot: diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 1fea78ab..75a396b5 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DatabaseClient: diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index 6ad45424..5c0ae86a 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DoSBot: diff --git a/docs/source/simulation_components/system/applications/nmap.rst b/docs/source/simulation_components/system/applications/nmap.rst index a5615a43..a82735c8 100644 --- a/docs/source/simulation_components/system/applications/nmap.rst +++ b/docs/source/simulation_components/system/applications/nmap.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _NMAP: diff --git a/docs/source/simulation_components/system/applications/ransomware_script.rst b/docs/source/simulation_components/system/applications/ransomware_script.rst index 5bff6991..b79ca802 100644 --- a/docs/source/simulation_components/system/applications/ransomware_script.rst +++ b/docs/source/simulation_components/system/applications/ransomware_script.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _RansomwareScript: diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index c56c450d..7062887b 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _WebBrowser: diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index c53ac8b8..411fd529 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Common Configuration: diff --git a/docs/source/simulation_components/system/common/db_payload_list.rst b/docs/source/simulation_components/system/common/db_payload_list.rst index 0930f09d..89668665 100644 --- a/docs/source/simulation_components/system/common/db_payload_list.rst +++ b/docs/source/simulation_components/system/common/db_payload_list.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Database Payload List: diff --git a/docs/source/simulation_components/system/internal_frame_processing.rst b/docs/source/simulation_components/system/internal_frame_processing.rst index 65336f9b..f82dec13 100644 --- a/docs/source/simulation_components/system/internal_frame_processing.rst +++ b/docs/source/simulation_components/system/internal_frame_processing.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _internal_frame_processing: diff --git a/docs/source/simulation_components/system/list_of_applications.rst b/docs/source/simulation_components/system/list_of_applications.rst index 94090d93..a7e05ea6 100644 --- a/docs/source/simulation_components/system/list_of_applications.rst +++ b/docs/source/simulation_components/system/list_of_applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. toctree:: :maxdepth: 1 diff --git a/docs/source/simulation_components/system/list_of_services.rst b/docs/source/simulation_components/system/list_of_services.rst index b6995647..2082ac6f 100644 --- a/docs/source/simulation_components/system/list_of_services.rst +++ b/docs/source/simulation_components/system/list_of_services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. toctree:: :maxdepth: 1 diff --git a/docs/source/simulation_components/system/list_of_system_applications.rst b/docs/source/simulation_components/system/list_of_system_applications.rst index c8807ef0..0c66662f 100644 --- a/docs/source/simulation_components/system/list_of_system_applications.rst +++ b/docs/source/simulation_components/system/list_of_system_applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``system applications`` """"""""""""""""""""""" diff --git a/docs/source/simulation_components/system/list_of_system_services.rst b/docs/source/simulation_components/system/list_of_system_services.rst index 9b5c3265..01df4dc8 100644 --- a/docs/source/simulation_components/system/list_of_system_services.rst +++ b/docs/source/simulation_components/system/list_of_system_services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK ``system services`` """"""""""""""""""" diff --git a/docs/source/simulation_components/system/pcap.rst b/docs/source/simulation_components/system/pcap.rst index 830c28bd..0da28a39 100644 --- a/docs/source/simulation_components/system/pcap.rst +++ b/docs/source/simulation_components/system/pcap.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK PCAP ==== diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index f3e800cd..b41c1097 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DatabaseService: diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index eca152f0..6475b4d4 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DNSClient: diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index 1e30b9bd..3d699048 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _DNSServer: diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index f9c7b4ce..47566e5f 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _FTPClient: diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index f52fa043..e4cada29 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _FTPServer: diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index 7af831bf..fb965029 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _NTPClient: diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index a09c8bdd..68fadca9 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _NTPServer: diff --git a/docs/source/simulation_components/system/services/terminal.rst b/docs/source/simulation_components/system/services/terminal.rst index 6909786e..bc5cee48 100644 --- a/docs/source/simulation_components/system/services/terminal.rst +++ b/docs/source/simulation_components/system/services/terminal.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _Terminal: diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index cec20a60..011aa00f 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _WebServer: diff --git a/docs/source/simulation_components/system/session_and_software_manager.rst b/docs/source/simulation_components/system/session_and_software_manager.rst index 230f6687..f20af556 100644 --- a/docs/source/simulation_components/system/session_and_software_manager.rst +++ b/docs/source/simulation_components/system/session_and_software_manager.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Session and Software Manager ============================ diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index c8f0e2d3..d28815bb 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _software: diff --git a/docs/source/simulation_components/system/sys_log.rst b/docs/source/simulation_components/system/sys_log.rst index cdf19faa..05629993 100644 --- a/docs/source/simulation_components/system/sys_log.rst +++ b/docs/source/simulation_components/system/sys_log.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK SysLog ====== diff --git a/docs/source/simulation_structure.rst b/docs/source/simulation_structure.rst index cd9ac409..7debe112 100644 --- a/docs/source/simulation_structure.rst +++ b/docs/source/simulation_structure.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Simulation Structure diff --git a/docs/source/state_system.rst b/docs/source/state_system.rst index e31474ea..a5fd1df1 100644 --- a/docs/source/state_system.rst +++ b/docs/source/state_system.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Simulation State ================ diff --git a/docs/source/varying_config_files.rst b/docs/source/varying_config_files.rst index fa66f0d9..942e522b 100644 --- a/docs/source/varying_config_files.rst +++ b/docs/source/varying_config_files.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK Defining variations in the config files ======================================= diff --git a/src/primaite/__init__.py b/src/primaite/__init__.py index 8dd84428..54eac69d 100644 --- a/src/primaite/__init__.py +++ b/src/primaite/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import datetime as datetime import logging import logging.config diff --git a/src/primaite/_legacy/actions.py b/src/primaite/_legacy/actions.py index 0eda7d86..d2457a20 100644 --- a/src/primaite/_legacy/actions.py +++ b/src/primaite/_legacy/actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """ This module contains the ActionManager class which belongs to the Agent class. diff --git a/src/primaite/cli.py b/src/primaite/cli.py index 4fbbdec9..2bd18baf 100644 --- a/src/primaite/cli.py +++ b/src/primaite/cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Provides a CLI using Typer as an entry point.""" import logging import os diff --git a/src/primaite/config/__init__.py b/src/primaite/config/__init__.py index c2ae1b5b..7b5e2889 100644 --- a/src/primaite/config/__init__.py +++ b/src/primaite/config/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Configuration parameters for running experiments.""" diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index 39040d76..3553f527 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Dict, Final, Union diff --git a/src/primaite/exceptions.py b/src/primaite/exceptions.py index afc55271..4487111d 100644 --- a/src/primaite/exceptions.py +++ b/src/primaite/exceptions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK class PrimaiteError(Exception): """The root PrimAITE Error.""" diff --git a/src/primaite/game/__init__.py b/src/primaite/game/__init__.py index 39034e92..57f96a56 100644 --- a/src/primaite/game/__init__.py +++ b/src/primaite/game/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """PrimAITE Game Layer.""" diff --git a/src/primaite/game/agent/__init__.py b/src/primaite/game/agent/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/game/agent/__init__.py +++ b/src/primaite/game/agent/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 016a09ba..1100e125 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ( abstract, diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 8c332d5e..15c9b4cb 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import ABC diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index d2846ddb..6fefeeda 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 91e34eae..96609f93 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.abstract import AbstractAction diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 050e9b94..760e8dfa 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List, Optional, Union diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index b5e47c8a..e5ca1c46 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index a27ca89b..d1fd5ef1 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index e2adf7d7..7b290103 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index a6a4f5a6..625d8cec 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """yaml example. agents: @@ -80,8 +80,6 @@ class ActionManager: self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} # make sure all numbers between 0 and N are represented as dict keys in action map assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) - self.node_names: List[str] = [n["node_name"] for n in nodes] - """List of node names in this action space. The list order is the mapping between node index and node name.""" def get_action(self, action: int) -> Tuple[str, Dict]: """Produce action in CAOS format.""" diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index 346da9b7..fa1c4451 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 480cb8da..c6b74f2e 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from typing import ClassVar, List, Optional, Union diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index 7ccffb0a..fa47ffb1 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index a0805a49..1191987b 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 62ef4884..59fb4702 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import logging from pathlib import Path diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 14b97821..4acc9108 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Interface for agents.""" from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING diff --git a/src/primaite/game/agent/observations/__init__.py b/src/primaite/game/agent/observations/__init__.py index c4811c98..a38095b3 100644 --- a/src/primaite/game/agent/observations/__init__.py +++ b/src/primaite/game/agent/observations/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa # Pre-import all the observations when we load up the observations module so that they can be resolved by the parser. from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index 41af5a8f..86a6463a 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 1c73d026..50ca93fd 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, Iterable, List, Optional diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index 42ceaff0..a89ddfc5 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 617e8eee..03e9aca1 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/link_observation.py b/src/primaite/game/agent/observations/link_observation.py index 9af39a22..851e9557 100644 --- a/src/primaite/game/agent/observations/link_observation.py +++ b/src/primaite/game/agent/observations/link_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Any, Dict, List diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index d180b641..f87d2d76 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index e11521b6..03869367 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 9b20fdcb..71a60433 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Any, Dict, List, Optional diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index a9663c56..49b9ab72 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Manages the observation space for the agent.""" from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, Optional, Type, Union diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index d064936a..ca455f4c 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 15cd2447..37810c6e 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index f528c851..fead27f2 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """ Manages the reward function for the agent. diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/game/agent/scripted_agents/__init__.py +++ b/src/primaite/game/agent/scripted_agents/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index eb0ce957..2432dd7b 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random from typing import Dict, Tuple diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index cd44644f..ce4d90d1 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Agents with predefined behaviours.""" from typing import Dict, Optional, Tuple diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index eade3a0c..2c2ff091 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random from typing import Dict, Optional, Tuple diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index 6d370654..1ed200d7 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random from typing import Dict, Tuple diff --git a/src/primaite/game/agent/utils.py b/src/primaite/game/agent/utils.py index 15efd0b6..87b02858 100644 --- a/src/primaite/game/agent/utils.py +++ b/src/primaite/game/agent/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, Hashable, Optional, Sequence NOT_PRESENT_IN_STATE = object() diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c8fbac4e..6555e272 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address from typing import Dict, List, Optional, Union diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py index 8d8949df..2cb5de7d 100644 --- a/src/primaite/game/science.py +++ b/src/primaite/game/science.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from random import random from typing import Any, Iterable, Mapping diff --git a/src/primaite/interface/__init__.py b/src/primaite/interface/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/interface/__init__.py +++ b/src/primaite/interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py index 1a9f0e5f..03d6491e 100644 --- a/src/primaite/interface/request.py +++ b/src/primaite/interface/request.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, ForwardRef, List, Literal, Union from pydantic import BaseModel, ConfigDict, StrictBool # , validate_call diff --git a/src/primaite/session/__init__.py b/src/primaite/session/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/session/__init__.py +++ b/src/primaite/session/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index c66663e3..8e608ede 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import random import sys diff --git a/src/primaite/session/episode_schedule.py b/src/primaite/session/episode_schedule.py index ad4d38e9..126dcf9f 100644 --- a/src/primaite/session/episode_schedule.py +++ b/src/primaite/session/episode_schedule.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from abc import ABC, abstractmethod from itertools import chain diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 78d7cb3c..6c2f4f29 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json from datetime import datetime from pathlib import Path diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 33c74b0e..33ba0540 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json from typing import Dict, SupportsFloat, Tuple diff --git a/src/primaite/setup/__init__.py b/src/primaite/setup/__init__.py index 12e7c4e7..1447a47b 100644 --- a/src/primaite/setup/__init__.py +++ b/src/primaite/setup/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Utilities to prepare the user's data folders.""" diff --git a/src/primaite/setup/reset_demo_notebooks.py b/src/primaite/setup/reset_demo_notebooks.py index f17fb211..ad4091e3 100644 --- a/src/primaite/setup/reset_demo_notebooks.py +++ b/src/primaite/setup/reset_demo_notebooks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import filecmp import shutil from logging import Logger diff --git a/src/primaite/setup/reset_example_configs.py b/src/primaite/setup/reset_example_configs.py index c7eeecd5..a94d6d4a 100644 --- a/src/primaite/setup/reset_example_configs.py +++ b/src/primaite/setup/reset_example_configs.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import filecmp import os import shutil diff --git a/src/primaite/simulator/__init__.py b/src/primaite/simulator/__init__.py index ade1a73b..e85a2d1e 100644 --- a/src/primaite/simulator/__init__.py +++ b/src/primaite/simulator/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Warning: SIM_OUTPUT is a mutable global variable for the simulation output directory.""" from datetime import datetime from enum import IntEnum diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 848570fe..567a0493 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa """Core of the PrimAITE Simulator.""" import warnings diff --git a/src/primaite/simulator/domain/__init__.py b/src/primaite/simulator/domain/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/domain/__init__.py +++ b/src/primaite/simulator/domain/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index d955cf55..85ec6d46 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """User account simulation.""" from enum import Enum from typing import Dict diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index a264ba24..d8b7782c 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Dict, Final, List, Literal, Tuple diff --git a/src/primaite/simulator/file_system/__init__.py b/src/primaite/simulator/file_system/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/file_system/__init__.py +++ b/src/primaite/simulator/file_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index ba39c791..57d01ec9 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import hashlib diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 2162915f..8ff4b6fb 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from pathlib import Path diff --git a/src/primaite/simulator/file_system/file_system_item_abc.py b/src/primaite/simulator/file_system/file_system_item_abc.py index a9db8825..48b95d20 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import math diff --git a/src/primaite/simulator/file_system/file_type.py b/src/primaite/simulator/file_system/file_type.py index e6e81070..343d3565 100644 --- a/src/primaite/simulator/file_system/file_type.py +++ b/src/primaite/simulator/file_system/file_type.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from enum import Enum diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index c98e4492..ee0f3d01 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import warnings diff --git a/src/primaite/simulator/network/__init__.py b/src/primaite/simulator/network/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/__init__.py +++ b/src/primaite/simulator/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 2b8503d6..1f6fe6b0 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 1082e172..bf677d5c 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Optional diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 5d36f58b..94c45428 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Literal, Type diff --git a/src/primaite/simulator/network/hardware/__init__.py b/src/primaite/simulator/network/hardware/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/__init__.py +++ b/src/primaite/simulator/network/hardware/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 51e200e7..8324715f 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import re diff --git a/src/primaite/simulator/network/hardware/network_interface/__init__.py b/src/primaite/simulator/network/hardware/network_interface/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/network_interface/__init__.py +++ b/src/primaite/simulator/network/hardware/network_interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py index a9a31768..3997872c 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from primaite.simulator.network.hardware.base import ( diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py index eebaedc5..9bc4cd6f 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from primaite.simulator.network.hardware.base import ( diff --git a/src/primaite/simulator/network/hardware/node_operating_state.py b/src/primaite/simulator/network/hardware/node_operating_state.py index e64ef08b..8771cb84 100644 --- a/src/primaite/simulator/network/hardware/node_operating_state.py +++ b/src/primaite/simulator/network/hardware/node_operating_state.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum diff --git a/src/primaite/simulator/network/hardware/nodes/__init__.py b/src/primaite/simulator/network/hardware/nodes/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/nodes/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/host/__init__.py b/src/primaite/simulator/network/hardware/nodes/host/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/host/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 4253d15c..11b925b9 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 0c309136..c51afbca 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index bf1ef39b..e16cfd8f 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/src/primaite/simulator/network/hardware/nodes/network/__init__.py b/src/primaite/simulator/network/hardware/nodes/network/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 84cf8530..f1ca4930 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Final, Union diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index a5b8544f..22ff2b28 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from typing import Optional diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index e921faff..4a049f99 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations import secrets diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index d29152a4..db923f1a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, Optional diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index aed314d2..804a570e 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, Optional, Union diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 2c3c15b4..c840748e 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import yaml diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index c9cff5de..a2e5f1fe 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List from pydantic import BaseModel, ConfigDict diff --git a/src/primaite/simulator/network/protocols/__init__.py b/src/primaite/simulator/network/protocols/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/protocols/__init__.py +++ b/src/primaite/simulator/network/protocols/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/protocols/arp.py b/src/primaite/simulator/network/protocols/arp.py index 9e7f7ebe..86e461d0 100644 --- a/src/primaite/simulator/network/protocols/arp.py +++ b/src/primaite/simulator/network/protocols/arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/protocols/dns.py b/src/primaite/simulator/network/protocols/dns.py index eb7b74ad..c0fed1aa 100644 --- a/src/primaite/simulator/network/protocols/dns.py +++ b/src/primaite/simulator/network/protocols/dns.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py index c570a634..fd8fdd2b 100644 --- a/src/primaite/simulator/network/protocols/ftp.py +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Any, Optional, Union diff --git a/src/primaite/simulator/network/protocols/http.py b/src/primaite/simulator/network/protocols/http.py index 5390cd26..54abdd98 100644 --- a/src/primaite/simulator/network/protocols/http.py +++ b/src/primaite/simulator/network/protocols/http.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum, IntEnum from primaite.simulator.network.protocols.packet import DataPacket diff --git a/src/primaite/simulator/network/protocols/icmp.py b/src/primaite/simulator/network/protocols/icmp.py index 9f0626f0..fcbe15da 100644 --- a/src/primaite/simulator/network/protocols/icmp.py +++ b/src/primaite/simulator/network/protocols/icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import secrets from enum import Enum from typing import Union diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index 5c5f03b2..e0ed26b7 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Optional diff --git a/src/primaite/simulator/network/protocols/ntp.py b/src/primaite/simulator/network/protocols/ntp.py index 74e02dab..c9b6f877 100644 --- a/src/primaite/simulator/network/protocols/ntp.py +++ b/src/primaite/simulator/network/protocols/ntp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from datetime import datetime diff --git a/src/primaite/simulator/network/protocols/packet.py b/src/primaite/simulator/network/protocols/packet.py index 7eeec13b..6f28f716 100644 --- a/src/primaite/simulator/network/protocols/packet.py +++ b/src/primaite/simulator/network/protocols/packet.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any from pydantic import BaseModel diff --git a/src/primaite/simulator/network/protocols/ssh.py b/src/primaite/simulator/network/protocols/ssh.py index be7f842f..03411fb5 100644 --- a/src/primaite/simulator/network/protocols/ssh.py +++ b/src/primaite/simulator/network/protocols/ssh.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import IntEnum from typing import Optional diff --git a/src/primaite/simulator/network/transmission/__init__.py b/src/primaite/simulator/network/transmission/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/network/transmission/__init__.py +++ b/src/primaite/simulator/network/transmission/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index 259d62e3..e7c2a124 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from datetime import datetime from typing import Any, Optional diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index 49dcd1f5..7a6b34c9 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from pydantic import BaseModel diff --git a/src/primaite/simulator/network/transmission/primaite_layer.py b/src/primaite/simulator/network/transmission/primaite_layer.py index 981b6fbc..8ff4ac02 100644 --- a/src/primaite/simulator/network/transmission/primaite_layer.py +++ b/src/primaite/simulator/network/transmission/primaite_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from pydantic import BaseModel diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index 10cf802c..689eea2f 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import List diff --git a/src/primaite/simulator/network/utils.py b/src/primaite/simulator/network/utils.py index 4fd1834a..b4d6c815 100644 --- a/src/primaite/simulator/network/utils.py +++ b/src/primaite/simulator/network/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Union diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 809b52db..2a1deef4 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from primaite.interface.request import RequestResponse diff --git a/src/primaite/simulator/system/__init__.py b/src/primaite/simulator/system/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/__init__.py +++ b/src/primaite/simulator/system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/__init__.py b/src/primaite/simulator/system/applications/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/applications/__init__.py +++ b/src/primaite/simulator/system/applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index a7871315..1752c09a 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index cd4b2a03..840214f3 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index e2b9117d..f064eae3 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, Final, List, Optional, Set, Tuple, Union diff --git a/src/primaite/simulator/system/applications/red_applications/__init__.py b/src/primaite/simulator/system/applications/red_applications/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/applications/red_applications/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index 60e39743..33cc555f 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Optional, Union from pydantic import BaseModel, Field, field_validator, ValidationInfo diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index f77bc33a..4cd54d69 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from enum import Enum from ipaddress import IPv4Address diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index c0c3d872..b25eea6e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index f948d696..654b86e7 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 9fdbae57..0423087e 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index fb2c8847..99c4acb3 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 93b4c50d..3a8ac5ae 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index c57a9bd3..ff185e2a 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address from typing import Dict, List, Optional diff --git a/src/primaite/simulator/system/core/__init__.py b/src/primaite/simulator/system/core/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/core/__init__.py +++ b/src/primaite/simulator/system/core/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index ea8b00a5..813c288e 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import logging from pathlib import Path diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 75322e86..48f1f383 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address, IPv4Network diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 2f19a8b0..5e63f2ec 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from copy import deepcopy from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union diff --git a/src/primaite/simulator/system/core/sys_log.py b/src/primaite/simulator/system/core/sys_log.py index 9e22696d..741e5d33 100644 --- a/src/primaite/simulator/system/core/sys_log.py +++ b/src/primaite/simulator/system/core/sys_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import logging from pathlib import Path diff --git a/src/primaite/simulator/system/processes/__init__.py b/src/primaite/simulator/system/processes/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/processes/__init__.py +++ b/src/primaite/simulator/system/processes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index 225505c8..ad2babc1 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from enum import Enum from typing import Dict diff --git a/src/primaite/simulator/system/services/__init__.py b/src/primaite/simulator/system/services/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/__init__.py +++ b/src/primaite/simulator/system/services/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/__init__.py b/src/primaite/simulator/system/services/access/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/access/__init__.py +++ b/src/primaite/simulator/system/services/access/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_manager.py b/src/primaite/simulator/system/services/access/user_manager.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/access/user_manager.py +++ b/src/primaite/simulator/system/services/access/user_manager.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_session_manager.py b/src/primaite/simulator/system/services/access/user_session_manager.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/access/user_session_manager.py +++ b/src/primaite/simulator/system/services/access/user_session_manager.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/arp/__init__.py b/src/primaite/simulator/system/services/arp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/arp/__init__.py +++ b/src/primaite/simulator/system/services/arp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 816eb99e..31938e83 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/database/__init__.py b/src/primaite/simulator/system/services/database/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/database/__init__.py +++ b/src/primaite/simulator/system/services/database/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index b7cd8886..3a5f5b31 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 diff --git a/src/primaite/simulator/system/services/dns/__init__.py b/src/primaite/simulator/system/services/dns/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/dns/__init__.py +++ b/src/primaite/simulator/system/services/dns/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 78642fa6..02cf54ae 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 5b380320..b7c9a42c 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, Optional diff --git a/src/primaite/simulator/system/services/ftp/__init__.py b/src/primaite/simulator/system/services/ftp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/ftp/__init__.py +++ b/src/primaite/simulator/system/services/ftp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 00b70332..9c7b91ce 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 671200f5..9ce7d658 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Optional from primaite import getLogger diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 77d82997..52f451e1 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import ABC from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/icmp/__init__.py b/src/primaite/simulator/system/services/icmp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/icmp/__init__.py +++ b/src/primaite/simulator/system/services/icmp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 84ad995d..933d0591 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import secrets from ipaddress import IPv4Address from typing import Any, Dict, Optional, Tuple, Union diff --git a/src/primaite/simulator/system/services/icmp/router_icmp.py b/src/primaite/simulator/system/services/icmp/router_icmp.py index 19c0ac2d..63fbd4b2 100644 --- a/src/primaite/simulator/system/services/icmp/router_icmp.py +++ b/src/primaite/simulator/system/services/icmp/router_icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # class RouterICMP(ICMP): # """ # A class to represent a router's Internet Control Message Protocol (ICMP) handler. diff --git a/src/primaite/simulator/system/services/ntp/__init__.py b/src/primaite/simulator/system/services/ntp/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/ntp/__init__.py +++ b/src/primaite/simulator/system/services/ntp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index ed89971f..9606c61f 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from datetime import datetime from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index b674a296..6e73ccc6 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from datetime import datetime from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 4f0b879c..3dc080b4 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/terminal/__init__.py b/src/primaite/simulator/system/services/terminal/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/terminal/__init__.py +++ b/src/primaite/simulator/system/services/terminal/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index ae3557f7..e26e77f6 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/web_server/__init__.py b/src/primaite/simulator/system/services/web_server/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/simulator/system/services/web_server/__init__.py +++ b/src/primaite/simulator/system/services/web_server/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 75d9c472..1aab374d 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Optional from urllib.parse import urlparse diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 6fb09a16..34c893eb 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from abc import abstractmethod from datetime import datetime diff --git a/src/primaite/utils/__init__.py b/src/primaite/utils/__init__.py index 4d7c430e..1dced372 100644 --- a/src/primaite/utils/__init__.py +++ b/src/primaite/utils/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Utilities for PrimAITE.""" diff --git a/src/primaite/utils/cli/__init__.py b/src/primaite/utils/cli/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/utils/cli/__init__.py +++ b/src/primaite/utils/cli/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/cli/dev_cli.py b/src/primaite/utils/cli/dev_cli.py index 8946a4ca..581cd0b1 100644 --- a/src/primaite/utils/cli/dev_cli.py +++ b/src/primaite/utils/cli/dev_cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import click import typer from rich import print diff --git a/src/primaite/utils/cli/primaite_config_utils.py b/src/primaite/utils/cli/primaite_config_utils.py index 635be5a7..1fefd0a4 100644 --- a/src/primaite/utils/cli/primaite_config_utils.py +++ b/src/primaite/utils/cli/primaite_config_utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, Optional import yaml diff --git a/src/primaite/utils/converters.py b/src/primaite/utils/converters.py index f803851d..95956448 100644 --- a/src/primaite/utils/converters.py +++ b/src/primaite/utils/converters.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Any, Dict diff --git a/src/primaite/utils/package_data.py b/src/primaite/utils/package_data.py index af0252f9..ed091dd0 100644 --- a/src/primaite/utils/package_data.py +++ b/src/primaite/utils/package_data.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import os from logging import Logger from pathlib import Path diff --git a/src/primaite/utils/session_metadata_parser.py b/src/primaite/utils/session_metadata_parser.py index f6594666..1a7345ea 100644 --- a/src/primaite/utils/session_metadata_parser.py +++ b/src/primaite/utils/session_metadata_parser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/session_output_reader.py b/src/primaite/utils/session_output_reader.py index b9ad68a1..f25bbe6a 100644 --- a/src/primaite/utils/session_output_reader.py +++ b/src/primaite/utils/session_output_reader.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/session_output_writer.py b/src/primaite/utils/session_output_writer.py index 75a97f60..a8cefe35 100644 --- a/src/primaite/utils/session_output_writer.py +++ b/src/primaite/utils/session_output_writer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/validation/__init__.py b/src/primaite/utils/validation/__init__.py index be6c00e7..836b79af 100644 --- a/src/primaite/utils/validation/__init__.py +++ b/src/primaite/utils/validation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/validation/ip_protocol.py b/src/primaite/utils/validation/ip_protocol.py index 4e358305..654a5156 100644 --- a/src/primaite/utils/validation/ip_protocol.py +++ b/src/primaite/utils/validation/ip_protocol.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Define a custom IP protocol validator from typing import Any diff --git a/src/primaite/utils/validation/ipv4_address.py b/src/primaite/utils/validation/ipv4_address.py index eb0e2574..c385ed1e 100644 --- a/src/primaite/utils/validation/ipv4_address.py +++ b/src/primaite/utils/validation/ipv4_address.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address diff --git a/src/primaite/utils/validation/port.py b/src/primaite/utils/validation/port.py index 90c36add..564e843c 100644 --- a/src/primaite/utils/validation/port.py +++ b/src/primaite/utils/validation/port.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Define a custom port validator from typing import Any diff --git a/tests/__init__.py b/tests/__init__.py index 846ec808..900649b2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Final diff --git a/tests/conftest.py b/tests/conftest.py index bd1b79ee..0d73aa07 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, Tuple import pytest @@ -471,33 +471,6 @@ def game_and_agent(): action_space = ActionManager( actions=actions, # ALL POSSIBLE ACTIONS - nodes=[ - { - "node_name": "client_1", - "applications": [ - {"application_name": "WebBrowser"}, - {"application_name": "DoSBot"}, - {"application_name": "C2Server"}, - ], - "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], - }, - { - "node_name": "server_1", - "services": [{"service_name": "DNSServer"}], - "applications": [{"application_name": "C2Beacon"}], - }, - {"node_name": "server_2", "services": [{"service_name": "WebServer"}]}, - {"node_name": "router"}, - ], - max_folders_per_node=2, - max_files_per_folder=2, - max_services_per_node=2, - max_applications_per_node=3, - max_nics_per_node=2, - max_acl_rules=10, - protocols=["TCP", "UDP", "ICMP"], - ports=["HTTP", "DNS", "ARP"], - ip_list=["10.0.1.1", "10.0.1.2", "10.0.2.1", "10.0.2.2", "10.0.2.3"], act_map={}, ) observation_space = ObservationManager(NestedObservation(components={})) diff --git a/tests/e2e_integration_tests/__init__.py b/tests/e2e_integration_tests/__init__.py index be6c00e7..836b79af 100644 --- a/tests/e2e_integration_tests/__init__.py +++ b/tests/e2e_integration_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/__init__.py b/tests/e2e_integration_tests/action_masking/__init__.py index be6c00e7..836b79af 100644 --- a/tests/e2e_integration_tests/action_masking/__init__.py +++ b/tests/e2e_integration_tests/action_masking/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py index addf6dca..a34d430b 100644 --- a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py +++ b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict import pytest diff --git a/tests/e2e_integration_tests/environments/__init__.py b/tests/e2e_integration_tests/environments/__init__.py index be6c00e7..836b79af 100644 --- a/tests/e2e_integration_tests/environments/__init__.py +++ b/tests/e2e_integration_tests/environments/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py index 26e690d0..06b080d8 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from ray.rllib.algorithms.ppo import PPOConfig diff --git a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py index 265257e4..da0ca458 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import tempfile from pathlib import Path diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index a07d5d2e..9ca3525a 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Test that we can create a primaite environment and train sb3 agent with no crash.""" import tempfile from pathlib import Path diff --git a/tests/e2e_integration_tests/test_environment.py b/tests/e2e_integration_tests/test_environment.py index dcd51193..881681aa 100644 --- a/tests/e2e_integration_tests/test_environment.py +++ b/tests/e2e_integration_tests/test_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pydantic import pytest import yaml diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 7ec38d72..fa4781db 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/__init__.py +++ b/tests/integration_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/cli/__init__.py b/tests/integration_tests/cli/__init__.py index cfce7ae6..603d228f 100644 --- a/tests/integration_tests/cli/__init__.py +++ b/tests/integration_tests/cli/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List from typer.testing import CliRunner, Result diff --git a/tests/integration_tests/cli/test_dev_cli.py b/tests/integration_tests/cli/test_dev_cli.py index cd390555..16c3de9f 100644 --- a/tests/integration_tests/cli/test_dev_cli.py +++ b/tests/integration_tests/cli/test_dev_cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import os import shutil import tempfile diff --git a/tests/integration_tests/component_creation/__init__.py b/tests/integration_tests/component_creation/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/component_creation/__init__.py +++ b/tests/integration_tests/component_creation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index 7bdc80fc..8b81b7d3 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.core import RequestType from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/component_creation/test_permission_system.py b/tests/integration_tests/component_creation/test_permission_system.py index baf75523..c7faa81b 100644 --- a/tests/integration_tests/component_creation/test_permission_system.py +++ b/tests/integration_tests/component_creation/test_permission_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from typing import Dict, List, Literal diff --git a/tests/integration_tests/configuration_file_parsing/__init__.py b/tests/integration_tests/configuration_file_parsing/__init__.py index 7e23a4c2..09861acb 100644 --- a/tests/integration_tests/configuration_file_parsing/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py index 7f251613..234e7342 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index d10c7dbb..16f4dee5 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py index 8526ab78..764a7aac 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.config.load import data_manipulation_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index a642564c..0ff6754d 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py index 13be830b..c588829b 100644 --- a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py +++ b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py index 32d88c92..4153adc0 100644 --- a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_io_settings.py b/tests/integration_tests/configuration_file_parsing/test_io_settings.py index 82977b82..79812d80 100644 --- a/tests/integration_tests/configuration_file_parsing/test_io_settings.py +++ b/tests/integration_tests/configuration_file_parsing/test_io_settings.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py index 26fc562d..016d264f 100644 --- a/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py index 168ebee0..b1c644cc 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from pathlib import Path from typing import Union diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 70dc7cba..9863dbba 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address from typing import Dict, List, Optional diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index e4100741..37a05b6e 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict from prettytable import MARKDOWN, PrettyTable diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 80f7e3c3..4af1b748 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index ddaf4a1e..0924a91b 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 5addcbd7..5515d900 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import os from primaite.config.load import get_extended_config_path diff --git a/tests/integration_tests/game_layer/actions/__init__.py b/tests/integration_tests/game_layer/actions/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/game_layer/actions/__init__.py +++ b/tests/integration_tests/game_layer/actions/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index 36a7ae57..e90fa591 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index d73c9834..36fee9a0 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 7bf45fb4..8c97573a 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 1c143aed..91aa9fcd 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import uuid from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index e5e0806a..56bbbd4e 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import uuid from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index d796b75e..8846809d 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index c34103bc..8fbbbd70 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index 3054c73b..ebc9fd3b 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index a70cea72..96110656 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/observations/__init__.py b/tests/integration_tests/game_layer/observations/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/game_layer/observations/__init__.py +++ b/tests/integration_tests/game_layer/observations/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index e7212f3c..02cf005a 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index e2ab2990..0268cb95 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 05cf910c..97608132 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.firewall_observation import FirewallObservation from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py index 7d1c1939..630e29ea 100644 --- a/tests/integration_tests/game_layer/observations/test_link_observations.py +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 8254dad2..0ad03198 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py index 69d9f106..63ca8f6b 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy from uuid import uuid4 diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index 4ced02f5..f4bfb193 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pprint import pprint from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py index 998aa755..291ee395 100644 --- a/tests/integration_tests/game_layer/observations/test_software_observations.py +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index e7287eee..92c533c9 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index e772af32..464f95db 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from pprint import pprint import pytest diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 7a1475c2..22c00aa4 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 859c056c..ff86dbf0 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # Plan for creating integration tests for the actions: # I need to test that the requests coming out of the actions have the intended effect on the simulation. # I can do this by creating a simulation, and then running the action on the simulation, and then checking diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index d5679007..23364f13 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from gymnasium import spaces from primaite.game.agent.observations.file_system_observations import FileObservation diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 882c0923..a2453782 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/network/__init__.py b/tests/integration_tests/network/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/network/__init__.py +++ b/tests/integration_tests/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py index e000f6ae..e8abc0f2 100644 --- a/tests/integration_tests/network/test_airspace_config.py +++ b/tests/integration_tests/network/test_airspace_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py index b7317c3d..36c77fe1 100644 --- a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.file_system.file_type import FileType from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.system.services.ftp.ftp_client import FTPClient diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index f07f02e7..33fe70c3 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Tuple diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index debf5b1c..b32d9657 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 79452318..24fbfd05 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index fc2d146e..327c87e5 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_multi_lan_internet_example_network.py b/tests/integration_tests/network/test_multi_lan_internet_example_network.py index bcc9ad94..ea7e1c45 100644 --- a/tests/integration_tests/network/test_multi_lan_internet_example_network.py +++ b/tests/integration_tests/network/test_multi_lan_internet_example_network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.networks import multi_lan_internet_network_example diff --git a/tests/integration_tests/network/test_network_creation.py b/tests/integration_tests/network/test_network_creation.py index 794ddde5..1ee3ccc2 100644 --- a/tests/integration_tests/network/test_network_creation.py +++ b/tests/integration_tests/network/test_network_creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_nic_link_connection.py b/tests/integration_tests/network/test_nic_link_connection.py index ab9160c8..8c45f511 100644 --- a/tests/integration_tests/network/test_nic_link_connection.py +++ b/tests/integration_tests/network/test_nic_link_connection.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Link diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 04cdbe78..948b409f 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/network/test_switched_network.py b/tests/integration_tests/network/test_switched_network.py index ae0aa8a7..67392da3 100644 --- a/tests/integration_tests/network/test_switched_network.py +++ b/tests/integration_tests/network/test_switched_network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK def test_switched_network(client_switch_server): """Tests a node can ping another node via the switch.""" computer, switch, server = client_switch_server diff --git a/tests/integration_tests/network/test_users_creation_from_config.py b/tests/integration_tests/network/test_users_creation_from_config.py index 8cd3b037..1963b1dd 100644 --- a/tests/integration_tests/network/test_users_creation_from_config.py +++ b/tests/integration_tests/network/test_users_creation_from_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index fb0035e9..26e50f4a 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/system/__init__.py b/tests/integration_tests/system/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/system/__init__.py +++ b/tests/integration_tests/system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 2cbd4d11..d88f8249 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 50b0ceac..3ef6469e 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index 1a09e875..cb0195f0 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index a5adbb04..14b83e6a 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index ffb5cc7f..fc7aa69c 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_arp.py b/tests/integration_tests/system/test_arp.py index be8656aa..055d58c6 100644 --- a/tests/integration_tests/system/test_arp.py +++ b/tests/integration_tests/system/test_arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.network.router import RouterARP from primaite.simulator.system.services.arp.arp import ARP from tests.integration_tests.network.test_routing import multi_hop_network diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 965b4ae8..674603fa 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 480a90bc..38caf1a2 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index 22c5d484..fa4df0a9 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index c52b5caa..d1925a94 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address, IPv4Network diff --git a/tests/integration_tests/system/test_ntp_client_server.py b/tests/integration_tests/system/test_ntp_client_server.py index 957c1aeb..42340eb3 100644 --- a/tests/integration_tests/system/test_ntp_client_server.py +++ b/tests/integration_tests/system/test_ntp_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from time import sleep from typing import Tuple 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 7a085ee1..2d3679ed 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Any, Dict, List, Set import yaml diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index cf9728ce..4e73a050 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_user_session_manager_logins.py b/tests/integration_tests/system/test_user_session_manager_logins.py index 4318530c..0c591a4b 100644 --- a/tests/integration_tests/system/test_user_session_manager_logins.py +++ b/tests/integration_tests/system/test_user_session_manager_logins.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple from uuid import uuid4 diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index 05cbae4f..c1028e8e 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index f2ac1183..8fb6dc18 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/test_simulation/__init__.py b/tests/integration_tests/test_simulation/__init__.py index be6c00e7..836b79af 100644 --- a/tests/integration_tests/test_simulation/__init__.py +++ b/tests/integration_tests/test_simulation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index a767f365..21152199 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK # some test cases: # 0. test that sending a request to a valid target results in a success # 1. test that sending a request to a component that doesn't exist results in a failure diff --git a/tests/mock_and_patch/__init__.py b/tests/mock_and_patch/__init__.py index be6c00e7..836b79af 100644 --- a/tests/mock_and_patch/__init__.py +++ b/tests/mock_and_patch/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/mock_and_patch/get_session_path_mock.py b/tests/mock_and_patch/get_session_path_mock.py index f315fca4..073028a7 100644 --- a/tests/mock_and_patch/get_session_path_mock.py +++ b/tests/mock_and_patch/get_session_path_mock.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import tempfile from datetime import datetime from pathlib import Path diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/__init__.py +++ b/tests/unit_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/__init__.py b/tests/unit_tests/_primaite/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/__init__.py +++ b/tests/unit_tests/_primaite/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/__init__.py b/tests/unit_tests/_primaite/_game/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_game/__init__.py +++ b/tests/unit_tests/_primaite/_game/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/_agent/__init__.py b/tests/unit_tests/_primaite/_game/_agent/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_game/_agent/__init__.py +++ b/tests/unit_tests/_primaite/_game/_agent/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index 46963015..9021b8af 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from unittest.mock import Mock import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py index d61e1a23..a7713437 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index 7f590685..bb3ad33c 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 2fd2da0c..8c06aeed 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 78113f5f..67c4290d 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import ( diff --git a/tests/unit_tests/_primaite/_interface/__init__.py b/tests/unit_tests/_primaite/_interface/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_interface/__init__.py +++ b/tests/unit_tests/_primaite/_interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_interface/test_request.py b/tests/unit_tests/_primaite/_interface/test_request.py index 6067f9e4..d9fae083 100644 --- a/tests/unit_tests/_primaite/_interface/test_request.py +++ b/tests/unit_tests/_primaite/_interface/test_request.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from pydantic import ValidationError diff --git a/tests/unit_tests/_primaite/_session/__init__.py b/tests/unit_tests/_primaite/_session/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_session/__init__.py +++ b/tests/unit_tests/_primaite/_session/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_session/test_episode_schedule.py b/tests/unit_tests/_primaite/_session/test_episode_schedule.py index 21448339..ff26bb02 100644 --- a/tests/unit_tests/_primaite/_session/test_episode_schedule.py +++ b/tests/unit_tests/_primaite/_session/test_episode_schedule.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/unit_tests/_primaite/_simulator/__init__.py b/tests/unit_tests/_primaite/_simulator/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_domain/__init__.py b/tests/unit_tests/_primaite/_simulator/_domain/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py index 8db68565..f5294844 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Test the account module of the simulator.""" import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py b/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py b/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py index 0b9bdc8e..6cbf93c8 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import warnings import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 594c7afe..4ec1ec57 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py index 4eb0dd10..5554b9ef 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py index 7d022ea4..44a4e22a 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py index 724d7903..473e0db2 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 4a561b97..609e29c4 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import warnings from typing import Tuple diff --git a/tests/unit_tests/_primaite/_simulator/_network/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 6eca0c44..79392d66 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index fe9387de..fe0c3a57 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index 2613d536..e6bff60e 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py index f35cf171..5cff4407 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import NetworkInterface, Node diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py index 29d5ec67..f9ff0328 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import re from ipaddress import IPv4Address diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 44c5c781..605f8c3b 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py index e7e425b1..161d9cb4 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py index 658726b5..990a0bbf 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index f764f9b5..b1de710a 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import json import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py index 2e86ebbc..9885df67 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py index c80189c1..d86aa876 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.utils import convert_bytes_to_megabits, convert_megabits_to_bytes diff --git a/tests/unit_tests/_primaite/_simulator/_system/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 12dddf67..4ff387ce 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index 34a29cd0..6e9ee224 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index e9762476..9d8b7809 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py index 0e9c536c..a69dc844 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.application import Application, ApplicationOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py index f97e915e..16a4c9ad 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.system.applications.application import Application diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py index aef5d6d1..dd29f18e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index e456ed78..5917fde7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple from uuid import uuid4 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index f1be475a..f78b3261 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index 9e7ab1d2..ef165c8f 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index db7e8d58..1bc5b353 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index c64602c0..3bc2b1a4 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 95788834..d3e679db 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 291cdede..37c3d019 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py index 537beb8b..60cd2422 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py index 8c12adaa..ad6fe135 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 9b6a4bf3..08bef92d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Tuple from uuid import uuid4 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 54f86ec8..606a195c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py b/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py index 053211cd..5a734b6e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py +++ b/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 300f8d9d..a203a636 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict import pytest diff --git a/tests/unit_tests/_primaite/_simulator/test_core.py b/tests/unit_tests/_primaite/_simulator/test_core.py index 02960978..271375eb 100644 --- a/tests/unit_tests/_primaite/_simulator/test_core.py +++ b/tests/unit_tests/_primaite/_simulator/test_core.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Callable, Dict, List, Literal, Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/test_sim_container.py b/tests/unit_tests/_primaite/_simulator/test_sim_container.py index fe702307..f482d7e6 100644 --- a/tests/unit_tests/_primaite/_simulator/test_sim_container.py +++ b/tests/unit_tests/_primaite/_simulator/test_sim_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.sim_container import Simulation diff --git a/tests/unit_tests/_primaite/_utils/__init__.py b/tests/unit_tests/_primaite/_utils/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_utils/__init__.py +++ b/tests/unit_tests/_primaite/_utils/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/__init__.py b/tests/unit_tests/_primaite/_utils/_validation/__init__.py index be6c00e7..836b79af 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/__init__.py +++ b/tests/unit_tests/_primaite/_utils/_validation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py index 27829570..7acbe4a7 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py +++ b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.utils.validation.ip_protocol import IPProtocol, is_valid_protocol, PROTOCOL_LOOKUP, protocol_validator diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_port.py b/tests/unit_tests/_primaite/_utils/_validation/test_port.py index 6a8a2429..2e30ab76 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/test_port.py +++ b/tests/unit_tests/_primaite/_utils/_validation/test_port.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import pytest from primaite.utils.validation.port import is_valid_port, Port, PORT_LOOKUP, port_validator diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index 1a1848ac..d0a64ece 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.utils.converters import convert_dict_enum_keys_to_enum_values from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP From ab2dd6ca2788325d9bce3061bd6f63515c7ce618 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 3 Jan 2025 14:41:45 +0000 Subject: [PATCH 105/224] Revert "#2912 - Actioning Review Comments" This reverts commit b11678a128183dbd1badaf663c2f57446f6258f3. --- benchmark/benchmark.py | 2 +- benchmark/primaite_benchmark.py | 2 +- benchmark/report.py | 2 +- benchmark/utils.py | 2 +- docs/_templates/custom-class-template.rst | 2 +- docs/_templates/custom-module-template.rst | 2 +- docs/api.rst | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/source/action_masking.rst | 2 +- docs/source/config.rst | 2 +- docs/source/configuration/agents.rst | 2 +- docs/source/configuration/game.rst | 2 +- docs/source/configuration/io_settings.rst | 2 +- docs/source/configuration/simulation.rst | 2 +- .../simulation/nodes/common/common.rst | 2 +- .../common/common_host_node_attributes.rst | 2 +- .../common/common_network_node_attributes.rst | 2 +- .../nodes/common/common_node_attributes.rst | 2 +- .../nodes/common/node_type_list.rst | 2 +- .../simulation/nodes/computer.rst | 2 +- .../simulation/nodes/firewall.rst | 2 +- .../simulation/nodes/network_examples.rst | 2 +- .../configuration/simulation/nodes/router.rst | 2 +- .../configuration/simulation/nodes/server.rst | 2 +- .../configuration/simulation/nodes/switch.rst | 2 +- .../simulation/software/applications.rst | 2 +- .../simulation/software/services.rst | 2 +- docs/source/customising_scenarios.rst | 2 +- docs/source/dependencies.rst | 2 +- docs/source/developer_tools.rst | 2 +- docs/source/environment.rst | 2 +- docs/source/example_notebooks.rst | 2 +- docs/source/game_layer.rst | 2 +- docs/source/getting_started.rst | 2 +- docs/source/glossary.rst | 2 +- .../how_to_guides/extensible_actions.rst | 2 +- docs/source/node_sets.rst | 2 +- docs/source/notebooks/executed_notebooks.rst | 2 +- docs/source/primaite-dependencies.rst | 2 +- docs/source/request_system.rst | 2 +- docs/source/rewards.rst | 2 +- docs/source/simulation.rst | 2 +- .../network/airspace.rst | 2 +- .../network/base_hardware.rst | 2 +- .../simulation_components/network/network.rst | 2 +- .../network/network_interfaces.rst | 2 +- .../network/nodes/firewall.rst | 2 +- .../network/nodes/host_node.rst | 2 +- .../network/nodes/network_node.rst | 2 +- .../network/nodes/router.rst | 2 +- .../network/nodes/switch.rst | 2 +- .../network/nodes/wireless_router.rst | 2 +- .../network/transport_to_data_link_layer.rst | 2 +- .../system/applications/c2_suite.rst | 2 +- .../applications/data_manipulation_bot.rst | 2 +- .../system/applications/database_client.rst | 2 +- .../system/applications/dos_bot.rst | 2 +- .../system/applications/nmap.rst | 2 +- .../system/applications/ransomware_script.rst | 2 +- .../system/applications/web_browser.rst | 2 +- .../system/common/common_configuration.rst | 2 +- .../system/common/db_payload_list.rst | 2 +- .../system/internal_frame_processing.rst | 2 +- .../system/list_of_applications.rst | 2 +- .../system/list_of_services.rst | 2 +- .../system/list_of_system_applications.rst | 2 +- .../system/list_of_system_services.rst | 2 +- .../simulation_components/system/pcap.rst | 2 +- .../system/services/database_service.rst | 2 +- .../system/services/dns_client.rst | 2 +- .../system/services/dns_server.rst | 2 +- .../system/services/ftp_client.rst | 2 +- .../system/services/ftp_server.rst | 2 +- .../system/services/ntp_client.rst | 2 +- .../system/services/ntp_server.rst | 2 +- .../system/services/terminal.rst | 2 +- .../system/services/web_server.rst | 2 +- .../system/session_and_software_manager.rst | 2 +- .../simulation_components/system/software.rst | 2 +- .../simulation_components/system/sys_log.rst | 2 +- docs/source/simulation_structure.rst | 2 +- docs/source/state_system.rst | 2 +- docs/source/varying_config_files.rst | 2 +- src/primaite/__init__.py | 2 +- src/primaite/_legacy/actions.py | 2 +- src/primaite/cli.py | 2 +- src/primaite/config/__init__.py | 2 +- src/primaite/config/load.py | 2 +- src/primaite/exceptions.py | 2 +- src/primaite/game/__init__.py | 2 +- src/primaite/game/agent/__init__.py | 2 +- src/primaite/game/agent/actions/__init__.py | 2 +- src/primaite/game/agent/actions/abstract.py | 2 +- src/primaite/game/agent/actions/acl.py | 2 +- .../game/agent/actions/application.py | 2 +- src/primaite/game/agent/actions/config.py | 2 +- src/primaite/game/agent/actions/file.py | 2 +- src/primaite/game/agent/actions/folder.py | 2 +- src/primaite/game/agent/actions/host_nic.py | 2 +- src/primaite/game/agent/actions/manager.py | 4 ++- src/primaite/game/agent/actions/network.py | 2 +- src/primaite/game/agent/actions/node.py | 2 +- src/primaite/game/agent/actions/service.py | 2 +- src/primaite/game/agent/actions/session.py | 2 +- src/primaite/game/agent/agent_log.py | 2 +- src/primaite/game/agent/interface.py | 2 +- .../game/agent/observations/__init__.py | 2 +- .../agent/observations/acl_observation.py | 2 +- .../observations/file_system_observations.py | 2 +- .../observations/firewall_observation.py | 2 +- .../agent/observations/host_observations.py | 2 +- .../agent/observations/link_observation.py | 2 +- .../agent/observations/nic_observations.py | 2 +- .../agent/observations/node_observations.py | 2 +- .../agent/observations/observation_manager.py | 2 +- .../game/agent/observations/observations.py | 2 +- .../agent/observations/router_observation.py | 2 +- .../observations/software_observation.py | 2 +- src/primaite/game/agent/rewards.py | 2 +- .../game/agent/scripted_agents/__init__.py | 2 +- .../scripted_agents/data_manipulation_bot.py | 2 +- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 2 +- .../game/agent/scripted_agents/tap001.py | 2 +- src/primaite/game/agent/utils.py | 2 +- src/primaite/game/game.py | 2 +- src/primaite/game/science.py | 2 +- src/primaite/interface/__init__.py | 2 +- src/primaite/interface/request.py | 2 +- src/primaite/session/__init__.py | 2 +- src/primaite/session/environment.py | 2 +- src/primaite/session/episode_schedule.py | 2 +- src/primaite/session/io.py | 2 +- src/primaite/session/ray_envs.py | 2 +- src/primaite/setup/__init__.py | 2 +- src/primaite/setup/reset_demo_notebooks.py | 2 +- src/primaite/setup/reset_example_configs.py | 2 +- src/primaite/simulator/__init__.py | 2 +- src/primaite/simulator/core.py | 2 +- src/primaite/simulator/domain/__init__.py | 2 +- src/primaite/simulator/domain/account.py | 2 +- src/primaite/simulator/domain/controller.py | 2 +- .../simulator/file_system/__init__.py | 2 +- src/primaite/simulator/file_system/file.py | 2 +- .../simulator/file_system/file_system.py | 2 +- .../file_system/file_system_item_abc.py | 2 +- .../simulator/file_system/file_type.py | 2 +- src/primaite/simulator/file_system/folder.py | 2 +- src/primaite/simulator/network/__init__.py | 2 +- src/primaite/simulator/network/airspace.py | 2 +- src/primaite/simulator/network/container.py | 2 +- src/primaite/simulator/network/creation.py | 2 +- .../simulator/network/hardware/__init__.py | 2 +- .../simulator/network/hardware/base.py | 2 +- .../hardware/network_interface/__init__.py | 2 +- .../network_interface/wireless/__init__.py | 2 +- .../wireless/wireless_access_point.py | 2 +- .../wireless/wireless_nic.py | 2 +- .../network/hardware/node_operating_state.py | 2 +- .../network/hardware/nodes/__init__.py | 2 +- .../network/hardware/nodes/host/__init__.py | 2 +- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../network/hardware/nodes/host/server.py | 2 +- .../hardware/nodes/network/__init__.py | 2 +- .../hardware/nodes/network/firewall.py | 2 +- .../hardware/nodes/network/network_node.py | 2 +- .../network/hardware/nodes/network/router.py | 2 +- .../network/hardware/nodes/network/switch.py | 2 +- .../hardware/nodes/network/wireless_router.py | 2 +- src/primaite/simulator/network/networks.py | 2 +- src/primaite/simulator/network/nmne.py | 2 +- .../simulator/network/protocols/__init__.py | 2 +- .../simulator/network/protocols/arp.py | 2 +- .../simulator/network/protocols/dns.py | 2 +- .../simulator/network/protocols/ftp.py | 2 +- .../simulator/network/protocols/http.py | 2 +- .../simulator/network/protocols/icmp.py | 2 +- .../simulator/network/protocols/masquerade.py | 2 +- .../simulator/network/protocols/ntp.py | 2 +- .../simulator/network/protocols/packet.py | 2 +- .../simulator/network/protocols/ssh.py | 2 +- .../network/transmission/__init__.py | 2 +- .../network/transmission/data_link_layer.py | 2 +- .../network/transmission/network_layer.py | 2 +- .../network/transmission/primaite_layer.py | 2 +- .../network/transmission/transport_layer.py | 2 +- src/primaite/simulator/network/utils.py | 2 +- src/primaite/simulator/sim_container.py | 2 +- src/primaite/simulator/system/__init__.py | 2 +- .../simulator/system/applications/__init__.py | 2 +- .../system/applications/application.py | 2 +- .../system/applications/database_client.py | 2 +- .../simulator/system/applications/nmap.py | 2 +- .../applications/red_applications/__init__.py | 2 +- .../red_applications/c2/__init__.py | 2 +- .../red_applications/c2/abstract_c2.py | 2 +- .../red_applications/c2/c2_beacon.py | 2 +- .../red_applications/c2/c2_server.py | 2 +- .../red_applications/data_manipulation_bot.py | 2 +- .../applications/red_applications/dos_bot.py | 2 +- .../red_applications/ransomware_script.py | 2 +- .../system/applications/web_browser.py | 2 +- .../simulator/system/core/__init__.py | 2 +- .../simulator/system/core/packet_capture.py | 2 +- .../simulator/system/core/session_manager.py | 2 +- .../simulator/system/core/software_manager.py | 2 +- src/primaite/simulator/system/core/sys_log.py | 2 +- .../simulator/system/processes/__init__.py | 2 +- .../simulator/system/processes/process.py | 2 +- .../simulator/system/services/__init__.py | 2 +- .../system/services/access/__init__.py | 2 +- .../system/services/access/user_manager.py | 2 +- .../services/access/user_session_manager.py | 2 +- .../simulator/system/services/arp/__init__.py | 2 +- .../simulator/system/services/arp/arp.py | 2 +- .../system/services/database/__init__.py | 2 +- .../services/database/database_service.py | 2 +- .../simulator/system/services/dns/__init__.py | 2 +- .../system/services/dns/dns_client.py | 2 +- .../system/services/dns/dns_server.py | 2 +- .../simulator/system/services/ftp/__init__.py | 2 +- .../system/services/ftp/ftp_client.py | 2 +- .../system/services/ftp/ftp_server.py | 2 +- .../system/services/ftp/ftp_service.py | 2 +- .../system/services/icmp/__init__.py | 2 +- .../simulator/system/services/icmp/icmp.py | 2 +- .../system/services/icmp/router_icmp.py | 2 +- .../simulator/system/services/ntp/__init__.py | 2 +- .../system/services/ntp/ntp_client.py | 2 +- .../system/services/ntp/ntp_server.py | 2 +- .../simulator/system/services/service.py | 2 +- .../system/services/terminal/__init__.py | 2 +- .../system/services/terminal/terminal.py | 2 +- .../system/services/web_server/__init__.py | 2 +- .../system/services/web_server/web_server.py | 2 +- src/primaite/simulator/system/software.py | 2 +- src/primaite/utils/__init__.py | 2 +- src/primaite/utils/cli/__init__.py | 2 +- src/primaite/utils/cli/dev_cli.py | 2 +- .../utils/cli/primaite_config_utils.py | 2 +- src/primaite/utils/converters.py | 2 +- src/primaite/utils/package_data.py | 2 +- src/primaite/utils/session_metadata_parser.py | 2 +- src/primaite/utils/session_output_reader.py | 2 +- src/primaite/utils/session_output_writer.py | 2 +- src/primaite/utils/validation/__init__.py | 2 +- src/primaite/utils/validation/ip_protocol.py | 2 +- src/primaite/utils/validation/ipv4_address.py | 2 +- src/primaite/utils/validation/port.py | 2 +- tests/__init__.py | 2 +- tests/conftest.py | 29 ++++++++++++++++++- tests/e2e_integration_tests/__init__.py | 2 +- .../action_masking/__init__.py | 2 +- .../test_agents_use_action_masks.py | 2 +- .../environments/__init__.py | 2 +- .../test_rllib_multi_agent_environment.py | 2 +- .../test_rllib_single_agent_environment.py | 2 +- .../environments/test_sb3_environment.py | 2 +- .../e2e_integration_tests/test_environment.py | 2 +- .../test_uc2_data_manipulation_scenario.py | 2 +- tests/integration_tests/__init__.py | 2 +- tests/integration_tests/cli/__init__.py | 2 +- tests/integration_tests/cli/test_dev_cli.py | 2 +- .../component_creation/__init__.py | 2 +- .../test_action_integration.py | 2 +- .../test_permission_system.py | 2 +- .../configuration_file_parsing/__init__.py | 2 +- .../nodes/__init__.py | 2 +- .../nodes/network/__init__.py | 2 +- .../nodes/network/test_firewall_config.py | 2 +- .../nodes/network/test_router_config.py | 2 +- .../nodes/test_node_config.py | 2 +- ...software_installation_and_configuration.py | 2 +- .../test_episode_scheduler.py | 2 +- .../test_game_options_config.py | 2 +- .../test_io_settings.py | 2 +- .../test_no_nodes_links_agents_config.py | 2 +- .../test_software_fix_duration.py | 2 +- .../applications/extended_application.py | 2 +- .../extensions/nodes/giga_switch.py | 2 +- .../extensions/nodes/super_computer.py | 2 +- .../extensions/services/extended_service.py | 2 +- .../extensions/test_extendable_config.py | 2 +- .../game_layer/actions/__init__.py | 2 +- .../test_application_request_permission.py | 2 +- .../actions/test_c2_suite_actions.py | 2 +- .../actions/test_configure_actions.py | 2 +- .../actions/test_file_request_permission.py | 2 +- .../actions/test_folder_request_permission.py | 2 +- .../actions/test_nic_request_permission.py | 2 +- .../actions/test_node_request_permission.py | 2 +- .../test_service_request_permission.py | 2 +- .../actions/test_terminal_actions.py | 2 +- .../game_layer/observations/__init__.py | 2 +- .../observations/test_acl_observations.py | 2 +- .../test_file_system_observations.py | 2 +- .../observations/test_firewall_observation.py | 2 +- .../observations/test_link_observations.py | 2 +- .../observations/test_nic_observations.py | 2 +- .../observations/test_node_observations.py | 2 +- .../observations/test_router_observation.py | 2 +- .../test_software_observations.py | 2 +- .../observations/test_user_observations.py | 2 +- .../game_layer/test_RNG_seed.py | 2 +- .../game_layer/test_action_mask.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_observations.py | 2 +- .../game_layer/test_rewards.py | 2 +- tests/integration_tests/network/__init__.py | 2 +- .../network/test_airspace_config.py | 2 +- ...ndwidth_load_checks_before_transmission.py | 2 +- .../network/test_broadcast.py | 2 +- .../network/test_capture_nmne.py | 2 +- .../network/test_firewall.py | 2 +- .../network/test_frame_transmission.py | 2 +- ...test_multi_lan_internet_example_network.py | 2 +- .../network/test_network_creation.py | 2 +- .../network/test_nic_link_connection.py | 2 +- .../integration_tests/network/test_routing.py | 2 +- .../network/test_switched_network.py | 2 +- .../test_users_creation_from_config.py | 2 +- .../network/test_wireless_router.py | 2 +- tests/integration_tests/system/__init__.py | 2 +- .../test_c2_suite_integration.py | 2 +- .../test_data_manipulation_bot_and_server.py | 2 +- .../test_dos_bot_and_server.py | 2 +- .../test_ransomware_script.py | 2 +- .../system/test_application_on_node.py | 2 +- tests/integration_tests/system/test_arp.py | 2 +- .../system/test_database_on_node.py | 2 +- .../system/test_dns_client_server.py | 2 +- .../system/test_ftp_client_server.py | 2 +- tests/integration_tests/system/test_nmap.py | 2 +- .../system/test_ntp_client_server.py | 2 +- .../system/test_service_listening_on_ports.py | 2 +- .../system/test_service_on_node.py | 2 +- .../test_user_session_manager_logins.py | 2 +- .../system/test_web_client_server.py | 2 +- .../test_web_client_server_and_database.py | 2 +- .../test_simulation/__init__.py | 2 +- .../test_simulation/test_request_response.py | 2 +- tests/mock_and_patch/__init__.py | 2 +- tests/mock_and_patch/get_session_path_mock.py | 2 +- tests/unit_tests/__init__.py | 2 +- tests/unit_tests/_primaite/__init__.py | 2 +- tests/unit_tests/_primaite/_game/__init__.py | 2 +- .../_primaite/_game/_agent/__init__.py | 2 +- .../_primaite/_game/_agent/test_actions.py | 2 +- .../_primaite/_game/_agent/test_agent_log.py | 2 +- .../_game/_agent/test_observations.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 2 +- .../_primaite/_interface/__init__.py | 2 +- .../_primaite/_interface/test_request.py | 2 +- .../unit_tests/_primaite/_session/__init__.py | 2 +- .../_session/test_episode_schedule.py | 2 +- .../_primaite/_simulator/__init__.py | 2 +- .../_primaite/_simulator/_domain/__init__.py | 2 +- .../_simulator/_domain/test_account.py | 2 +- .../_simulator/_domain/test_controller.py | 2 +- .../_simulator/_file_system/__init__.py | 2 +- .../_simulator/_file_system/test_file.py | 2 +- .../_file_system/test_file_actions.py | 2 +- .../_file_system/test_file_system.py | 2 +- .../_file_system/test_file_system_actions.py | 2 +- .../_simulator/_file_system/test_folder.py | 2 +- .../_file_system/test_folder_actions.py | 2 +- .../_primaite/_simulator/_network/__init__.py | 2 +- .../_simulator/_network/_hardware/__init__.py | 2 +- .../_network/_hardware/nodes/__init__.py | 2 +- .../_network/_hardware/nodes/test_acl.py | 2 +- .../_network/_hardware/nodes/test_router.py | 2 +- .../_network/_hardware/nodes/test_switch.py | 2 +- .../test_network_interface_actions.py | 2 +- .../_simulator/_network/_hardware/test_nic.py | 2 +- .../_network/_hardware/test_node_actions.py | 2 +- .../_network/_transmission/__init__.py | 2 +- .../_transmission/test_data_link_layer.py | 2 +- .../_transmission/test_network_layer.py | 2 +- .../_simulator/_network/test_container.py | 2 +- .../_simulator/_network/test_creation.py | 2 +- .../_simulator/_network/test_utils.py | 2 +- .../_primaite/_simulator/_system/__init__.py | 2 +- .../_system/_applications/__init__.py | 2 +- .../_red_applications/__init__.py | 2 +- .../_red_applications/test_c2_suite.py | 2 +- .../test_data_manipulation_bot.py | 2 +- .../_red_applications/test_dos_bot.py | 2 +- .../_applications/test_application_actions.py | 2 +- .../test_application_registry.py | 2 +- .../_applications/test_applications.py | 2 +- .../_applications/test_database_client.py | 2 +- .../_system/_applications/test_web_browser.py | 2 +- .../_simulator/_system/_services/__init__.py | 2 +- .../_system/_services/test_database.py | 2 +- .../_system/_services/test_dns_client.py | 2 +- .../_system/_services/test_dns_server.py | 2 +- .../_system/_services/test_ftp_client.py | 2 +- .../_system/_services/test_ftp_server.py | 2 +- .../_system/_services/test_service_actions.py | 2 +- .../_system/_services/test_services.py | 2 +- .../_system/_services/test_terminal.py | 2 +- .../_system/_services/test_web_server.py | 2 +- .../_simulator/_system/core/test_sys_log.py | 2 +- .../_simulator/_system/test_software.py | 2 +- .../_primaite/_simulator/test_core.py | 2 +- .../_simulator/test_sim_container.py | 2 +- tests/unit_tests/_primaite/_utils/__init__.py | 2 +- .../_primaite/_utils/_validation/__init__.py | 2 +- .../_utils/_validation/test_ip_protocol.py | 2 +- .../_primaite/_utils/_validation/test_port.py | 2 +- .../_utils/test_dict_enum_keys_conversion.py | 2 +- 414 files changed, 443 insertions(+), 414 deletions(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index ddedebb7..4ad398b9 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any, Dict, Optional, Tuple from gymnasium.core import ObsType diff --git a/benchmark/primaite_benchmark.py b/benchmark/primaite_benchmark.py index 70ea8900..86ed22a9 100644 --- a/benchmark/primaite_benchmark.py +++ b/benchmark/primaite_benchmark.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json import shutil from datetime import datetime diff --git a/benchmark/report.py b/benchmark/report.py index c11528ab..4035ceca 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json import sys from datetime import datetime diff --git a/benchmark/utils.py b/benchmark/utils.py index f17c64b7..2e92d80d 100644 --- a/benchmark/utils.py +++ b/benchmark/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import platform from typing import Dict diff --git a/docs/_templates/custom-class-template.rst b/docs/_templates/custom-class-template.rst index 71e992bc..920158d5 100644 --- a/docs/_templates/custom-class-template.rst +++ b/docs/_templates/custom-class-template.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. Credit to https://github.com/JamesALeedham/Sphinx-Autosummary-Recursion for the custom templates. diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst index 3a2ced35..98627e43 100644 --- a/docs/_templates/custom-module-template.rst +++ b/docs/_templates/custom-module-template.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. Credit to https://github.com/JamesALeedham/Sphinx-Autosummary-Recursion for the custom templates. diff --git a/docs/api.rst b/docs/api.rst index eb7e4719..977f9e87 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,7 +2,7 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. DO NOT DELETE THIS FILE! It contains the all-important `.. autosummary::` directive with `:recursive:` option, without diff --git a/docs/conf.py b/docs/conf.py index 60739499..318829fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: diff --git a/docs/index.rst b/docs/index.rst index 42cc1d6d..2ba43162 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Welcome to PrimAITE's documentation ==================================== diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index dad6a484..264ab254 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Action Masking ************** diff --git a/docs/source/config.rst b/docs/source/config.rst index 0fa4a4d5..eb0b9906 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK PrimAITE |VERSION| Configuration ******************************** diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index d11f7892..dece94c5 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``agents`` diff --git a/docs/source/configuration/game.rst b/docs/source/configuration/game.rst index b3c139b2..2048708c 100644 --- a/docs/source/configuration/game.rst +++ b/docs/source/configuration/game.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``game`` diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index ab3a978e..1c9585c9 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``io_settings`` diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index 0b2067d8..fa1d774a 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``simulation`` diff --git a/docs/source/configuration/simulation/nodes/common/common.rst b/docs/source/configuration/simulation/nodes/common/common.rst index c45eccf6..a0f2eb13 100644 --- a/docs/source/configuration/simulation/nodes/common/common.rst +++ b/docs/source/configuration/simulation/nodes/common/common.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _Node Attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst index b717340e..bb3b2a52 100644 --- a/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _common_host_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst index 035c7e55..d556e2dc 100644 --- a/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _common_network_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst index 542b817b..6a95911f 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _common_node_attributes: diff --git a/docs/source/configuration/simulation/nodes/common/node_type_list.rst b/docs/source/configuration/simulation/nodes/common/node_type_list.rst index 21181019..1ec496d9 100644 --- a/docs/source/configuration/simulation/nodes/common/node_type_list.rst +++ b/docs/source/configuration/simulation/nodes/common/node_type_list.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``type`` -------- diff --git a/docs/source/configuration/simulation/nodes/computer.rst b/docs/source/configuration/simulation/nodes/computer.rst index 456d11a2..32e0b2b9 100644 --- a/docs/source/configuration/simulation/nodes/computer.rst +++ b/docs/source/configuration/simulation/nodes/computer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _computer_configuration: diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst index 84b5c99e..775ffabd 100644 --- a/docs/source/configuration/simulation/nodes/firewall.rst +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _firewall_configuration: diff --git a/docs/source/configuration/simulation/nodes/network_examples.rst b/docs/source/configuration/simulation/nodes/network_examples.rst index 80e934e5..2a34a206 100644 --- a/docs/source/configuration/simulation/nodes/network_examples.rst +++ b/docs/source/configuration/simulation/nodes/network_examples.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _network_examples: diff --git a/docs/source/configuration/simulation/nodes/router.rst b/docs/source/configuration/simulation/nodes/router.rst index 4b41784c..ac9d6411 100644 --- a/docs/source/configuration/simulation/nodes/router.rst +++ b/docs/source/configuration/simulation/nodes/router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _router_configuration: diff --git a/docs/source/configuration/simulation/nodes/server.rst b/docs/source/configuration/simulation/nodes/server.rst index 616efb38..92b33ca7 100644 --- a/docs/source/configuration/simulation/nodes/server.rst +++ b/docs/source/configuration/simulation/nodes/server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _server_configuration: diff --git a/docs/source/configuration/simulation/nodes/switch.rst b/docs/source/configuration/simulation/nodes/switch.rst index d09f5ba7..17cf76f9 100644 --- a/docs/source/configuration/simulation/nodes/switch.rst +++ b/docs/source/configuration/simulation/nodes/switch.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _switch_configuration: diff --git a/docs/source/configuration/simulation/software/applications.rst b/docs/source/configuration/simulation/software/applications.rst index 9973a167..8c590d53 100644 --- a/docs/source/configuration/simulation/software/applications.rst +++ b/docs/source/configuration/simulation/software/applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``applications`` ---------------- diff --git a/docs/source/configuration/simulation/software/services.rst b/docs/source/configuration/simulation/software/services.rst index ec6bbba9..fafdf2e8 100644 --- a/docs/source/configuration/simulation/software/services.rst +++ b/docs/source/configuration/simulation/software/services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``services`` ------------ diff --git a/docs/source/customising_scenarios.rst b/docs/source/customising_scenarios.rst index df7d4b1e..092f306b 100644 --- a/docs/source/customising_scenarios.rst +++ b/docs/source/customising_scenarios.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Customising Agents ****************** diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst index e8be00d3..74f3cd14 100644 --- a/docs/source/dependencies.rst +++ b/docs/source/dependencies.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. role:: raw-html(raw) :format: html diff --git a/docs/source/developer_tools.rst b/docs/source/developer_tools.rst index b3d81a27..a66b7902 100644 --- a/docs/source/developer_tools.rst +++ b/docs/source/developer_tools.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _Developer Tools: diff --git a/docs/source/environment.rst b/docs/source/environment.rst index 251b1090..a282c09e 100644 --- a/docs/source/environment.rst +++ b/docs/source/environment.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK RL Environments *************** diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index 6caeae3d..920175c9 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _example jupyter notebooks: diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 58a274d9..775c02b5 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK PrimAITE Game layer ******************* diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 427d1823..ded92c60 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _getting-started: diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 02c578d1..8fff0ea3 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Glossary ============= diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 1c44c2b2..6e44a905 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _about: diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst index 3c247478..866f0139 100644 --- a/docs/source/node_sets.rst +++ b/docs/source/node_sets.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _network_node_adder: diff --git a/docs/source/notebooks/executed_notebooks.rst b/docs/source/notebooks/executed_notebooks.rst index f4acfad6..3431d344 100644 --- a/docs/source/notebooks/executed_notebooks.rst +++ b/docs/source/notebooks/executed_notebooks.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _Executed Notebooks: diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst index 14a96349..8367ee61 100644 --- a/docs/source/primaite-dependencies.rst +++ b/docs/source/primaite-dependencies.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ | Name | Version | License | Description | URL | diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index b89d0906..f2d2e68d 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Request System ************** diff --git a/docs/source/rewards.rst b/docs/source/rewards.rst index 254237ee..0163284c 100644 --- a/docs/source/rewards.rst +++ b/docs/source/rewards.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Rewards ####### diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index 95807703..cc723e40 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Simulation diff --git a/docs/source/simulation_components/network/airspace.rst b/docs/source/simulation_components/network/airspace.rst index a6967b91..06a884a7 100644 --- a/docs/source/simulation_components/network/airspace.rst +++ b/docs/source/simulation_components/network/airspace.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _airspace: diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index 8b325ffc..ce1e5c74 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ############# Base Hardware diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index 152b74b8..4cc121a3 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _network: diff --git a/docs/source/simulation_components/network/network_interfaces.rst b/docs/source/simulation_components/network/network_interfaces.rst index 663af7ba..c6b97a8e 100644 --- a/docs/source/simulation_components/network/network_interfaces.rst +++ b/docs/source/simulation_components/network/network_interfaces.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ################################# Network Interface Hierarchy Model diff --git a/docs/source/simulation_components/network/nodes/firewall.rst b/docs/source/simulation_components/network/nodes/firewall.rst index f2d7e61a..1ef16d63 100644 --- a/docs/source/simulation_components/network/nodes/firewall.rst +++ b/docs/source/simulation_components/network/nodes/firewall.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ######## Firewall diff --git a/docs/source/simulation_components/network/nodes/host_node.rst b/docs/source/simulation_components/network/nodes/host_node.rst index 2c1e75d0..b8aae098 100644 --- a/docs/source/simulation_components/network/nodes/host_node.rst +++ b/docs/source/simulation_components/network/nodes/host_node.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ######### diff --git a/docs/source/simulation_components/network/nodes/network_node.rst b/docs/source/simulation_components/network/nodes/network_node.rst index 4aebe09f..e1fa976c 100644 --- a/docs/source/simulation_components/network/nodes/network_node.rst +++ b/docs/source/simulation_components/network/nodes/network_node.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ############ Network Node diff --git a/docs/source/simulation_components/network/nodes/router.rst b/docs/source/simulation_components/network/nodes/router.rst index fb582b23..5d3de60f 100644 --- a/docs/source/simulation_components/network/nodes/router.rst +++ b/docs/source/simulation_components/network/nodes/router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ###### Router diff --git a/docs/source/simulation_components/network/nodes/switch.rst b/docs/source/simulation_components/network/nodes/switch.rst index e7143f0c..0ecbcbf3 100644 --- a/docs/source/simulation_components/network/nodes/switch.rst +++ b/docs/source/simulation_components/network/nodes/switch.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ###### Switch diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index d7207846..c0c245b2 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ###### Wireless Router diff --git a/docs/source/simulation_components/network/transport_to_data_link_layer.rst b/docs/source/simulation_components/network/transport_to_data_link_layer.rst index 54118c90..02bfdcdc 100644 --- a/docs/source/simulation_components/network/transport_to_data_link_layer.rst +++ b/docs/source/simulation_components/network/transport_to_data_link_layer.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Transport Layer to Data Link Layer ================================== diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 3dd2b4fc..d045949a 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _C2_Suite: diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 91c33ede..1a387514 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _DataManipulationBot: diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 75a396b5..1fea78ab 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _DatabaseClient: diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index 5c0ae86a..6ad45424 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _DoSBot: diff --git a/docs/source/simulation_components/system/applications/nmap.rst b/docs/source/simulation_components/system/applications/nmap.rst index a82735c8..a5615a43 100644 --- a/docs/source/simulation_components/system/applications/nmap.rst +++ b/docs/source/simulation_components/system/applications/nmap.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _NMAP: diff --git a/docs/source/simulation_components/system/applications/ransomware_script.rst b/docs/source/simulation_components/system/applications/ransomware_script.rst index b79ca802..5bff6991 100644 --- a/docs/source/simulation_components/system/applications/ransomware_script.rst +++ b/docs/source/simulation_components/system/applications/ransomware_script.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _RansomwareScript: diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index 7062887b..c56c450d 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _WebBrowser: diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index 411fd529..c53ac8b8 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _Common Configuration: diff --git a/docs/source/simulation_components/system/common/db_payload_list.rst b/docs/source/simulation_components/system/common/db_payload_list.rst index 89668665..0930f09d 100644 --- a/docs/source/simulation_components/system/common/db_payload_list.rst +++ b/docs/source/simulation_components/system/common/db_payload_list.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _Database Payload List: diff --git a/docs/source/simulation_components/system/internal_frame_processing.rst b/docs/source/simulation_components/system/internal_frame_processing.rst index f82dec13..65336f9b 100644 --- a/docs/source/simulation_components/system/internal_frame_processing.rst +++ b/docs/source/simulation_components/system/internal_frame_processing.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _internal_frame_processing: diff --git a/docs/source/simulation_components/system/list_of_applications.rst b/docs/source/simulation_components/system/list_of_applications.rst index a7e05ea6..94090d93 100644 --- a/docs/source/simulation_components/system/list_of_applications.rst +++ b/docs/source/simulation_components/system/list_of_applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. toctree:: :maxdepth: 1 diff --git a/docs/source/simulation_components/system/list_of_services.rst b/docs/source/simulation_components/system/list_of_services.rst index 2082ac6f..b6995647 100644 --- a/docs/source/simulation_components/system/list_of_services.rst +++ b/docs/source/simulation_components/system/list_of_services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. toctree:: :maxdepth: 1 diff --git a/docs/source/simulation_components/system/list_of_system_applications.rst b/docs/source/simulation_components/system/list_of_system_applications.rst index 0c66662f..c8807ef0 100644 --- a/docs/source/simulation_components/system/list_of_system_applications.rst +++ b/docs/source/simulation_components/system/list_of_system_applications.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``system applications`` """"""""""""""""""""""" diff --git a/docs/source/simulation_components/system/list_of_system_services.rst b/docs/source/simulation_components/system/list_of_system_services.rst index 01df4dc8..9b5c3265 100644 --- a/docs/source/simulation_components/system/list_of_system_services.rst +++ b/docs/source/simulation_components/system/list_of_system_services.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK ``system services`` """"""""""""""""""" diff --git a/docs/source/simulation_components/system/pcap.rst b/docs/source/simulation_components/system/pcap.rst index 0da28a39..830c28bd 100644 --- a/docs/source/simulation_components/system/pcap.rst +++ b/docs/source/simulation_components/system/pcap.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK PCAP ==== diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index b41c1097..f3e800cd 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _DatabaseService: diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index 6475b4d4..eca152f0 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _DNSClient: diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index 3d699048..1e30b9bd 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _DNSServer: diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index 47566e5f..f9c7b4ce 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _FTPClient: diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index e4cada29..f52fa043 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _FTPServer: diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index fb965029..7af831bf 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _NTPClient: diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index 68fadca9..a09c8bdd 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _NTPServer: diff --git a/docs/source/simulation_components/system/services/terminal.rst b/docs/source/simulation_components/system/services/terminal.rst index bc5cee48..6909786e 100644 --- a/docs/source/simulation_components/system/services/terminal.rst +++ b/docs/source/simulation_components/system/services/terminal.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _Terminal: diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index 011aa00f..cec20a60 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _WebServer: diff --git a/docs/source/simulation_components/system/session_and_software_manager.rst b/docs/source/simulation_components/system/session_and_software_manager.rst index f20af556..230f6687 100644 --- a/docs/source/simulation_components/system/session_and_software_manager.rst +++ b/docs/source/simulation_components/system/session_and_software_manager.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Session and Software Manager ============================ diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index d28815bb..c8f0e2d3 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK .. _software: diff --git a/docs/source/simulation_components/system/sys_log.rst b/docs/source/simulation_components/system/sys_log.rst index 05629993..cdf19faa 100644 --- a/docs/source/simulation_components/system/sys_log.rst +++ b/docs/source/simulation_components/system/sys_log.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK SysLog ====== diff --git a/docs/source/simulation_structure.rst b/docs/source/simulation_structure.rst index 7debe112..cd9ac409 100644 --- a/docs/source/simulation_structure.rst +++ b/docs/source/simulation_structure.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Simulation Structure diff --git a/docs/source/state_system.rst b/docs/source/state_system.rst index a5fd1df1..e31474ea 100644 --- a/docs/source/state_system.rst +++ b/docs/source/state_system.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Simulation State ================ diff --git a/docs/source/varying_config_files.rst b/docs/source/varying_config_files.rst index 942e522b..fa66f0d9 100644 --- a/docs/source/varying_config_files.rst +++ b/docs/source/varying_config_files.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK Defining variations in the config files ======================================= diff --git a/src/primaite/__init__.py b/src/primaite/__init__.py index 54eac69d..8dd84428 100644 --- a/src/primaite/__init__.py +++ b/src/primaite/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import datetime as datetime import logging import logging.config diff --git a/src/primaite/_legacy/actions.py b/src/primaite/_legacy/actions.py index d2457a20..0eda7d86 100644 --- a/src/primaite/_legacy/actions.py +++ b/src/primaite/_legacy/actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """ This module contains the ActionManager class which belongs to the Agent class. diff --git a/src/primaite/cli.py b/src/primaite/cli.py index 2bd18baf..4fbbdec9 100644 --- a/src/primaite/cli.py +++ b/src/primaite/cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Provides a CLI using Typer as an entry point.""" import logging import os diff --git a/src/primaite/config/__init__.py b/src/primaite/config/__init__.py index 7b5e2889..c2ae1b5b 100644 --- a/src/primaite/config/__init__.py +++ b/src/primaite/config/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Configuration parameters for running experiments.""" diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index 3553f527..39040d76 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Dict, Final, Union diff --git a/src/primaite/exceptions.py b/src/primaite/exceptions.py index 4487111d..afc55271 100644 --- a/src/primaite/exceptions.py +++ b/src/primaite/exceptions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK class PrimaiteError(Exception): """The root PrimAITE Error.""" diff --git a/src/primaite/game/__init__.py b/src/primaite/game/__init__.py index 57f96a56..39034e92 100644 --- a/src/primaite/game/__init__.py +++ b/src/primaite/game/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """PrimAITE Game Layer.""" diff --git a/src/primaite/game/agent/__init__.py b/src/primaite/game/agent/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/game/agent/__init__.py +++ b/src/primaite/game/agent/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 1100e125..016a09ba 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ( abstract, diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 15c9b4cb..8c332d5e 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import ABC diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 6fefeeda..d2846ddb 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 96609f93..91e34eae 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.abstract import AbstractAction diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/config.py index 760e8dfa..050e9b94 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import List, Optional, Union diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index e5ca1c46..b5e47c8a 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index d1fd5ef1..a27ca89b 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 7b290103..e2adf7d7 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 625d8cec..a6a4f5a6 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """yaml example. agents: @@ -80,6 +80,8 @@ class ActionManager: self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} # make sure all numbers between 0 and N are represented as dict keys in action map assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) + self.node_names: List[str] = [n["node_name"] for n in nodes] + """List of node names in this action space. The list order is the mapping between node index and node name.""" def get_action(self, action: int) -> Tuple[str, Dict]: """Produce action in CAOS format.""" diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index fa1c4451..346da9b7 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index c6b74f2e..480cb8da 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod from typing import ClassVar, List, Optional, Union diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index fa47ffb1..7ccffb0a 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 1191987b..a0805a49 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 59fb4702..62ef4884 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import logging from pathlib import Path diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 4acc9108..14b97821 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Interface for agents.""" from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING diff --git a/src/primaite/game/agent/observations/__init__.py b/src/primaite/game/agent/observations/__init__.py index a38095b3..c4811c98 100644 --- a/src/primaite/game/agent/observations/__init__.py +++ b/src/primaite/game/agent/observations/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # flake8: noqa # Pre-import all the observations when we load up the observations module so that they can be resolved by the parser. from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index 86a6463a..41af5a8f 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 50ca93fd..1c73d026 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, Iterable, List, Optional diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index a89ddfc5..42ceaff0 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 03e9aca1..617e8eee 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/link_observation.py b/src/primaite/game/agent/observations/link_observation.py index 851e9557..9af39a22 100644 --- a/src/primaite/game/agent/observations/link_observation.py +++ b/src/primaite/game/agent/observations/link_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Any, Dict, List diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index f87d2d76..d180b641 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 03869367..e11521b6 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 71a60433..9b20fdcb 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Any, Dict, List, Optional diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index 49b9ab72..a9663c56 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Manages the observation space for the agent.""" from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, Optional, Type, Union diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index ca455f4c..d064936a 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, List, Optional diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 37810c6e..15cd2447 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index fead27f2..f528c851 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """ Manages the reward function for the agent. diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/game/agent/scripted_agents/__init__.py +++ b/src/primaite/game/agent/scripted_agents/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 2432dd7b..eb0ce957 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import random from typing import Dict, Tuple diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index ce4d90d1..cd44644f 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Agents with predefined behaviours.""" from typing import Dict, Optional, Tuple diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 2c2ff091..eade3a0c 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import random from typing import Dict, Optional, Tuple diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py index 1ed200d7..6d370654 100644 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ b/src/primaite/game/agent/scripted_agents/tap001.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import random from typing import Dict, Tuple diff --git a/src/primaite/game/agent/utils.py b/src/primaite/game/agent/utils.py index 87b02858..15efd0b6 100644 --- a/src/primaite/game/agent/utils.py +++ b/src/primaite/game/agent/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any, Dict, Hashable, Optional, Sequence NOT_PRESENT_IN_STATE = object() diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 6555e272..c8fbac4e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © 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, Union diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py index 2cb5de7d..8d8949df 100644 --- a/src/primaite/game/science.py +++ b/src/primaite/game/science.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from random import random from typing import Any, Iterable, Mapping diff --git a/src/primaite/interface/__init__.py b/src/primaite/interface/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/interface/__init__.py +++ b/src/primaite/interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py index 03d6491e..1a9f0e5f 100644 --- a/src/primaite/interface/request.py +++ b/src/primaite/interface/request.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict, ForwardRef, List, Literal, Union from pydantic import BaseModel, ConfigDict, StrictBool # , validate_call diff --git a/src/primaite/session/__init__.py b/src/primaite/session/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/session/__init__.py +++ b/src/primaite/session/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 8e608ede..c66663e3 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json import random import sys diff --git a/src/primaite/session/episode_schedule.py b/src/primaite/session/episode_schedule.py index 126dcf9f..ad4d38e9 100644 --- a/src/primaite/session/episode_schedule.py +++ b/src/primaite/session/episode_schedule.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import copy from abc import ABC, abstractmethod from itertools import chain diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 6c2f4f29..78d7cb3c 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json from datetime import datetime from pathlib import Path diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 33ba0540..33c74b0e 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json from typing import Dict, SupportsFloat, Tuple diff --git a/src/primaite/setup/__init__.py b/src/primaite/setup/__init__.py index 1447a47b..12e7c4e7 100644 --- a/src/primaite/setup/__init__.py +++ b/src/primaite/setup/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Utilities to prepare the user's data folders.""" diff --git a/src/primaite/setup/reset_demo_notebooks.py b/src/primaite/setup/reset_demo_notebooks.py index ad4091e3..f17fb211 100644 --- a/src/primaite/setup/reset_demo_notebooks.py +++ b/src/primaite/setup/reset_demo_notebooks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import filecmp import shutil from logging import Logger diff --git a/src/primaite/setup/reset_example_configs.py b/src/primaite/setup/reset_example_configs.py index a94d6d4a..c7eeecd5 100644 --- a/src/primaite/setup/reset_example_configs.py +++ b/src/primaite/setup/reset_example_configs.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import filecmp import os import shutil diff --git a/src/primaite/simulator/__init__.py b/src/primaite/simulator/__init__.py index e85a2d1e..ade1a73b 100644 --- a/src/primaite/simulator/__init__.py +++ b/src/primaite/simulator/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Warning: SIM_OUTPUT is a mutable global variable for the simulation output directory.""" from datetime import datetime from enum import IntEnum diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 567a0493..848570fe 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # flake8: noqa """Core of the PrimAITE Simulator.""" import warnings diff --git a/src/primaite/simulator/domain/__init__.py b/src/primaite/simulator/domain/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/domain/__init__.py +++ b/src/primaite/simulator/domain/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index 85ec6d46..d955cf55 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """User account simulation.""" from enum import Enum from typing import Dict diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index d8b7782c..a264ba24 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from typing import Dict, Final, List, Literal, Tuple diff --git a/src/primaite/simulator/file_system/__init__.py b/src/primaite/simulator/file_system/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/file_system/__init__.py +++ b/src/primaite/simulator/file_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index 57d01ec9..ba39c791 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations import hashlib diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 8ff4b6fb..2162915f 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from pathlib import Path diff --git a/src/primaite/simulator/file_system/file_system_item_abc.py b/src/primaite/simulator/file_system/file_system_item_abc.py index 48b95d20..a9db8825 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations import math diff --git a/src/primaite/simulator/file_system/file_type.py b/src/primaite/simulator/file_system/file_type.py index 343d3565..e6e81070 100644 --- a/src/primaite/simulator/file_system/file_type.py +++ b/src/primaite/simulator/file_system/file_type.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from enum import Enum diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index ee0f3d01..c98e4492 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations import warnings diff --git a/src/primaite/simulator/network/__init__.py b/src/primaite/simulator/network/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/__init__.py +++ b/src/primaite/simulator/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 1f6fe6b0..2b8503d6 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index bf677d5c..1082e172 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Optional diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 94c45428..5d36f58b 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Literal, Type diff --git a/src/primaite/simulator/network/hardware/__init__.py b/src/primaite/simulator/network/hardware/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/hardware/__init__.py +++ b/src/primaite/simulator/network/hardware/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 8324715f..51e200e7 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations import re diff --git a/src/primaite/simulator/network/hardware/network_interface/__init__.py b/src/primaite/simulator/network/hardware/network_interface/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/hardware/network_interface/__init__.py +++ b/src/primaite/simulator/network/hardware/network_interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py index 3997872c..a9a31768 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_access_point.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict from primaite.simulator.network.hardware.base import ( diff --git a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py index 9bc4cd6f..eebaedc5 100644 --- a/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py +++ b/src/primaite/simulator/network/hardware/network_interface/wireless/wireless_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict from primaite.simulator.network.hardware.base import ( diff --git a/src/primaite/simulator/network/hardware/node_operating_state.py b/src/primaite/simulator/network/hardware/node_operating_state.py index 8771cb84..e64ef08b 100644 --- a/src/primaite/simulator/network/hardware/node_operating_state.py +++ b/src/primaite/simulator/network/hardware/node_operating_state.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum diff --git a/src/primaite/simulator/network/hardware/nodes/__init__.py b/src/primaite/simulator/network/hardware/nodes/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/hardware/nodes/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/host/__init__.py b/src/primaite/simulator/network/hardware/nodes/host/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/host/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 11b925b9..4253d15c 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index c51afbca..0c309136 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index e16cfd8f..bf1ef39b 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/src/primaite/simulator/network/hardware/nodes/network/__init__.py b/src/primaite/simulator/network/hardware/nodes/network/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/__init__.py +++ b/src/primaite/simulator/network/hardware/nodes/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index f1ca4930..84cf8530 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Final, Union diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 22ff2b28..a5b8544f 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod from typing import Optional diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 4a049f99..e921faff 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations import secrets diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index db923f1a..d29152a4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from typing import Dict, Optional diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 804a570e..aed314d2 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, Optional, Union diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index c840748e..2c3c15b4 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import yaml diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index a2e5f1fe..c9cff5de 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import List from pydantic import BaseModel, ConfigDict diff --git a/src/primaite/simulator/network/protocols/__init__.py b/src/primaite/simulator/network/protocols/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/protocols/__init__.py +++ b/src/primaite/simulator/network/protocols/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/protocols/arp.py b/src/primaite/simulator/network/protocols/arp.py index 86e461d0..9e7f7ebe 100644 --- a/src/primaite/simulator/network/protocols/arp.py +++ b/src/primaite/simulator/network/protocols/arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/protocols/dns.py b/src/primaite/simulator/network/protocols/dns.py index c0fed1aa..eb7b74ad 100644 --- a/src/primaite/simulator/network/protocols/dns.py +++ b/src/primaite/simulator/network/protocols/dns.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py index fd8fdd2b..c570a634 100644 --- a/src/primaite/simulator/network/protocols/ftp.py +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from typing import Any, Optional, Union diff --git a/src/primaite/simulator/network/protocols/http.py b/src/primaite/simulator/network/protocols/http.py index 54abdd98..5390cd26 100644 --- a/src/primaite/simulator/network/protocols/http.py +++ b/src/primaite/simulator/network/protocols/http.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum, IntEnum from primaite.simulator.network.protocols.packet import DataPacket diff --git a/src/primaite/simulator/network/protocols/icmp.py b/src/primaite/simulator/network/protocols/icmp.py index fcbe15da..9f0626f0 100644 --- a/src/primaite/simulator/network/protocols/icmp.py +++ b/src/primaite/simulator/network/protocols/icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import secrets from enum import Enum from typing import Union diff --git a/src/primaite/simulator/network/protocols/masquerade.py b/src/primaite/simulator/network/protocols/masquerade.py index e0ed26b7..5c5f03b2 100644 --- a/src/primaite/simulator/network/protocols/masquerade.py +++ b/src/primaite/simulator/network/protocols/masquerade.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from typing import Optional diff --git a/src/primaite/simulator/network/protocols/ntp.py b/src/primaite/simulator/network/protocols/ntp.py index c9b6f877..74e02dab 100644 --- a/src/primaite/simulator/network/protocols/ntp.py +++ b/src/primaite/simulator/network/protocols/ntp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from datetime import datetime diff --git a/src/primaite/simulator/network/protocols/packet.py b/src/primaite/simulator/network/protocols/packet.py index 6f28f716..7eeec13b 100644 --- a/src/primaite/simulator/network/protocols/packet.py +++ b/src/primaite/simulator/network/protocols/packet.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any from pydantic import BaseModel diff --git a/src/primaite/simulator/network/protocols/ssh.py b/src/primaite/simulator/network/protocols/ssh.py index 03411fb5..be7f842f 100644 --- a/src/primaite/simulator/network/protocols/ssh.py +++ b/src/primaite/simulator/network/protocols/ssh.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import IntEnum from typing import Optional diff --git a/src/primaite/simulator/network/transmission/__init__.py b/src/primaite/simulator/network/transmission/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/network/transmission/__init__.py +++ b/src/primaite/simulator/network/transmission/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index e7c2a124..259d62e3 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from datetime import datetime from typing import Any, Optional diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index 7a6b34c9..49dcd1f5 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from pydantic import BaseModel diff --git a/src/primaite/simulator/network/transmission/primaite_layer.py b/src/primaite/simulator/network/transmission/primaite_layer.py index 8ff4ac02..981b6fbc 100644 --- a/src/primaite/simulator/network/transmission/primaite_layer.py +++ b/src/primaite/simulator/network/transmission/primaite_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from pydantic import BaseModel diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index 689eea2f..10cf802c 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from typing import List diff --git a/src/primaite/simulator/network/utils.py b/src/primaite/simulator/network/utils.py index b4d6c815..4fd1834a 100644 --- a/src/primaite/simulator/network/utils.py +++ b/src/primaite/simulator/network/utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Union diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 2a1deef4..809b52db 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict from primaite.interface.request import RequestResponse diff --git a/src/primaite/simulator/system/__init__.py b/src/primaite/simulator/system/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/__init__.py +++ b/src/primaite/simulator/system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/__init__.py b/src/primaite/simulator/system/applications/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/applications/__init__.py +++ b/src/primaite/simulator/system/applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 1752c09a..a7871315 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 840214f3..cd4b2a03 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index f064eae3..e2b9117d 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, Final, List, Optional, Set, Tuple, Union diff --git a/src/primaite/simulator/system/applications/red_applications/__init__.py b/src/primaite/simulator/system/applications/red_applications/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/applications/red_applications/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py index 33cc555f..60e39743 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/__init__.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Optional, Union from pydantic import BaseModel, Field, field_validator, ValidationInfo diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 4cd54d69..f77bc33a 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod from enum import Enum from ipaddress import IPv4Address diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index b25eea6e..c0c3d872 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 654b86e7..f948d696 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 0423087e..9fdbae57 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index 99c4acb3..fb2c8847 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import IntEnum from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 3a8ac5ae..93b4c50d 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ff185e2a..c57a9bd3 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address from typing import Dict, List, Optional diff --git a/src/primaite/simulator/system/core/__init__.py b/src/primaite/simulator/system/core/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/core/__init__.py +++ b/src/primaite/simulator/system/core/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index 813c288e..ea8b00a5 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json import logging from pathlib import Path diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 48f1f383..75322e86 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address, IPv4Network diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 5e63f2ec..2f19a8b0 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from copy import deepcopy from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union diff --git a/src/primaite/simulator/system/core/sys_log.py b/src/primaite/simulator/system/core/sys_log.py index 741e5d33..9e22696d 100644 --- a/src/primaite/simulator/system/core/sys_log.py +++ b/src/primaite/simulator/system/core/sys_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import logging from pathlib import Path diff --git a/src/primaite/simulator/system/processes/__init__.py b/src/primaite/simulator/system/processes/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/processes/__init__.py +++ b/src/primaite/simulator/system/processes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index ad2babc1..225505c8 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import abstractmethod from enum import Enum from typing import Dict diff --git a/src/primaite/simulator/system/services/__init__.py b/src/primaite/simulator/system/services/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/__init__.py +++ b/src/primaite/simulator/system/services/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/__init__.py b/src/primaite/simulator/system/services/access/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/access/__init__.py +++ b/src/primaite/simulator/system/services/access/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_manager.py b/src/primaite/simulator/system/services/access/user_manager.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/access/user_manager.py +++ b/src/primaite/simulator/system/services/access/user_manager.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/access/user_session_manager.py b/src/primaite/simulator/system/services/access/user_session_manager.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/access/user_session_manager.py +++ b/src/primaite/simulator/system/services/access/user_session_manager.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/arp/__init__.py b/src/primaite/simulator/system/services/arp/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/arp/__init__.py +++ b/src/primaite/simulator/system/services/arp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 31938e83..816eb99e 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/database/__init__.py b/src/primaite/simulator/system/services/database/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/database/__init__.py +++ b/src/primaite/simulator/system/services/database/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 3a5f5b31..b7cd8886 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 diff --git a/src/primaite/simulator/system/services/dns/__init__.py b/src/primaite/simulator/system/services/dns/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/dns/__init__.py +++ b/src/primaite/simulator/system/services/dns/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 02cf54ae..78642fa6 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index b7c9a42c..5b380320 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, Optional diff --git a/src/primaite/simulator/system/services/ftp/__init__.py b/src/primaite/simulator/system/services/ftp/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/ftp/__init__.py +++ b/src/primaite/simulator/system/services/ftp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 9c7b91ce..00b70332 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 9ce7d658..671200f5 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any, Optional from primaite import getLogger diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 52f451e1..77d82997 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from abc import ABC from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/icmp/__init__.py b/src/primaite/simulator/system/services/icmp/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/icmp/__init__.py +++ b/src/primaite/simulator/system/services/icmp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 933d0591..84ad995d 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import secrets from ipaddress import IPv4Address from typing import Any, Dict, Optional, Tuple, Union diff --git a/src/primaite/simulator/system/services/icmp/router_icmp.py b/src/primaite/simulator/system/services/icmp/router_icmp.py index 63fbd4b2..19c0ac2d 100644 --- a/src/primaite/simulator/system/services/icmp/router_icmp.py +++ b/src/primaite/simulator/system/services/icmp/router_icmp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # class RouterICMP(ICMP): # """ # A class to represent a router's Internet Control Message Protocol (ICMP) handler. diff --git a/src/primaite/simulator/system/services/ntp/__init__.py b/src/primaite/simulator/system/services/ntp/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/ntp/__init__.py +++ b/src/primaite/simulator/system/services/ntp/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 9606c61f..ed89971f 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from datetime import datetime from ipaddress import IPv4Address from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index 6e73ccc6..b674a296 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from datetime import datetime from typing import Dict, Optional diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 3dc080b4..4f0b879c 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/terminal/__init__.py b/src/primaite/simulator/system/services/terminal/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/terminal/__init__.py +++ b/src/primaite/simulator/system/services/terminal/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index e26e77f6..ae3557f7 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import abstractmethod diff --git a/src/primaite/simulator/system/services/web_server/__init__.py b/src/primaite/simulator/system/services/web_server/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/simulator/system/services/web_server/__init__.py +++ b/src/primaite/simulator/system/services/web_server/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 1aab374d..75d9c472 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Optional from urllib.parse import urlparse diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 34c893eb..6fb09a16 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import copy from abc import abstractmethod from datetime import datetime diff --git a/src/primaite/utils/__init__.py b/src/primaite/utils/__init__.py index 1dced372..4d7c430e 100644 --- a/src/primaite/utils/__init__.py +++ b/src/primaite/utils/__init__.py @@ -1,2 +1,2 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Utilities for PrimAITE.""" diff --git a/src/primaite/utils/cli/__init__.py b/src/primaite/utils/cli/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/utils/cli/__init__.py +++ b/src/primaite/utils/cli/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/cli/dev_cli.py b/src/primaite/utils/cli/dev_cli.py index 581cd0b1..8946a4ca 100644 --- a/src/primaite/utils/cli/dev_cli.py +++ b/src/primaite/utils/cli/dev_cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import click import typer from rich import print diff --git a/src/primaite/utils/cli/primaite_config_utils.py b/src/primaite/utils/cli/primaite_config_utils.py index 1fefd0a4..635be5a7 100644 --- a/src/primaite/utils/cli/primaite_config_utils.py +++ b/src/primaite/utils/cli/primaite_config_utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict, Optional import yaml diff --git a/src/primaite/utils/converters.py b/src/primaite/utils/converters.py index 95956448..f803851d 100644 --- a/src/primaite/utils/converters.py +++ b/src/primaite/utils/converters.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from typing import Any, Dict diff --git a/src/primaite/utils/package_data.py b/src/primaite/utils/package_data.py index ed091dd0..af0252f9 100644 --- a/src/primaite/utils/package_data.py +++ b/src/primaite/utils/package_data.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import os from logging import Logger from pathlib import Path diff --git a/src/primaite/utils/session_metadata_parser.py b/src/primaite/utils/session_metadata_parser.py index 1a7345ea..f6594666 100644 --- a/src/primaite/utils/session_metadata_parser.py +++ b/src/primaite/utils/session_metadata_parser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/session_output_reader.py b/src/primaite/utils/session_output_reader.py index f25bbe6a..b9ad68a1 100644 --- a/src/primaite/utils/session_output_reader.py +++ b/src/primaite/utils/session_output_reader.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/session_output_writer.py b/src/primaite/utils/session_output_writer.py index a8cefe35..75a97f60 100644 --- a/src/primaite/utils/session_output_writer.py +++ b/src/primaite/utils/session_output_writer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # flake8: noqa raise DeprecationWarning( "Benchmarking depends on deprecated functionality and it has not been updated to primaite v3 yet." diff --git a/src/primaite/utils/validation/__init__.py b/src/primaite/utils/validation/__init__.py index 836b79af..be6c00e7 100644 --- a/src/primaite/utils/validation/__init__.py +++ b/src/primaite/utils/validation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/src/primaite/utils/validation/ip_protocol.py b/src/primaite/utils/validation/ip_protocol.py index 654a5156..4e358305 100644 --- a/src/primaite/utils/validation/ip_protocol.py +++ b/src/primaite/utils/validation/ip_protocol.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # Define a custom IP protocol validator from typing import Any diff --git a/src/primaite/utils/validation/ipv4_address.py b/src/primaite/utils/validation/ipv4_address.py index c385ed1e..eb0e2574 100644 --- a/src/primaite/utils/validation/ipv4_address.py +++ b/src/primaite/utils/validation/ipv4_address.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address diff --git a/src/primaite/utils/validation/port.py b/src/primaite/utils/validation/port.py index 564e843c..90c36add 100644 --- a/src/primaite/utils/validation/port.py +++ b/src/primaite/utils/validation/port.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # Define a custom port validator from typing import Any diff --git a/tests/__init__.py b/tests/__init__.py index 900649b2..846ec808 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Final diff --git a/tests/conftest.py b/tests/conftest.py index 0d73aa07..bd1b79ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any, Dict, Tuple import pytest @@ -471,6 +471,33 @@ def game_and_agent(): action_space = ActionManager( actions=actions, # ALL POSSIBLE ACTIONS + nodes=[ + { + "node_name": "client_1", + "applications": [ + {"application_name": "WebBrowser"}, + {"application_name": "DoSBot"}, + {"application_name": "C2Server"}, + ], + "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], + }, + { + "node_name": "server_1", + "services": [{"service_name": "DNSServer"}], + "applications": [{"application_name": "C2Beacon"}], + }, + {"node_name": "server_2", "services": [{"service_name": "WebServer"}]}, + {"node_name": "router"}, + ], + max_folders_per_node=2, + max_files_per_folder=2, + max_services_per_node=2, + max_applications_per_node=3, + max_nics_per_node=2, + max_acl_rules=10, + protocols=["TCP", "UDP", "ICMP"], + ports=["HTTP", "DNS", "ARP"], + ip_list=["10.0.1.1", "10.0.1.2", "10.0.2.1", "10.0.2.2", "10.0.2.3"], act_map={}, ) observation_space = ObservationManager(NestedObservation(components={})) diff --git a/tests/e2e_integration_tests/__init__.py b/tests/e2e_integration_tests/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/e2e_integration_tests/__init__.py +++ b/tests/e2e_integration_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/__init__.py b/tests/e2e_integration_tests/action_masking/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/e2e_integration_tests/action_masking/__init__.py +++ b/tests/e2e_integration_tests/action_masking/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py index a34d430b..addf6dca 100644 --- a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py +++ b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict import pytest diff --git a/tests/e2e_integration_tests/environments/__init__.py b/tests/e2e_integration_tests/environments/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/e2e_integration_tests/environments/__init__.py +++ b/tests/e2e_integration_tests/environments/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py index 06b080d8..26e690d0 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import yaml from ray.rllib.algorithms.ppo import PPOConfig diff --git a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py index da0ca458..265257e4 100644 --- a/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py +++ b/tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import tempfile from pathlib import Path diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index 9ca3525a..a07d5d2e 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Test that we can create a primaite environment and train sb3 agent with no crash.""" import tempfile from pathlib import Path diff --git a/tests/e2e_integration_tests/test_environment.py b/tests/e2e_integration_tests/test_environment.py index 881681aa..dcd51193 100644 --- a/tests/e2e_integration_tests/test_environment.py +++ b/tests/e2e_integration_tests/test_environment.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pydantic import pytest import yaml diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index fa4781db..7ec38d72 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/__init__.py +++ b/tests/integration_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/cli/__init__.py b/tests/integration_tests/cli/__init__.py index 603d228f..cfce7ae6 100644 --- a/tests/integration_tests/cli/__init__.py +++ b/tests/integration_tests/cli/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import List from typer.testing import CliRunner, Result diff --git a/tests/integration_tests/cli/test_dev_cli.py b/tests/integration_tests/cli/test_dev_cli.py index 16c3de9f..cd390555 100644 --- a/tests/integration_tests/cli/test_dev_cli.py +++ b/tests/integration_tests/cli/test_dev_cli.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import os import shutil import tempfile diff --git a/tests/integration_tests/component_creation/__init__.py b/tests/integration_tests/component_creation/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/component_creation/__init__.py +++ b/tests/integration_tests/component_creation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index 8b81b7d3..7bdc80fc 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.core import RequestType from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server diff --git a/tests/integration_tests/component_creation/test_permission_system.py b/tests/integration_tests/component_creation/test_permission_system.py index c7faa81b..baf75523 100644 --- a/tests/integration_tests/component_creation/test_permission_system.py +++ b/tests/integration_tests/component_creation/test_permission_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from typing import Dict, List, Literal diff --git a/tests/integration_tests/configuration_file_parsing/__init__.py b/tests/integration_tests/configuration_file_parsing/__init__.py index 09861acb..7e23a4c2 100644 --- a/tests/integration_tests/configuration_file_parsing/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/__init__.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py index 234e7342..7f251613 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index 16f4dee5..d10c7dbb 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py index 764a7aac..8526ab78 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.config.load import data_manipulation_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index 0ff6754d..a642564c 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py index c588829b..13be830b 100644 --- a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py +++ b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py index 4153adc0..32d88c92 100644 --- a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_io_settings.py b/tests/integration_tests/configuration_file_parsing/test_io_settings.py index 79812d80..82977b82 100644 --- a/tests/integration_tests/configuration_file_parsing/test_io_settings.py +++ b/tests/integration_tests/configuration_file_parsing/test_io_settings.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py index 016d264f..26fc562d 100644 --- a/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py index b1c644cc..168ebee0 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import copy from pathlib import Path from typing import Union diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 9863dbba..70dc7cba 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address from typing import Dict, List, Optional diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index 37a05b6e..e4100741 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict from prettytable import MARKDOWN, PrettyTable diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 4af1b748..80f7e3c3 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index 0924a91b..ddaf4a1e 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union from uuid import uuid4 diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 5515d900..5addcbd7 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import os from primaite.config.load import get_extended_config_path diff --git a/tests/integration_tests/game_layer/actions/__init__.py b/tests/integration_tests/game_layer/actions/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/game_layer/actions/__init__.py +++ b/tests/integration_tests/game_layer/actions/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index e90fa591..36a7ae57 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 36fee9a0..d73c9834 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 8c97573a..7bf45fb4 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 91aa9fcd..1c143aed 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import uuid from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index 56bbbd4e..e5e0806a 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import uuid from typing import Tuple diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index 8846809d..d796b75e 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index 8fbbbd70..c34103bc 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index ebc9fd3b..3054c73b 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index 96110656..a70cea72 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/game_layer/observations/__init__.py b/tests/integration_tests/game_layer/observations/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/game_layer/observations/__init__.py +++ b/tests/integration_tests/game_layer/observations/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index 02cf005a..e7212f3c 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index 0268cb95..e2ab2990 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 97608132..05cf910c 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.firewall_observation import FirewallObservation from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py index 630e29ea..7d1c1939 100644 --- a/tests/integration_tests/game_layer/observations/test_link_observations.py +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 0ad03198..8254dad2 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pathlib import Path from typing import Union diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py index 63ca8f6b..69d9f106 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import copy from uuid import uuid4 diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index f4bfb193..4ced02f5 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pprint import pprint from primaite.game.agent.observations.acl_observation import ACLObservation diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py index 291ee395..998aa755 100644 --- a/tests/integration_tests/game_layer/observations/test_software_observations.py +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from gymnasium import spaces diff --git a/tests/integration_tests/game_layer/observations/test_user_observations.py b/tests/integration_tests/game_layer/observations/test_user_observations.py index 92c533c9..e7287eee 100644 --- a/tests/integration_tests/game_layer/observations/test_user_observations.py +++ b/tests/integration_tests/game_layer/observations/test_user_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index 464f95db..e772af32 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from pprint import pprint import pytest diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 22c00aa4..7a1475c2 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index ff86dbf0..859c056c 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # Plan for creating integration tests for the actions: # I need to test that the requests coming out of the actions have the intended effect on the simulation. # I can do this by creating a simulation, and then running the action on the simulation, and then checking diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index 23364f13..d5679007 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from gymnasium import spaces from primaite.game.agent.observations.file_system_observations import FileObservation diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index a2453782..882c0923 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/network/__init__.py b/tests/integration_tests/network/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/network/__init__.py +++ b/tests/integration_tests/network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py index e8abc0f2..e000f6ae 100644 --- a/tests/integration_tests/network/test_airspace_config.py +++ b/tests/integration_tests/network/test_airspace_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py index 36c77fe1..b7317c3d 100644 --- a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.file_system.file_type import FileType from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.system.services.ftp.ftp_client import FTPClient diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index 33fe70c3..f07f02e7 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, List, Tuple diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index b32d9657..debf5b1c 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.observations.nic_observations import NICObservation from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 24fbfd05..79452318 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index 327c87e5..fc2d146e 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_multi_lan_internet_example_network.py b/tests/integration_tests/network/test_multi_lan_internet_example_network.py index ea7e1c45..bcc9ad94 100644 --- a/tests/integration_tests/network/test_multi_lan_internet_example_network.py +++ b/tests/integration_tests/network/test_multi_lan_internet_example_network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.networks import multi_lan_internet_network_example diff --git a/tests/integration_tests/network/test_network_creation.py b/tests/integration_tests/network/test_network_creation.py index 1ee3ccc2..794ddde5 100644 --- a/tests/integration_tests/network/test_network_creation.py +++ b/tests/integration_tests/network/test_network_creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC diff --git a/tests/integration_tests/network/test_nic_link_connection.py b/tests/integration_tests/network/test_nic_link_connection.py index 8c45f511..ab9160c8 100644 --- a/tests/integration_tests/network/test_nic_link_connection.py +++ b/tests/integration_tests/network/test_nic_link_connection.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Link diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 948b409f..04cdbe78 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/network/test_switched_network.py b/tests/integration_tests/network/test_switched_network.py index 67392da3..ae0aa8a7 100644 --- a/tests/integration_tests/network/test_switched_network.py +++ b/tests/integration_tests/network/test_switched_network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK def test_switched_network(client_switch_server): """Tests a node can ping another node via the switch.""" computer, switch, server = client_switch_server diff --git a/tests/integration_tests/network/test_users_creation_from_config.py b/tests/integration_tests/network/test_users_creation_from_config.py index 1963b1dd..8cd3b037 100644 --- a/tests/integration_tests/network/test_users_creation_from_config.py +++ b/tests/integration_tests/network/test_users_creation_from_config.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import yaml from primaite.game.game import PrimaiteGame diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index 26e50f4a..fb0035e9 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/integration_tests/system/__init__.py b/tests/integration_tests/system/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/system/__init__.py +++ b/tests/integration_tests/system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index d88f8249..2cbd4d11 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 3ef6469e..50b0ceac 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index cb0195f0..1a09e875 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index 14b83e6a..a5adbb04 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index fc7aa69c..ffb5cc7f 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_arp.py b/tests/integration_tests/system/test_arp.py index 055d58c6..be8656aa 100644 --- a/tests/integration_tests/system/test_arp.py +++ b/tests/integration_tests/system/test_arp.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.network.router import RouterARP from primaite.simulator.system.services.arp.arp import ARP from tests.integration_tests.network.test_routing import multi_hop_network diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 674603fa..965b4ae8 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 38caf1a2..480a90bc 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index fa4df0a9..22c5d484 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index d1925a94..c52b5caa 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from enum import Enum from ipaddress import IPv4Address, IPv4Network diff --git a/tests/integration_tests/system/test_ntp_client_server.py b/tests/integration_tests/system/test_ntp_client_server.py index 42340eb3..957c1aeb 100644 --- a/tests/integration_tests/system/test_ntp_client_server.py +++ b/tests/integration_tests/system/test_ntp_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from time import sleep from typing import Tuple 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 2d3679ed..7a085ee1 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Any, Dict, List, Set import yaml diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 4e73a050..cf9728ce 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_user_session_manager_logins.py b/tests/integration_tests/system/test_user_session_manager_logins.py index 0c591a4b..4318530c 100644 --- a/tests/integration_tests/system/test_user_session_manager_logins.py +++ b/tests/integration_tests/system/test_user_session_manager_logins.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple from uuid import uuid4 diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index c1028e8e..05cbae4f 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 8fb6dc18..f2ac1183 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple diff --git a/tests/integration_tests/test_simulation/__init__.py b/tests/integration_tests/test_simulation/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/integration_tests/test_simulation/__init__.py +++ b/tests/integration_tests/test_simulation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index 21152199..a767f365 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK # some test cases: # 0. test that sending a request to a valid target results in a success # 1. test that sending a request to a component that doesn't exist results in a failure diff --git a/tests/mock_and_patch/__init__.py b/tests/mock_and_patch/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/mock_and_patch/__init__.py +++ b/tests/mock_and_patch/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/mock_and_patch/get_session_path_mock.py b/tests/mock_and_patch/get_session_path_mock.py index 073028a7..f315fca4 100644 --- a/tests/mock_and_patch/get_session_path_mock.py +++ b/tests/mock_and_patch/get_session_path_mock.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import tempfile from datetime import datetime from pathlib import Path diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/__init__.py +++ b/tests/unit_tests/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/__init__.py b/tests/unit_tests/_primaite/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/__init__.py +++ b/tests/unit_tests/_primaite/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/__init__.py b/tests/unit_tests/_primaite/_game/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_game/__init__.py +++ b/tests/unit_tests/_primaite/_game/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/_agent/__init__.py b/tests/unit_tests/_primaite/_game/_agent/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_game/_agent/__init__.py +++ b/tests/unit_tests/_primaite/_game/_agent/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index 9021b8af..46963015 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from unittest.mock import Mock import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py index a7713437..d61e1a23 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index bb3ad33c..7f590685 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import List import pytest diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 8c06aeed..2fd2da0c 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 67c4290d..78113f5f 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import ( diff --git a/tests/unit_tests/_primaite/_interface/__init__.py b/tests/unit_tests/_primaite/_interface/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_interface/__init__.py +++ b/tests/unit_tests/_primaite/_interface/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_interface/test_request.py b/tests/unit_tests/_primaite/_interface/test_request.py index d9fae083..6067f9e4 100644 --- a/tests/unit_tests/_primaite/_interface/test_request.py +++ b/tests/unit_tests/_primaite/_interface/test_request.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from pydantic import ValidationError diff --git a/tests/unit_tests/_primaite/_session/__init__.py b/tests/unit_tests/_primaite/_session/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_session/__init__.py +++ b/tests/unit_tests/_primaite/_session/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_session/test_episode_schedule.py b/tests/unit_tests/_primaite/_session/test_episode_schedule.py index ff26bb02..21448339 100644 --- a/tests/unit_tests/_primaite/_session/test_episode_schedule.py +++ b/tests/unit_tests/_primaite/_session/test_episode_schedule.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest import yaml diff --git a/tests/unit_tests/_primaite/_simulator/__init__.py b/tests/unit_tests/_primaite/_simulator/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_domain/__init__.py b/tests/unit_tests/_primaite/_simulator/_domain/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py index f5294844..8db68565 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK """Test the account module of the simulator.""" import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py b/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_controller.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py b/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py index 6cbf93c8..0b9bdc8e 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import warnings import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 4ec1ec57..594c7afe 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py index 5554b9ef..4eb0dd10 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py index 44a4e22a..7d022ea4 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_system_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py index 473e0db2..724d7903 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 609e29c4..4a561b97 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import warnings from typing import Tuple diff --git a/tests/unit_tests/_primaite/_simulator/_network/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 79392d66..6eca0c44 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index fe0c3a57..fe9387de 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index e6bff60e..2613d536 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py index 5cff4407..f35cf171 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import NetworkInterface, Node diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py index f9ff0328..29d5ec67 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import re from ipaddress import IPv4Address diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 605f8c3b..44c5c781 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file import File diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py index 161d9cb4..e7e425b1 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_data_link_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket diff --git a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py index 990a0bbf..658726b5 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_transmission/test_network_layer.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.protocols.icmp import ICMPPacket, ICMPType diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index b1de710a..f764f9b5 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import json import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py index 9885df67..2e86ebbc 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py index d86aa876..c80189c1 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.network.utils import convert_bytes_to_megabits, convert_megabits_to_bytes diff --git a/tests/unit_tests/_primaite/_simulator/_system/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 4ff387ce..12dddf67 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.container import Network diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index 6e9ee224..34a29cd0 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index 9d8b7809..e9762476 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py index a69dc844..0e9c536c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.application import Application, ApplicationOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py index 16a4c9ad..f97e915e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.system.applications.application import Application diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py index dd29f18e..aef5d6d1 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index 5917fde7..e456ed78 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address from typing import Tuple from uuid import uuid4 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index f78b3261..f1be475a 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py b/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index ef165c8f..9e7ab1d2 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.base import Node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 1bc5b353..db7e8d58 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 3bc2b1a4..c64602c0 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index d3e679db..95788834 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 37c3d019..291cdede 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py index 60cd2422..537beb8b 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_service_actions.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py index ad6fe135..8c12adaa 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 08bef92d..9b6a4bf3 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Tuple from uuid import uuid4 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 606a195c..54f86ec8 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState diff --git a/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py b/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py index 5a734b6e..053211cd 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py +++ b/tests/unit_tests/_primaite/_simulator/_system/core/test_sys_log.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from uuid import uuid4 import pytest diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index a203a636..300f8d9d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Dict import pytest diff --git a/tests/unit_tests/_primaite/_simulator/test_core.py b/tests/unit_tests/_primaite/_simulator/test_core.py index 271375eb..02960978 100644 --- a/tests/unit_tests/_primaite/_simulator/test_core.py +++ b/tests/unit_tests/_primaite/_simulator/test_core.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from typing import Callable, Dict, List, Literal, Tuple import pytest diff --git a/tests/unit_tests/_primaite/_simulator/test_sim_container.py b/tests/unit_tests/_primaite/_simulator/test_sim_container.py index f482d7e6..fe702307 100644 --- a/tests/unit_tests/_primaite/_simulator/test_sim_container.py +++ b/tests/unit_tests/_primaite/_simulator/test_sim_container.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.simulator.sim_container import Simulation diff --git a/tests/unit_tests/_primaite/_utils/__init__.py b/tests/unit_tests/_primaite/_utils/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_utils/__init__.py +++ b/tests/unit_tests/_primaite/_utils/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/__init__.py b/tests/unit_tests/_primaite/_utils/_validation/__init__.py index 836b79af..be6c00e7 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/__init__.py +++ b/tests/unit_tests/_primaite/_utils/_validation/__init__.py @@ -1 +1 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py index 7acbe4a7..27829570 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py +++ b/tests/unit_tests/_primaite/_utils/_validation/test_ip_protocol.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.utils.validation.ip_protocol import IPProtocol, is_valid_protocol, PROTOCOL_LOOKUP, protocol_validator diff --git a/tests/unit_tests/_primaite/_utils/_validation/test_port.py b/tests/unit_tests/_primaite/_utils/_validation/test_port.py index 2e30ab76..6a8a2429 100644 --- a/tests/unit_tests/_primaite/_utils/_validation/test_port.py +++ b/tests/unit_tests/_primaite/_utils/_validation/test_port.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK import pytest from primaite.utils.validation.port import is_valid_port, Port, PORT_LOOKUP, port_validator diff --git a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py index d0a64ece..1a1848ac 100644 --- a/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py +++ b/tests/unit_tests/_primaite/_utils/test_dict_enum_keys_conversion.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK from primaite.utils.converters import convert_dict_enum_keys_to_enum_values from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP From 0ee454b13ef69f3ba58064d1edd2573720c51a2a Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 3 Jan 2025 14:53:54 +0000 Subject: [PATCH 106/224] #2912 - Rename actions/config.py to actions/software.py --- .../how_to_guides/extensible_actions.rst | 69 +------------------ src/primaite/game/agent/actions/__init__.py | 6 +- src/primaite/game/agent/actions/abstract.py | 2 +- src/primaite/game/agent/actions/acl.py | 2 +- .../game/agent/actions/application.py | 2 +- src/primaite/game/agent/actions/file.py | 2 +- src/primaite/game/agent/actions/folder.py | 2 +- src/primaite/game/agent/actions/host_nic.py | 2 +- src/primaite/game/agent/actions/manager.py | 2 +- src/primaite/game/agent/actions/network.py | 2 +- src/primaite/game/agent/actions/node.py | 2 +- src/primaite/game/agent/actions/service.py | 2 +- src/primaite/game/agent/actions/session.py | 2 +- .../agent/actions/{config.py => software.py} | 2 +- src/primaite/game/agent/rewards.py | 3 +- .../actions/test_configure_actions.py | 2 +- 16 files changed, 19 insertions(+), 85 deletions(-) rename src/primaite/game/agent/actions/{config.py => software.py} (99%) diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 6e44a905..f2e053aa 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -1,70 +1,3 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK - -.. _about: - -Extensible Actions -****************** - -Actions defined within PrimAITE have been updated to allow for easier creation of new bespoke actions, without the need to make changes to the ActionManager class within the core PrimAITE repository. - - -Developing Actions for PrimAITE -=============================== - -When developing new actions for PrimAITE, it's important to ensure new actions inherit from the AbstractAction class. This is so that the `ActionManager` has visibility -of the new action through the `AbstractAction` registry attribute. This also removes the need for actions to contain an `__init__` method. - -New actions to be used within PrimAITE require: - -#. **ConfigSchema**: - - This should be a nested class that defines the required configuration items for the new action. - - .. code-block:: python - - class ExampleAction(AbstractAction, identifier="Example_action"): - """An example action for demonstration purposes.""" - - config: "ExampleAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): - """The configuration schema with all attributes expected goes here.""" - target_application: str - - The ConfigSchema is used when the class is called to form the action, within the `form_request` method, detailed below. - - -#. **Unique Identifier**: - - New actions should have a Unique identifier when declared. This is used by the `ActionManager` when forming/processing action commands from agents. See the example code block in ConfigSchema for how this should be implemented. - -#. **form_request method**: - - New actions need a `form_request()` method, to convert the action into a ``Requestformat`` that can be ingested by PrimAITE's `RequestManager`. - The below is an example of how this is done, taken from the `NodeFolderCreateAction`. - - .. code-block:: python - - @classmethod - def form_request(cls, config: ConfigSchema) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.node_name is None or config.folder_name is None: - return ["do_nothing"] - return [ - "network", - "node", - config.node_name, - "file_system", - config.verb, - "folder", - config.folder_name, - ] - -There is no longer a need for a `from_config()` method to be defined within new actions, as this is handled within the base `AbstractAction` class. - -Changes to YAML file. -===================== - -Action identifiers now follow the snake_case naming style, instead of the MACRO_CASE that has been seen previously. Please review any custom YAML files for any issues seen. This should be backwards compatible. + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK diff --git a/src/primaite/game/agent/actions/__init__.py b/src/primaite/game/agent/actions/__init__.py index 016a09ba..8517ded8 100644 --- a/src/primaite/game/agent/actions/__init__.py +++ b/src/primaite/game/agent/actions/__init__.py @@ -1,10 +1,9 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent.actions import ( abstract, acl, application, - config, file, folder, host_nic, @@ -13,6 +12,7 @@ from primaite.game.agent.actions import ( node, service, session, + software, ) from primaite.game.agent.actions.manager import ActionManager @@ -20,7 +20,7 @@ __all__ = ( "abstract", "acl", "application", - "config", + "software", "file", "folder", "host_nic", diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 8c332d5e..15c9b4cb 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from abc import ABC diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index d2846ddb..6fefeeda 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations from ipaddress import IPv4Address diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 91e34eae..96609f93 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.abstract import AbstractAction diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index b5e47c8a..e5ca1c46 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index a27ca89b..d1fd5ef1 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index e2adf7d7..7b290103 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index a6a4f5a6..b612d9ce 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """yaml example. agents: diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index 346da9b7..fa1c4451 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 480cb8da..c6b74f2e 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from typing import ClassVar, List, Optional, Union diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index 7ccffb0a..fa47ffb1 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index a0805a49..1191987b 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod from primaite.game.agent.actions.manager import AbstractAction diff --git a/src/primaite/game/agent/actions/config.py b/src/primaite/game/agent/actions/software.py similarity index 99% rename from src/primaite/game/agent/actions/config.py rename to src/primaite/game/agent/actions/software.py index 050e9b94..760e8dfa 100644 --- a/src/primaite/game/agent/actions/config.py +++ b/src/primaite/game/agent/actions/software.py @@ -1,4 +1,4 @@ -# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import List, Optional, Union diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index f065d888..8f0bd24b 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -381,15 +381,16 @@ class ActionPenalty(AbstractReward, identifier="ACTION_PENALTY"): class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for ActionPenalty. + :param action_penalty: Reward to give agents for taking any action except do_nothing :type action_penalty: float :param do_nothing_penalty: Reward to give agent for taking the do_nothing action :type do_nothing_penalty: float """ + action_penalty: float = -1.0 do_nothing_penalty: float = 0.0 - def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: """Calculate the penalty to be applied. diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 8c97573a..338bd049 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -4,7 +4,7 @@ from ipaddress import IPv4Address import pytest from pydantic import ValidationError -from primaite.game.agent.actions.config import ( +from primaite.game.agent.actions.software import ( ConfigureDatabaseClientAction, ConfigureDoSBotAction, ConfigureRansomwareScriptAction, From a0a5f2ca38ddd6f2cb6cba3acccd7df15358266d Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 3 Jan 2025 15:03:01 +0000 Subject: [PATCH 107/224] #2912 - Review comment actions following commit revertions --- src/primaite/game/agent/actions/manager.py | 2 -- tests/conftest.py | 27 ---------------------- 2 files changed, 29 deletions(-) diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index b612d9ce..625d8cec 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -80,8 +80,6 @@ class ActionManager: self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} # make sure all numbers between 0 and N are represented as dict keys in action map assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) - self.node_names: List[str] = [n["node_name"] for n in nodes] - """List of node names in this action space. The list order is the mapping between node index and node name.""" def get_action(self, action: int) -> Tuple[str, Dict]: """Produce action in CAOS format.""" diff --git a/tests/conftest.py b/tests/conftest.py index 59fe025f..0d73aa07 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -471,33 +471,6 @@ def game_and_agent(): action_space = ActionManager( actions=actions, # ALL POSSIBLE ACTIONS - nodes=[ - { - "node_name": "client_1", - "applications": [ - {"application_name": "WebBrowser"}, - {"application_name": "DoSBot"}, - {"application_name": "C2Server"}, - ], - "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], - }, - { - "node_name": "server_1", - "services": [{"service_name": "DNSServer"}], - "applications": [{"application_name": "C2Beacon"}], - }, - {"node_name": "server_2", "services": [{"service_name": "WebServer"}]}, - {"node_name": "router"}, - ], - max_folders_per_node=2, - max_files_per_folder=2, - max_services_per_node=2, - max_applications_per_node=3, - max_nics_per_node=2, - max_acl_rules=10, - protocols=["TCP", "UDP", "ICMP"], - ports=["HTTP", "DNS", "ARP"], - ip_list=["10.0.1.1", "10.0.1.2", "10.0.2.1", "10.0.2.2", "10.0.2.3"], act_map={}, ) observation_space = ObservationManager(NestedObservation(components={})) From 30d8f142511e2d3c0add63c0bcb13ddce09bb91c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 3 Jan 2025 16:26:12 +0000 Subject: [PATCH 108/224] #2888 - Put software configuration items in the ConfigSchema --- src/primaite/game/game.py | 64 ++----------------- .../system/applications/application.py | 4 +- .../system/applications/database_client.py | 4 ++ .../red_applications/c2/abstract_c2.py | 17 ++--- .../red_applications/c2/c2_beacon.py | 9 ++- .../red_applications/data_manipulation_bot.py | 12 ++++ .../applications/red_applications/dos_bot.py | 27 ++++++-- .../red_applications/ransomware_script.py | 7 ++ .../system/applications/web_browser.py | 2 + .../simulator/system/core/software_manager.py | 25 +++++--- .../simulator/system/services/service.py | 14 ++-- src/primaite/simulator/system/software.py | 35 ++++++++-- .../applications/extended_application.py | 2 + 13 files changed, 125 insertions(+), 97 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 6555e272..5764ad11 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -50,7 +50,7 @@ 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 -from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -422,74 +422,20 @@ class PrimaiteGame: application_type = application_cfg["type"] if application_type in Application._registry: - new_node.software_manager.install(Application._registry[application_type]) + application_class = Application._registry[application_type] + application_options = application_cfg.get("options", {}) + application_options["type"] = application_type + new_node.software_manager.install(application_class, software_config=application_options) new_application = new_node.software_manager.software[application_type] # grab the instance - # fixing duration for the application - if "fix_duration" in application_cfg.get("options", {}): - new_application.fixing_duration = application_cfg["options"]["fix_duration"] else: msg = f"Configuration contains an invalid application type: {application_type}" _LOGGER.error(msg) raise ValueError(msg) - _set_software_listen_on_ports(new_application, application_cfg) - # run the application new_application.run() - if application_type == "DataManipulationBot": - if "options" in application_cfg: - opt = application_cfg["options"] - new_application.configure( - server_ip_address=IPv4Address(opt.get("server_ip")), - server_password=opt.get("server_password"), - payload=opt.get("payload", "DELETE"), - port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")), - data_manipulation_p_of_success=float(opt.get("data_manipulation_p_of_success", "0.1")), - ) - elif application_type == "RansomwareScript": - if "options" in application_cfg: - opt = application_cfg["options"] - new_application.configure( - server_ip_address=IPv4Address(opt.get("server_ip")) if opt.get("server_ip") else None, - server_password=opt.get("server_password"), - payload=opt.get("payload", "ENCRYPT"), - ) - elif application_type == "DatabaseClient": - if "options" in application_cfg: - opt = application_cfg["options"] - new_application.configure( - server_ip_address=IPv4Address(opt.get("db_server_ip")), - server_password=opt.get("server_password"), - ) - elif application_type == "WebBrowser": - if "options" in application_cfg: - opt = application_cfg["options"] - new_application.target_url = opt.get("target_url") - elif application_type == "DoSBot": - if "options" in application_cfg: - opt = application_cfg["options"] - new_application.configure( - target_ip_address=IPv4Address(opt.get("target_ip_address")), - target_port=PORT_LOOKUP[opt.get("target_port", "POSTGRES_SERVER")], - payload=opt.get("payload"), - repeat=bool(opt.get("repeat")), - port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")), - dos_intensity=float(opt.get("dos_intensity", "1.0")), - max_sessions=int(opt.get("max_sessions", "1000")), - ) - elif application_type == "C2Beacon": - if "options" in application_cfg: - opt = application_cfg["options"] - new_application.configure( - c2_server_ip_address=IPv4Address(opt.get("c2_server_ip_address")), - keep_alive_frequency=(opt.get("keep_alive_frequency", 5)), - masquerade_protocol=PROTOCOL_LOOKUP[ - (opt.get("masquerade_protocol", PROTOCOL_LOOKUP["TCP"])) - ], - masquerade_port=PORT_LOOKUP[(opt.get("masquerade_port", PORT_LOOKUP["HTTP"]))], - ) if "network_interfaces" in node_cfg: for nic_num, nic_cfg in node_cfg["network_interfaces"].items(): new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index e0cac6b4..4e6f5cf0 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from enum import Enum from typing import Any, ClassVar, Dict, Optional, Set, Type -from pydantic import BaseModel, Field +from pydantic import Field from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestPermissionValidator, RequestType @@ -30,7 +30,7 @@ class Application(IOSoftware, ABC): Applications are user-facing programs that may perform input/output operations. """ - class ConfigSchema(BaseModel, ABC): + class ConfigSchema(IOSoftware.ConfigSchema, ABC): """Config Schema for Application class.""" type: str diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index facc4016..4b7286de 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -73,6 +73,8 @@ class DatabaseClient(Application, identifier="DatabaseClient"): """ConfigSchema for DatabaseClient.""" type: str = "DatabaseClient" + db_server_ip: Optional[IPV4Address] = None + server_password: Optional[str] = None config: ConfigSchema = Field(default_factory=lambda: DatabaseClient.ConfigSchema()) @@ -99,6 +101,8 @@ class DatabaseClient(Application, identifier="DatabaseClient"): kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) + self.server_ip_address = self.config.db_server_ip + self.server_password = self.config.server_password def _init_request_manager(self) -> RequestManager: """ diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index a379769d..71a896bc 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -2,7 +2,7 @@ from abc import abstractmethod from enum import Enum from ipaddress import IPv4Address -from typing import Dict, Optional, Union +from typing import Dict, Optional, Set, Union from pydantic import Field, validate_call @@ -75,6 +75,8 @@ class AbstractC2(Application): masquerade_port: Port = Field(default=PORT_LOOKUP["HTTP"]) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" + listen_on_ports: Set[Port] = {PORT_LOOKUP["HTTP"], PORT_LOOKUP["FTP"], PORT_LOOKUP["DNS"]} + config: ConfigSchema = Field(default_factory=lambda: AbstractC2.ConfigSchema()) c2_connection_active: bool = False @@ -101,6 +103,12 @@ class AbstractC2(Application): C2 beacon to reconfigure it's configuration settings. """ + def __init__(self, **kwargs): + """Initialise the C2 applications to by default listen for HTTP traffic.""" + kwargs["port"] = PORT_LOOKUP["NONE"] + kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] + super().__init__(**kwargs) + def _craft_packet( self, c2_payload: C2Payload, c2_command: Optional[C2Command] = None, command_options: Optional[Dict] = {} ) -> C2Packet: @@ -141,13 +149,6 @@ class AbstractC2(Application): """ return super().describe_state() - def __init__(self, **kwargs): - """Initialise the C2 applications to by default listen for HTTP traffic.""" - kwargs["listen_on_ports"] = {PORT_LOOKUP["HTTP"], PORT_LOOKUP["FTP"], PORT_LOOKUP["DNS"]} - kwargs["port"] = PORT_LOOKUP["NONE"] - kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] - super().__init__(**kwargs) - @property def _host_ftp_client(self) -> Optional[FTPClient]: """Return the FTPClient that is installed C2 Application's host. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 014a4096..b9c968c5 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -12,8 +12,9 @@ from primaite.simulator.system.applications.red_applications.c2 import ExfilOpts from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.terminal.terminal import Terminal, TerminalClientConnection -from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP -from primaite.utils.validation.port import PORT_LOOKUP +from primaite.utils.validation.ip_protocol import IPProtocol, PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import Port, PORT_LOOKUP class C2Beacon(AbstractC2, identifier="C2Beacon"): @@ -39,6 +40,10 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): """ConfigSchema for C2Beacon.""" type: str = "C2Beacon" + c2_server_ip_address: Optional[IPV4Address] = None + keep_alive_frequency: int = 5 + masquerade_protocol: IPProtocol = PROTOCOL_LOOKUP["TCP"] + masquerade_port: Port = PORT_LOOKUP["HTTP"] config: ConfigSchema = Field(default_factory=lambda: C2Beacon.ConfigSchema()) diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 1978afb9..392cdfba 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -12,6 +12,7 @@ from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -46,6 +47,11 @@ class DataManipulationBot(Application, identifier="DataManipulationBot"): """Configuration schema for DataManipulationBot.""" type: str = "DataManipulationBot" + server_ip: Optional[IPV4Address] = None + server_password: Optional[str] = None + payload: str = "DELETE" + port_scan_p_of_success: float = 0.1 + data_manipulation_p_of_success: float = 0.1 config: "DataManipulationBot.ConfigSchema" = Field(default_factory=lambda: DataManipulationBot.ConfigSchema()) @@ -65,6 +71,12 @@ class DataManipulationBot(Application, identifier="DataManipulationBot"): super().__init__(**kwargs) self._db_connection: Optional[DatabaseClientConnection] = None + self.server_ip_address = self.config.server_ip + self.server_password = self.config.server_password + self.payload = self.config.payload + self.port_scan_p_of_success = self.config.port_scan_p_of_success + self.data_manipulation_p_of_success = self.config.data_manipulation_p_of_success + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index e284ba92..ea7a4d8d 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -9,8 +9,8 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -35,6 +35,18 @@ class DoSAttackStage(IntEnum): class DoSBot(DatabaseClient, identifier="DoSBot"): """A bot that simulates a Denial of Service attack.""" + class ConfigSchema(DatabaseClient.ConfigSchema): + """ConfigSchema for DoSBot.""" + + type: str = "DoSBot" + target_ip_address: Optional[IPV4Address] = None + target_port: Port = PORT_LOOKUP["POSTGRES_SERVER"] + payload: Optional[str] = None + repeat: bool = False + port_scan_p_of_success: float = 0.1 + dos_intensity: float = 1.0 + max_sessions: int = 1000 + config: "DoSBot.ConfigSchema" = Field(default_factory=lambda: DoSBot.ConfigSchema()) target_ip_address: Optional[IPv4Address] = None @@ -58,15 +70,16 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): dos_intensity: float = 1.0 """How much of the max sessions will be used by the DoS when attacking.""" - class ConfigSchema(Application.ConfigSchema): - """ConfigSchema for DoSBot.""" - - type: str = "DoSBot" - def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "DoSBot" - self.max_sessions = 1000 # override normal max sessions + self.target_ip_address = self.config.target_ip_address + self.target_port = self.config.target_port + self.payload = self.config.payload + self.repeat = self.config.repeat + self.port_scan_p_of_success = self.config.port_scan_p_of_success + self.dos_intensity = self.config.dos_intensity + self.max_sessions = self.config.max_sessions def _init_request_manager(self) -> RequestManager: """ diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index b72dc8e5..114d5716 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -10,6 +10,7 @@ from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP @@ -23,6 +24,9 @@ class RansomwareScript(Application, identifier="RansomwareScript"): """ConfigSchema for RansomwareScript.""" type: str = "RansomwareScript" + server_ip: Optional[IPV4Address] = None + server_password: Optional[str] = None + payload: str = "ENCRYPT" config: "RansomwareScript.ConfigSchema" = Field(default_factory=lambda: RansomwareScript.ConfigSchema()) @@ -40,6 +44,9 @@ class RansomwareScript(Application, identifier="RansomwareScript"): super().__init__(**kwargs) self._db_connection: Optional[DatabaseClientConnection] = None + self.server_ip_address = self.config.server_ip + self.server_password = self.config.server_password + self.payload = self.config.payload def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 52a566f2..ad20640f 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -34,6 +34,7 @@ class WebBrowser(Application, identifier="WebBrowser"): """ConfigSchema for WebBrowser.""" type: str = "WebBrowser" + target_url: Optional[str] = None config: "WebBrowser.ConfigSchema" = Field(default_factory=lambda: WebBrowser.ConfigSchema()) @@ -56,6 +57,7 @@ class WebBrowser(Application, identifier="WebBrowser"): kwargs["port"] = PORT_LOOKUP["HTTP"] super().__init__(**kwargs) + self.target_url = self.config.target_url self.run() def _init_request_manager(self) -> RequestManager: diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index f0ee6f7c..ddb30a3b 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -106,7 +106,7 @@ class SoftwareManager: return True return False - def install(self, software_class: Type[IOSoftware], **install_kwargs): + def install(self, software_class: Type[IOSoftware], software_config: Optional[IOSoftware.ConfigSchema] = None): """ Install an Application or Service. @@ -115,13 +115,22 @@ class SoftwareManager: if software_class in self._software_class_to_name_map: self.sys_log.warning(f"Cannot install {software_class} as it is already installed") return - software = software_class( - software_manager=self, - sys_log=self.sys_log, - file_system=self.file_system, - dns_server=self.dns_server, - **install_kwargs, - ) + if software_config is None: + software = software_class( + software_manager=self, + sys_log=self.sys_log, + file_system=self.file_system, + dns_server=self.dns_server, + ) + else: + software = software_class( + software_manager=self, + sys_log=self.sys_log, + file_system=self.file_system, + dns_server=self.dns_server, + config=software_config, + ) + software.parent = self.node if isinstance(software, Application): self.node.applications[software.uuid] = software diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index bbf8c479..c30294bb 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from enum import Enum from typing import Any, ClassVar, Dict, Optional, Type -from pydantic import BaseModel +from pydantic import Field from primaite import getLogger from primaite.interface.request import RequestFormat, RequestResponse @@ -39,7 +39,12 @@ class Service(IOSoftware): Services are programs that run in the background and may perform input/output operations. """ - config: "Service.ConfigSchema" + class ConfigSchema(IOSoftware.ConfigSchema, ABC): + """Config Schema for Service class.""" + + type: str + + config: "Service.ConfigSchema" = Field(default_factory=lambda: Service.ConfigSchema()) operating_state: ServiceOperatingState = ServiceOperatingState.STOPPED "The current operating state of the Service." @@ -53,11 +58,6 @@ class Service(IOSoftware): _registry: ClassVar[Dict[str, Type["Service"]]] = {} """Registry of service types. Automatically populated when subclasses are defined.""" - class ConfigSchema(BaseModel, ABC): - """Config Schema for Service class.""" - - type: str - def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 34c893eb..4b670fe0 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -1,13 +1,13 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import copy -from abc import abstractmethod +from abc import ABC, abstractmethod from datetime import datetime from enum import Enum from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, Optional, Set, TYPE_CHECKING, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import Field +from pydantic import BaseModel, ConfigDict, Field from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -70,7 +70,7 @@ class SoftwareCriticality(Enum): "The highest level of criticality." -class Software(SimComponent): +class Software(SimComponent, ABC): """ A base class representing software in a simulator environment. @@ -78,6 +78,16 @@ class Software(SimComponent): It outlines the fundamental attributes and behaviors expected of any software in the simulation. """ + class ConfigSchema(BaseModel, ABC): + """Configurable options for all software.""" + + model_config = ConfigDict(extra="forbid") + starting_health_state: SoftwareHealthState = SoftwareHealthState.UNUSED + criticality: SoftwareCriticality = SoftwareCriticality.LOWEST + fixing_duration: int = 2 + + config: ConfigSchema = Field(default_factory=lambda: Software.ConfigSchema()) + name: str "The name of the software." health_state_actual: SoftwareHealthState = SoftwareHealthState.UNUSED @@ -105,6 +115,12 @@ class Software(SimComponent): _fixing_countdown: Optional[int] = None "Current number of ticks left to patch the software." + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.health_state_actual = self.config.starting_health_state + self.criticality = self.config.criticality + self.fixing_duration = self.config.fixing_duration + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager. @@ -233,7 +249,7 @@ class Software(SimComponent): super().pre_timestep(timestep) -class IOSoftware(Software): +class IOSoftware(Software, ABC): """ Represents software in a simulator environment that is capable of input/output operations. @@ -243,6 +259,13 @@ class IOSoftware(Software): required. """ + class ConfigSchema(Software.ConfigSchema, ABC): + """Configuration options for all IO Software.""" + + listen_on_ports: Set[Port] = Field(default_factory=set) + + config: ConfigSchema = Field(default_factory=lambda: IOSoftware.ConfigSchema()) + installing_count: int = 0 "The number of times the software has been installed. Default is 0." max_sessions: int = 100 @@ -260,6 +283,10 @@ class IOSoftware(Software): _connections: Dict[str, Dict] = {} "Active connections." + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.listen_on_ports = self.config.listen_on_ports + @abstractmethod def describe_state(self) -> Dict: """ diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 13fa3d1b..159cfd06 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -35,6 +35,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): """ConfigSchema for ExtendedApplication.""" type: str = "ExtendedApplication" + target_url: Optional[str] = None config: "ExtendedApplication.ConfigSchema" = Field(default_factory=lambda: ExtendedApplication.ConfigSchema()) @@ -57,6 +58,7 @@ class ExtendedApplication(Application, identifier="ExtendedApplication"): kwargs["port"] = PORT_LOOKUP["HTTP"] super().__init__(**kwargs) + self.target_url = self.config.target_url self.run() def _init_request_manager(self) -> RequestManager: From 632201681b15195f1652d766f1ce542628fc33d7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 6 Jan 2025 10:08:32 +0000 Subject: [PATCH 109/224] #2888 - fix software config issues --- .../applications/red_applications/c2/c2_beacon.py | 14 ++++++++++---- src/primaite/simulator/system/software.py | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index b9c968c5..449cc8d3 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -53,6 +53,16 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): terminal_session: TerminalClientConnection = None "The currently in use terminal session." + def __init__(self, **kwargs): + kwargs["name"] = "C2Beacon" + super().__init__(**kwargs) + self.configure( + c2_server_ip_address=self.config.c2_server_ip_address, + keep_alive_frequency=self.config.keep_alive_frequency, + masquerade_port=self.config.masquerade_port, + masquerade_protocol=self.config.masquerade_protocol, + ) + @property def _host_terminal(self) -> Optional[Terminal]: """Return the Terminal that is installed on the same machine as the C2 Beacon.""" @@ -131,10 +141,6 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): rm.add_request("configure", request_type=RequestType(func=_configure)) return rm - def __init__(self, **kwargs): - kwargs["name"] = "C2Beacon" - super().__init__(**kwargs) - # Configure is practically setter method for the ``c2.config`` attribute that also ties into the request manager. @validate_call def configure( diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 4b670fe0..12e3b2f2 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -84,7 +84,7 @@ class Software(SimComponent, ABC): model_config = ConfigDict(extra="forbid") starting_health_state: SoftwareHealthState = SoftwareHealthState.UNUSED criticality: SoftwareCriticality = SoftwareCriticality.LOWEST - fixing_duration: int = 2 + fix_duration: int = 2 config: ConfigSchema = Field(default_factory=lambda: Software.ConfigSchema()) @@ -117,9 +117,9 @@ class Software(SimComponent, ABC): def __init__(self, **kwargs): super().__init__(**kwargs) - self.health_state_actual = self.config.starting_health_state + self.health_state_actual = self.config.starting_health_state # don't remove this self.criticality = self.config.criticality - self.fixing_duration = self.config.fixing_duration + self.fixing_duration = self.config.fix_duration def _init_request_manager(self) -> RequestManager: """ From 695ebb5ec70f8422f53472cd9bd266a20c2f9138 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 6 Jan 2025 10:13:27 +0000 Subject: [PATCH 110/224] #2888 - fix test database class to use correct listener default --- .../integration_tests/system/test_service_listening_on_ports.py | 1 + 1 file changed, 1 insertion(+) 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 a57bd539..84413ac9 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -19,6 +19,7 @@ class _DatabaseListener(Service, identifier="_DatabaseListener"): """ConfigSchema for _DatabaseListener.""" type: str = "_DatabaseListener" + listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} config: "_DatabaseListener.ConfigSchema" = Field(default_factory=lambda: _DatabaseListener.ConfigSchema()) name: str = "DatabaseListener" From 66d309871f18060fe3821aab158a6ff1e7b08770 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 6 Jan 2025 11:38:07 +0000 Subject: [PATCH 111/224] #2869 - Minor changes trying to make pytest happy --- docs/source/how_to_guides/extensible_agents.rst | 4 ++-- src/primaite/game/agent/interface.py | 6 +++--- src/primaite/game/agent/scripted_agents/__init__.py | 7 +------ .../game/agent/scripted_agents/data_manipulation_bot.py | 1 - src/primaite/game/agent/scripted_agents/random_agent.py | 4 ++-- src/primaite/game/game.py | 7 +++---- tests/conftest.py | 3 ++- .../software_installation_and_configuration.py | 2 +- .../game_layer/observations/test_nic_observations.py | 2 +- tests/integration_tests/game_layer/test_rewards.py | 2 +- .../_primaite/_game/_agent/test_sticky_rewards.py | 2 +- 11 files changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index b7c17b83..b9e00b60 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -7,13 +7,13 @@ Extensible Agents ***************** -Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents. +Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents. Developing Agents for PrimAITE ============================== -Agents within PrimAITE, follow the shown inheritance structure below. +Agents within PrimAITE, follow the shown inheritance structure below. # TODO: Turn this into an inheritance diagram diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index b980d748..6c1c633a 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -51,9 +51,9 @@ class AbstractAgent(BaseModel): history: List[AgentHistoryItem] = [] config: "AbstractAgent.ConfigSchema" - action_manager: ActionManager - observation_manager: ObservationManager - reward_function: RewardFunction + action_manager: "ActionManager" + observation_manager: "ObservationManager" + reward_function: "RewardFunction" class ConfigSchema(BaseModel): """ diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py index 59e8bf40..5a97d15b 100644 --- a/src/primaite/game/agent/scripted_agents/__init__.py +++ b/src/primaite/game/agent/scripted_agents/__init__.py @@ -1,11 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.game.agent import interface -from primaite.game.agent.scripted_agents import ( - abstract_tap, - data_manipulation_bot, - probabilistic_agent, - random_agent, -) +from primaite.game.agent.scripted_agents import abstract_tap, data_manipulation_bot, probabilistic_agent, random_agent __all__ = ("abstract_tap", "data_manipulation_bot", "interface", "probabilistic_agent", "random_agent") diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 3d9a7101..7ec119cf 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,7 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import Dict, Optional, Tuple - from gymnasium.core import ObsType from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 89b19ece..14f642ef 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -37,6 +37,7 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" + agent_name: str = "Periodic_Agent" """Name of the agent.""" @@ -57,13 +58,12 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): def start_variance(self) -> int: """Returns the deviation around the start step.""" return self.config.agent_settings.start_settings.variance - + @property def frequency(self) -> int: """Returns the number of timesteps to wait between performing actions.""" return self.config.agent_settings.start_settings.frequency - def _set_next_execution_timestep(self, timestep: int, variance: int) -> None: """Set the next execution timestep with a configured random variance. diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index e83f59a6..db23eb14 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -525,23 +525,22 @@ class PrimaiteGame: agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: - agent_ref = agent_cfg["ref"] # noqa: F841 + agent_name = agent_cfg["ref"] # noqa: F841 agent_type = agent_cfg["type"] action_space_cfg = agent_cfg["action_space"] observation_space_cfg = agent_cfg["observation_space"] reward_function_cfg = agent_cfg["reward_function"] agent_settings = agent_cfg["agent_settings"] - # CREATE AGENT agent_config = { - "agent_name": agent_ref, + "agent_name": agent_name, "action_manager": action_space_cfg, "observation_manager": observation_space_cfg, "reward_function": reward_function_cfg, "agent_settings": agent_settings, } - # new_agent_cfg.update{} + # CREATE AGENT if agent_type in AbstractAgent._registry: new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) # If blue agent is created, add to game.rl_agents diff --git a/tests/conftest.py b/tests/conftest.py index 28563333..0c211f49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,9 @@ from ray import init as rayinit from primaite import getLogger, PRIMAITE_PATHS from primaite.game.agent.actions import ActionManager +from primaite.game.agent.interface import AbstractAgent from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction -from primaite.game.agent.interface import AbstractAgent from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.game.game import PrimaiteGame from primaite.simulator.file_system.file_system import FileSystem @@ -369,6 +369,7 @@ def install_stuff_to_sim(sim: Simulation): # 5: Assert that the simulation starts off in the state that we expect assert len(sim.network.nodes) == 6 assert len(sim.network.links) == 5 + # 5.1: Assert the router is correctly configured r = sim.network.router_nodes[0] for i, acl_rule in enumerate(r.acl.acl): diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index 560fc44c..0ff6754d 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -6,8 +6,8 @@ from typing import Union import yaml from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.game.game import PrimaiteGame, SERVICE_TYPES_MAPPING from primaite.simulator.network.container import Network diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 6bcf6b4d..0ad03198 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -6,8 +6,8 @@ import pytest import yaml from gymnasium import spaces -from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.observations.nic_observations import NICObservation from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.base import NetworkInterface from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index c7aa6c99..dc7ed132 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -2,8 +2,8 @@ import pytest import yaml -from primaite.game.agent.rewards import ActionPenalty, GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty from primaite.game.agent.interface import AgentHistoryItem +from primaite.game.agent.rewards import ActionPenalty, GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty from primaite.game.game import PrimaiteGame from primaite.interface.request import RequestResponse from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index de3f8144..0e4bf1bb 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -1,11 +1,11 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from primaite.game.agent.interface import AgentHistoryItem from primaite.game.agent.rewards import ( GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty, WebServer404Penalty, ) -from primaite.game.agent.interface import AgentHistoryItem from primaite.interface.request import RequestResponse From cb4e10921ee03b194d5810ce1a016adceba7ad8f Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 6 Jan 2025 16:33:57 +0000 Subject: [PATCH 112/224] #2888: Use fixing_duration from ConfigSchema. --- .../system/applications/ransomware_script.rst | 2 +- .../system/common/common_configuration.rst | 4 +-- src/primaite/game/game.py | 4 +-- src/primaite/simulator/system/software.py | 12 ++------ ...tem.yaml => fixing_duration_one_item.yaml} | 4 +-- ...ion.yaml => software_fixing_duration.yaml} | 28 +++++++++---------- ...on.py => test_software_fixing_duration.py} | 28 +++++++++---------- 7 files changed, 38 insertions(+), 44 deletions(-) rename tests/assets/configs/{fix_duration_one_item.yaml => fixing_duration_one_item.yaml} (99%) rename tests/assets/configs/{software_fix_duration.yaml => software_fixing_duration.yaml} (93%) rename tests/integration_tests/configuration_file_parsing/{test_software_fix_duration.py => test_software_fixing_duration.py} (77%) diff --git a/docs/source/simulation_components/system/applications/ransomware_script.rst b/docs/source/simulation_components/system/applications/ransomware_script.rst index b79ca802..192618fc 100644 --- a/docs/source/simulation_components/system/applications/ransomware_script.rst +++ b/docs/source/simulation_components/system/applications/ransomware_script.rst @@ -70,7 +70,7 @@ Python Configuration ============= -The RansomwareScript inherits configuration options such as ``fix_duration`` from its parent class. However, for the ``RansomwareScript`` the most relevant option is ``server_ip``. +The RansomwareScript inherits configuration options such as ``fixing_duration`` from its parent class. However, for the ``RansomwareScript`` the most relevant option is ``server_ip``. ``server_ip`` diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index 411fd529..c1bbd4b2 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -22,8 +22,8 @@ options The configuration options are the attributes that fall under the options for an application or service. -fix_duration -"""""""""""" +fixing_duration +""""""""""""""" Optional. Default value is ``2``. diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5764ad11..d8b28e94 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -380,8 +380,8 @@ class PrimaiteGame: new_service = new_node.software_manager.software[service_class.__name__] # fixing duration for the service - if "fix_duration" in service_cfg.get("options", {}): - new_service.fixing_duration = service_cfg["options"]["fix_duration"] + if "fixing_duration" in service_cfg.get("options", {}): + new_service.config.fixing_duration = service_cfg["options"]["fixing_duration"] _set_software_listen_on_ports(new_service, service_cfg) # start the service diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 12e3b2f2..25b2366c 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -84,7 +84,7 @@ class Software(SimComponent, ABC): model_config = ConfigDict(extra="forbid") starting_health_state: SoftwareHealthState = SoftwareHealthState.UNUSED criticality: SoftwareCriticality = SoftwareCriticality.LOWEST - fix_duration: int = 2 + fixing_duration: int = 2 config: ConfigSchema = Field(default_factory=lambda: Software.ConfigSchema()) @@ -94,8 +94,6 @@ class Software(SimComponent, ABC): "The actual health state of the software." health_state_visible: SoftwareHealthState = SoftwareHealthState.UNUSED "The health state of the software visible to the red agent." - criticality: SoftwareCriticality = SoftwareCriticality.LOWEST - "The criticality level of the software." fixing_count: int = 0 "The count of patches applied to the software, defaults to 0." scanning_count: int = 0 @@ -110,16 +108,12 @@ class Software(SimComponent, ABC): "The FileSystem of the Node the Software is installed on." folder: Optional[Folder] = None "The folder on the file system the Software uses." - fixing_duration: int = 2 - "The number of ticks it takes to patch the software." _fixing_countdown: Optional[int] = None "Current number of ticks left to patch the software." def __init__(self, **kwargs): super().__init__(**kwargs) self.health_state_actual = self.config.starting_health_state # don't remove this - self.criticality = self.config.criticality - self.fixing_duration = self.config.fix_duration def _init_request_manager(self) -> RequestManager: """ @@ -168,7 +162,7 @@ class Software(SimComponent, ABC): { "health_state_actual": self.health_state_actual.value, "health_state_visible": self.health_state_visible.value, - "criticality": self.criticality.value, + "criticality": self.config.criticality.value, "fixing_count": self.fixing_count, "scanning_count": self.scanning_count, "revealed_to_red": self.revealed_to_red, @@ -217,7 +211,7 @@ class Software(SimComponent, ABC): def fix(self) -> bool: """Perform a fix on the software.""" if self.health_state_actual in (SoftwareHealthState.COMPROMISED, SoftwareHealthState.GOOD): - self._fixing_countdown = self.fixing_duration + self._fixing_countdown = self.config.fixing_duration self.set_health_state(SoftwareHealthState.FIXING) return True return False diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fixing_duration_one_item.yaml similarity index 99% rename from tests/assets/configs/fix_duration_one_item.yaml rename to tests/assets/configs/fixing_duration_one_item.yaml index bd0fb61f..57c1c4ce 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fixing_duration_one_item.yaml @@ -185,7 +185,7 @@ simulation: options: db_server_ip: 192.168.1.10 server_password: arcd - fix_duration: 1 + fixing_duration: 1 - type: DataManipulationBot options: port_scan_p_of_success: 0.8 @@ -208,7 +208,7 @@ simulation: arcd.com: 192.168.1.10 - type: DatabaseService options: - fix_duration: 5 + fixing_duration: 5 backup_server_ip: 192.168.1.10 - type: WebServer - type: FTPClient diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fixing_duration.yaml similarity index 93% rename from tests/assets/configs/software_fix_duration.yaml rename to tests/assets/configs/software_fixing_duration.yaml index 1a28258b..bb1254ed 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fixing_duration.yaml @@ -179,19 +179,19 @@ simulation: applications: - type: NMAP options: - fix_duration: 1 + fixing_duration: 1 - type: RansomwareScript options: - fix_duration: 1 + fixing_duration: 1 - type: WebBrowser options: target_url: http://arcd.com/users/ - fix_duration: 1 + fixing_duration: 1 - type: DatabaseClient options: db_server_ip: 192.168.1.10 server_password: arcd - fix_duration: 1 + fixing_duration: 1 - type: DataManipulationBot options: port_scan_p_of_success: 0.8 @@ -199,44 +199,44 @@ simulation: payload: "DELETE" server_ip: 192.168.1.21 server_password: arcd - fix_duration: 1 + fixing_duration: 1 - type: DoSBot options: target_ip_address: 192.168.10.21 payload: SPOOF DATA port_scan_p_of_success: 0.8 - fix_duration: 1 + fixing_duration: 1 services: - type: DNSClient options: dns_server: 192.168.1.10 - fix_duration: 3 + fixing_duration: 3 - type: DNSServer options: - fix_duration: 3 + fixing_duration: 3 domain_mapping: arcd.com: 192.168.1.10 - type: DatabaseService options: backup_server_ip: 192.168.1.10 - fix_duration: 3 + fixing_duration: 3 - type: WebServer options: - fix_duration: 3 + fixing_duration: 3 - type: FTPClient options: - fix_duration: 3 + fixing_duration: 3 - type: FTPServer options: server_password: arcd - fix_duration: 3 + fixing_duration: 3 - type: NTPClient options: ntp_server_ip: 192.168.1.10 - fix_duration: 3 + fixing_duration: 3 - type: NTPServer options: - fix_duration: 3 + fixing_duration: 3 - hostname: client_2 type: computer ip_address: 192.168.10.22 diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py similarity index 77% rename from tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py rename to tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py index b1c644cc..8e8013d5 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fix_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py @@ -27,27 +27,27 @@ def load_config(config_path: Union[str, Path]) -> PrimaiteGame: return PrimaiteGame.from_config(cfg) -def test_default_fix_duration(): - """Test that software with no defined fix duration in config uses the default fix duration of 2.""" +def test_default_fixing_duration(): + """Test that software with no defined fixing duration in config uses the default fixing duration of 2.""" game = load_config(TEST_CONFIG) client_2: Computer = game.simulation.network.get_node_by_hostname("client_2") database_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") - assert database_client.fixing_duration == 2 + assert database_client.config.fixing_duration == 2 dns_client: DNSClient = client_2.software_manager.software.get("DNSClient") - assert dns_client.fixing_duration == 2 + assert dns_client.config.fixing_duration == 2 -def test_fix_duration_set_from_config(): - """Test to check that the fix duration set for applications and services works as intended.""" +def test_fixing_duration_set_from_config(): + """Test to check that the fixing duration set for applications and services works as intended.""" game = load_config(TEST_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") # in config - services take 3 timesteps to fix for service in ["DNSClient", "DNSServer", "DatabaseService", "WebServer", "FTPClient", "FTPServer", "NTPServer"]: assert client_1.software_manager.software.get(service) is not None - assert client_1.software_manager.software.get(service).fixing_duration == 3 + assert client_1.software_manager.software.get(service).config.fixing_duration == 3 # in config - applications take 1 timestep to fix # remove test applications from list @@ -55,27 +55,27 @@ def test_fix_duration_set_from_config(): for application in ["RansomwareScript", "WebBrowser", "DataManipulationBot", "DoSBot", "DatabaseClient"]: assert client_1.software_manager.software.get(application) is not None - assert client_1.software_manager.software.get(application).fixing_duration == 1 + assert client_1.software_manager.software.get(application).config.fixing_duration == 1 -def test_fix_duration_for_one_item(): - """Test that setting fix duration for one application does not affect other components.""" +def test_fixing_duration_for_one_item(): + """Test that setting fixing duration for one application does not affect other components.""" game = load_config(ONE_ITEM_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") # in config - services take 3 timesteps to fix for service in ["DNSClient", "DNSServer", "WebServer", "FTPClient", "FTPServer", "NTPServer"]: assert client_1.software_manager.software.get(service) is not None - assert client_1.software_manager.software.get(service).fixing_duration == 2 + assert client_1.software_manager.software.get(service).config.fixing_duration == 2 # in config - applications take 1 timestep to fix # remove test applications from list for applications in ["RansomwareScript", "WebBrowser", "DataManipulationBot", "DoSBot"]: assert client_1.software_manager.software.get(applications) is not None - assert client_1.software_manager.software.get(applications).fixing_duration == 2 + assert client_1.software_manager.software.get(applications).config.fixing_duration == 2 database_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") - assert database_client.fixing_duration == 1 + assert database_client.config.fixing_duration == 1 database_service: DatabaseService = client_1.software_manager.software.get("DatabaseService") - assert database_service.fixing_duration == 5 + assert database_service.config.fixing_duration == 5 From d0c357355cf2fb65ea6d7749ea7ecbe17df6de83 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 7 Jan 2025 10:27:41 +0000 Subject: [PATCH 113/224] #2888: Update tests to use config.fixing_duration. --- .../test_software_fixing_duration.py | 4 ++-- tests/integration_tests/system/test_database_on_node.py | 6 +++--- .../system/test_web_client_server_and_database.py | 2 +- .../_primaite/_simulator/_system/_services/test_services.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py index 8e8013d5..10896956 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py @@ -13,8 +13,8 @@ from primaite.simulator.system.services.database.database_service import Databas from primaite.simulator.system.services.dns.dns_client import DNSClient from tests import TEST_ASSETS_ROOT -TEST_CONFIG = TEST_ASSETS_ROOT / "configs/software_fix_duration.yaml" -ONE_ITEM_CONFIG = TEST_ASSETS_ROOT / "configs/fix_duration_one_item.yaml" +TEST_CONFIG = TEST_ASSETS_ROOT / "configs/software_fixing_duration.yaml" +ONE_ITEM_CONFIG = TEST_ASSETS_ROOT / "configs/fixing_duration_one_item.yaml" TestApplications = ["DummyApplication", "BroadcastTestClient"] diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 674603fa..31732f77 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -232,7 +232,7 @@ def test_database_service_fix(uc2_network): assert db_service.health_state_actual == SoftwareHealthState.FIXING # apply timestep until the fix is applied - for i in range(db_service.fixing_duration + 1): + for i in range(db_service.config.fixing_duration + 1): uc2_network.apply_timestep(i) assert db_service.db_file.health_status == FileSystemItemHealthStatus.GOOD @@ -266,7 +266,7 @@ def test_database_cannot_be_queried_while_fixing(uc2_network): assert db_connection.query(sql="SELECT") is False # apply timestep until the fix is applied - for i in range(db_service.fixing_duration + 1): + for i in range(db_service.config.fixing_duration + 1): uc2_network.apply_timestep(i) assert db_service.health_state_actual == SoftwareHealthState.GOOD @@ -308,7 +308,7 @@ def test_database_can_create_connection_while_fixing(uc2_network): assert new_db_connection.query(sql="SELECT") is False # still should fail to query because FIXING # apply timestep until the fix is applied - for i in range(db_service.fixing_duration + 1): + for i in range(db_service.config.fixing_duration + 1): uc2_network.apply_timestep(i) assert db_service.health_state_actual == SoftwareHealthState.GOOD diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 8fb6dc18..b53c02ac 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -131,7 +131,7 @@ def test_database_fix_disrupts_web_client(uc2_network): assert web_browser.get_webpage() is False - for i in range(database_service.fixing_duration + 1): + for i in range(database_service.config.fixing_duration + 1): uc2_network.apply_timestep(i) assert database_service.health_state_actual == SoftwareHealthState.GOOD diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py index ad6fe135..5598e1a7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py @@ -148,7 +148,7 @@ def test_service_fixing(service): service.fix() assert service.health_state_actual == SoftwareHealthState.FIXING - for i in range(service.fixing_duration + 1): + for i in range(service.config.fixing_duration + 1): service.apply_timestep(i) assert service.health_state_actual == SoftwareHealthState.GOOD From 0203a8699a9841044fcaa4bd5bc859dfb6d0e6d9 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 7 Jan 2025 14:21:07 +0000 Subject: [PATCH 114/224] #2888: Fixed C2Beacon test failures. --- .../system/applications/red_applications/c2/c2_beacon.py | 6 ------ .../system/red_applications/test_c2_suite_integration.py | 7 +++++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 449cc8d3..13918cd7 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -56,12 +56,6 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): def __init__(self, **kwargs): kwargs["name"] = "C2Beacon" super().__init__(**kwargs) - self.configure( - c2_server_ip_address=self.config.c2_server_ip_address, - keep_alive_frequency=self.config.keep_alive_frequency, - masquerade_port=self.config.masquerade_port, - masquerade_protocol=self.config.masquerade_protocol, - ) @property def _host_terminal(self) -> Optional[Terminal]: diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index d88f8249..6eab7361 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -495,6 +495,13 @@ def test_c2_suite_yaml(): computer_b: Computer = yaml_network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + c2_beacon.configure( + c2_server_ip_address=c2_beacon.config.c2_server_ip_address, + keep_alive_frequency=c2_beacon.config.keep_alive_frequency, + masquerade_port=c2_beacon.config.masquerade_port, + masquerade_protocol=c2_beacon.config.masquerade_protocol, + ) + assert c2_server.operating_state == ApplicationOperatingState.RUNNING From 7af9d3724f41bcb4fa8e9a015e6d233598768db9 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 8 Jan 2025 14:42:35 +0000 Subject: [PATCH 115/224] #2869 - Updates to address test failures. Updated YAML configs to remove redundant start_settings --- .../_package_data/data_manipulation.yaml | 7 ++-- .../_package_data/data_manipulation_marl.yaml | 7 ++-- .../scenario_with_placeholders/reds_1.yaml | 7 ++-- .../scenario_with_placeholders/reds_2.yaml | 7 ++-- src/primaite/game/agent/interface.py | 12 +++--- .../agent/scripted_agents/abstract_tap.py | 4 +- .../scripted_agents/data_manipulation_bot.py | 4 +- .../scripted_agents/probabilistic_agent.py | 16 +++----- src/primaite/game/game.py | 1 + src/primaite/session/environment.py | 2 +- .../assets/configs/bad_primaite_session.yaml | 14 +++---- tests/assets/configs/basic_firewall.yaml | 7 ++-- .../configs/basic_switched_network.yaml | 7 ++-- tests/assets/configs/data_manipulation.yaml | 7 ++-- tests/assets/configs/dmz_network.yaml | 7 ++-- .../configs/eval_only_primaite_session.yaml | 14 +++---- tests/assets/configs/extended_config.yaml | 7 ++-- .../configs/firewall_actions_network.yaml | 7 ++-- .../assets/configs/fix_duration_one_item.yaml | 7 ++-- tests/assets/configs/multi_agent_session.yaml | 7 ++-- .../scenario_with_placeholders/reds_1.yaml | 7 ++-- .../scenario_with_placeholders/reds_2.yaml | 7 ++-- tests/assets/configs/shared_rewards.yaml | 7 ++-- .../assets/configs/software_fix_duration.yaml | 7 ++-- .../configs/test_application_install.yaml | 7 ++-- .../assets/configs/test_primaite_session.yaml | 14 +++---- tests/conftest.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 39 +++++++++++-------- 28 files changed, 111 insertions(+), 130 deletions(-) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 97442903..d604192e 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -151,10 +151,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index ba666781..00a34403 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -150,10 +150,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender_1 team: BLUE diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml index 31675a0b..b775cb24 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml @@ -20,7 +20,6 @@ reds: &reds - type: DUMMY agent_settings: - start_settings: - start_step: 10 - frequency: 10 - variance: 0 + start_step: 10 + frequency: 10 + variance: 0 diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml index c5572b89..4cae1ec6 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml @@ -20,7 +20,6 @@ reds: &reds - type: DUMMY agent_settings: - start_settings: - start_step: 3 - frequency: 2 - variance: 1 + start_step: 3 + frequency: 2 + variance: 1 diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 6c1c633a..1627d360 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -47,13 +47,14 @@ class AbstractAgent(BaseModel): """Base class for scripted and RL agents.""" _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} - _logger: AgentLog = AgentLog(agent_name="Abstract_Agent") + logger: AgentLog = AgentLog(agent_name="Abstract_Agent") history: List[AgentHistoryItem] = [] config: "AbstractAgent.ConfigSchema" action_manager: "ActionManager" observation_manager: "ObservationManager" reward_function: "RewardFunction" + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) class ConfigSchema(BaseModel): """ @@ -114,11 +115,12 @@ class AbstractAgent(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": """Creates an agent component from a configuration dictionary.""" + print(config) obj = cls( config=cls.ConfigSchema(**config["agent_settings"]), - action_manager=ActionManager.from_config(**config["action_manager"]), - observation_manager=ObservationManager.from_config(**config["observation_space"]), - reward_function=RewardFunction.from_config(**config["reward_function"]), + action_manager=ActionManager.from_config(config["game"], config["action_manager"]), + observation_manager=ObservationManager.from_config(config["observation_manager"]), + reward_function=RewardFunction.from_config(config["reward_function"]), ) return obj @@ -140,7 +142,7 @@ class AbstractAgent(BaseModel): :return: Reward from the state. :rtype: float """ - return self.reward_function.update(state=state, last_action_response=self.config.history[-1]) + return self.reward_function.update(state=state, last_action_response=self.history[-1]) @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index 9fb782d4..f0dd096d 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -39,9 +39,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): :param timestep: The timestep to add variance to. """ - random_timestep_increment = random.randint( - -self.config.agent_settings.start_settings.variance, self.config.agent_settings.start_settings.variance - ) + random_timestep_increment = random.randint(-self.config.variance, self.config.variance) self.next_execution_timestep = timestep + random_timestep_increment def _select_start_node(self) -> None: diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 7ec119cf..f7bf4bc5 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -42,7 +42,7 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingA self.logger.debug(msg="Performing do nothing action") return "do_nothing", {} - self._set_next_execution_timestep(timestep + self.config.agent_settings.start_settings.frequency) + self._set_next_execution_timestep(timestep + self.config.frequency) self.logger.info(msg="Performing a data manipulation attack!") return "node_application_execute", { "node_name": self.config.starting_node_name, @@ -52,4 +52,4 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingA def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" self._select_start_node() - self._set_next_execution_timestep(self.config.agent_settings.start_settings.start_step) + self._set_next_execution_timestep(self.config.start_step) diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 159e5cd2..78f806d0 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -6,7 +6,7 @@ import numpy as np import pydantic from gymnasium.core import ObsType -from primaite.game.agent.interface import AbstractScriptedAgent, AgentSettings +from primaite.game.agent.interface import AbstractScriptedAgent __all__ = "ProbabilisticAgent" @@ -17,8 +17,10 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") config: "ProbabilisticAgent.ConfigSchema" rng: Any = np.random.default_rng(np.random.randint(0, 65535)) - class AgentSettings(AgentSettings): - """ProbabilisticAgent settings.""" + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration schema for Probabilistic Agent.""" + + agent_name: str = "ProbabilisticAgent" action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" @@ -42,16 +44,10 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") ) return v - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): - """Configuration schema for Probabilistic Agent.""" - - agent_name: str = "ProbabilisticAgent" - agent_settings: "ProbabilisticAgent.AgentSettings" - @property def probabilities(self) -> Dict[str, int]: """Convenience method to view the probabilities of the Agent.""" - return np.asarray(list(self.config.agent_settings.action_probabilities.values())) + return np.asarray(list(self.config.action_probabilities.values())) def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index db23eb14..69e294ae 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -538,6 +538,7 @@ class PrimaiteGame: "observation_manager": observation_space_cfg, "reward_function": reward_function_cfg, "agent_settings": agent_settings, + "game": game, } # CREATE AGENT diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 8e608ede..29f7c33d 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -89,7 +89,7 @@ class PrimaiteGymEnv(gymnasium.Env): :return: Action mask :rtype: List[bool] """ - if not self.agent.action_masking: + if not self.agent.config.action_masking: return np.asarray([True] * len(self.agent.action_manager.action_map)) else: return self.game.action_mask(self._agent_name) diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index c83cadc8..6a19c2fb 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -31,10 +31,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: data_manipulation_attacker team: RED @@ -63,10 +62,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index e37a67da..fe5e0099 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -56,10 +56,9 @@ agents: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_step: 5 + frequency: 4 + variance: 3 action_probabilities: 0: 0.4 1: 0.6 diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 00ba381b..f27735d1 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -59,10 +59,9 @@ agents: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_step: 5 + frequency: 4 + variance: 3 action_probabilities: 0: 0.6 1: 0.4 diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 97442903..d604192e 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -151,10 +151,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index d560efa3..41a530b0 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -81,10 +81,9 @@ agents: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_step: 5 + frequency: 4 + variance: 3 action_probabilities: 0: 0.4 1: 0.6 diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 3d60eb6e..dc0acdaa 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -35,10 +35,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: data_manipulation_attacker team: RED @@ -75,10 +74,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index e1a06938..fc1b72dd 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -151,10 +151,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 2292616d..2d42e4c5 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -266,10 +266,9 @@ agents: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_step: 5 + frequency: 4 + variance: 3 diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index 62579e35..0252ac32 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -56,10 +56,9 @@ agents: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_step: 5 + frequency: 4 + variance: 3 action_probabilities: 0: 0.4 1: 0.6 diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index a2d64605..13cffab1 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -150,10 +150,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender_1 team: BLUE diff --git a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml index 31675a0b..b775cb24 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml @@ -20,7 +20,6 @@ reds: &reds - type: DUMMY agent_settings: - start_settings: - start_step: 10 - frequency: 10 - variance: 0 + start_step: 10 + frequency: 10 + variance: 0 diff --git a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml index c5572b89..4cae1ec6 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml @@ -20,7 +20,6 @@ reds: &reds - type: DUMMY agent_settings: - start_settings: - start_step: 3 - frequency: 2 - variance: 1 + start_step: 3 + frequency: 2 + variance: 1 diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 81cb85f7..3ba480ea 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -146,10 +146,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 3e3d6e22..98260fe3 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -56,10 +56,9 @@ agents: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_step: 5 + frequency: 4 + variance: 3 action_probabilities: 0: 0.4 1: 0.6 diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index 3a3a6890..e8b080b7 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -150,10 +150,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index cf241f3c..c59bbcbf 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -43,10 +43,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 action_probabilities: 0: 1.0 @@ -86,10 +85,9 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/tests/conftest.py b/tests/conftest.py index 0c211f49..9d18a18b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from typing import Any, Dict, Tuple +from typing import Any, Dict, Optional, Tuple import pytest import yaml diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 4e2378b2..69540f0a 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -3,6 +3,7 @@ from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent +from primaite.game.game import PrimaiteGame def test_probabilistic_agent(): @@ -25,36 +26,41 @@ def test_probabilistic_agent(): MIN_NODE_FILE_DELETE = 5750 MAX_NODE_FILE_DELETE = 6250 - action_space = ActionManager( - actions=[ + action_space_cfg = { + "action_list": [ {"type": "DONOTHING"}, {"type": "NODE_APPLICATION_EXECUTE"}, {"type": "NODE_FILE_DELETE"}, ], - nodes=[ + "nodes": [ { "node_name": "client_1", "applications": [{"application_name": "WebBrowser"}], "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], }, ], - max_folders_per_node=2, - max_files_per_folder=2, - max_services_per_node=2, - max_applications_per_node=2, - max_nics_per_node=2, - max_acl_rules=10, - protocols=["TCP", "UDP", "ICMP"], - ports=["HTTP", "DNS", "ARP"], - act_map={ + "max_folders_per_node": 2, + "max_files_per_folder": 2, + "max_services_per_node": 2, + "max_applications_per_node": 2, + "max_nics_per_node": 2, + "max_acl_rules": 10, + "protocols": ["TCP", "UDP", "ICMP"], + "ports": ["HTTP", "DNS", "ARP"], + "act_map": { 0: {"action": "DONOTHING", "options": {}}, 1: {"action": "NODE_APPLICATION_EXECUTE", "options": {"node_id": 0, "application_id": 0}}, 2: {"action": "NODE_FILE_DELETE", "options": {"node_id": 0, "folder_id": 0, "file_id": 0}}, }, - ) + "options": {}, + } observation_space = ObservationManager(NestedObservation(components={})) reward_function = RewardFunction() + observation_space_cfg = None + + reward_function_cfg = {} + # pa = ProbabilisticAgent( # agent_name="test_agent", # action_space=action_space, @@ -67,9 +73,10 @@ def test_probabilistic_agent(): pa_config = { "agent_name": "test_agent", - "action_manager": action_space, - "observation_manager": observation_space, - "reward_function": reward_function, + "game": PrimaiteGame(), + "action_manager": action_space_cfg, + "observation_manager": observation_space_cfg, + "reward_function": reward_function_cfg, "agent_settings": { "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, }, From e3f4775acb2961d8265cd27b0bb2a04252011abc Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 10 Jan 2025 14:09:15 +0000 Subject: [PATCH 116/224] #2869 - Updates to ConfigSchema declaration and addressing some review comments --- .../how_to_guides/extensible_agents.rst | 37 +++++---- src/primaite/game/agent/interface.py | 1 - .../agent/scripted_agents/abstract_tap.py | 11 +-- .../scripted_agents/data_manipulation_bot.py | 3 +- .../scripted_agents/probabilistic_agent.py | 5 +- .../agent/scripted_agents/random_agent.py | 29 ++----- .../game/agent/scripted_agents/tap001.py | 78 ------------------- ...a-Manipulation-Customising-Red-Agent.ipynb | 2 +- .../configs/basic_switched_network.yaml | 4 +- 9 files changed, 44 insertions(+), 126 deletions(-) delete mode 100644 src/primaite/game/agent/scripted_agents/tap001.py diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index b9e00b60..c653bd05 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -1,6 +1,6 @@ .. only:: comment - © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK .. _about: @@ -15,7 +15,12 @@ Developing Agents for PrimAITE Agents within PrimAITE, follow the shown inheritance structure below. +The inheritance structure of agents within PrimAITE are shown below. When developing custom agents for use with PrimAITE, please see the relevant documentation for each agent type to determine which is most relevant for your implementation. + +All agent types within PrimAITE are listed under the ``_registry`` attribute of the parent class, ``AbstractAgent``. + # TODO: Turn this into an inheritance diagram +# TODO: Would this be necessary? AbstractAgent | @@ -40,12 +45,12 @@ AbstractAgent #. **ConfigSchema**: Configurable items within a new agent within PrimAITE should contain a ``ConfigSchema`` which holds all configurable variables of the agent. This should not include parameters related to its *state*. - Agent generation will fail if incorrect parameters are passed to the ConfigSchema, for the chosen Agent. + Agent generation will fail if incorrect or invalid parameters are passed to the ConfigSchema, of the chosen Agent. .. code-block:: python - class ExampleAgent(AbstractAgent, identifier = "example_agent"): + class ExampleAgent(AbstractAgent, identifier = "ExampleAgent"): """An example agent for demonstration purposes.""" config: "ExampleAgent.ConfigSchema" @@ -56,10 +61,10 @@ AbstractAgent class ConfigSchema(AbstractAgent.ConfigSchema): """ExampleAgent configuration schema""" - agent_name: str + agent_name: str = "ExampleAgent """Name of agent""" - action_interval: int - """Number of steps between agent actions""" + starting_host: int + """Host node that this agent should start from in the given environment.""" .. code-block:: YAML @@ -89,22 +94,24 @@ AbstractAgent - type: DUMMY agent_settings: - start_settings: - start_step: 25 - frequency: 20 - variance: 5 + start_step: 25 + frequency: 20 + variance: 5 + agent_name: "Example Agent" + starting_host: "Server_1" -#. **identifier**: +#. **Identifiers**: - All agent classes should have a ``identifier`` attribute, a unique snake_case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. + All agent classes should have a ``identifier`` attribute, a unique kebab-case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. Changes to YAML file ==================== -Agent configurations specified within YAML files used for earlier versions of PrimAITE will need updating to be compatible with PrimAITE v4.0.0+. - -Agents now follow a more standardised settings definition, so should be more consistent across YAML. +PrimAITE v4.0.0 introduces some breaking changes to how environment configuration yaml files are created. YAML files created for Primaite versions 3.3.0 should be compatible through a translation function, though it is encouraged that these are updated to reflect the updated format of 4.0.0+. +Agents now follow a more standardised settings definition, so should be more consistent across YAML files and the available agent types with PrimAITE. # TODO: Show changes to YAML config needed here + +All configurable items for agents sit under the ``agent_settings`` heading within your YAML files. There is no need for the inclusion of a ``start_settings``. diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 1627d360..794ce511 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -115,7 +115,6 @@ class AbstractAgent(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": """Creates an agent component from a configuration dictionary.""" - print(config) obj = cls( config=cls.ConfigSchema(**config["agent_settings"]), action_manager=ActionManager.from_config(config["game"], config["action_manager"]), diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index f0dd096d..2c0101f8 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -6,16 +6,17 @@ from abc import abstractmethod from typing import Dict, Optional, Tuple from gymnasium.core import ObsType +from pydantic import Field from primaite.game.agent.interface import AbstractScriptedAgent __all__ = "AbstractTAPAgent" -class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): +class AbstractTAPAgent(AbstractScriptedAgent, identifier="AbstractTAP"): """Base class for TAP agents to inherit from.""" - config: "AbstractTAPAgent.ConfigSchema" + config: "AbstractTAPAgent.ConfigSchema" = Field(default_factory=lambda: AbstractTAPAgent.ConfigSchema()) agent_name: str = "Abstract_TAP" next_execution_timestep: int = 0 @@ -45,7 +46,7 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="Abstract_TAP"): def _select_start_node(self) -> None: """Set the starting starting node of the agent to be a random node from this agent's action manager.""" # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.config.action_manager.node_names) + num_nodes = len(self.action_manager.node_names) starting_node_idx = random.randint(0, num_nodes - 1) - self.starting_node_name = self.config.action_manager.node_names[starting_node_idx] - self.logger.debug(f"Selected starting node: {self.starting_node_name}") + self.config.starting_node_name = self.action_manager.node_names[starting_node_idx] + self.logger.debug(f"Selected starting node: {self.config.starting_node_name}") diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index f7bf4bc5..66c744aa 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -2,6 +2,7 @@ from typing import Dict, Optional, Tuple from gymnasium.core import ObsType +from pydantic import Field from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent @@ -11,7 +12,7 @@ __all__ = "DataManipulationAgent" class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" - config: "DataManipulationAgent.ConfigSchema" + config: "DataManipulationAgent.ConfigSchema" = Field(default_factory=lambda: DataManipulationAgent.ConfigSchema()) agent_name: str = "Data_Manipulation_Agent" class ConfigSchema(AbstractTAPAgent.ConfigSchema): diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 78f806d0..455c996b 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Tuple import numpy as np import pydantic from gymnasium.core import ObsType +from pydantic import Field from primaite.game.agent.interface import AbstractScriptedAgent @@ -14,7 +15,7 @@ __all__ = "ProbabilisticAgent" class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" - config: "ProbabilisticAgent.ConfigSchema" + config: "ProbabilisticAgent.ConfigSchema" = Field(default_factory=lambda: ProbabilisticAgent.ConfigSchema()) rng: Any = np.random.default_rng(np.random.randint(0, 65535)) class ConfigSchema(AbstractScriptedAgent.ConfigSchema): @@ -22,7 +23,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") agent_name: str = "ProbabilisticAgent" - action_probabilities: Dict[int, float] + action_probabilities: Dict[int, float] = None """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" @pydantic.field_validator("action_probabilities", mode="after") diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 14f642ef..f8681cee 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -3,15 +3,18 @@ import random from typing import Dict, Tuple from gymnasium.core import ObsType +from pydantic import Field from primaite.game.agent.interface import AbstractScriptedAgent __all__ = ("RandomAgent", "PeriodicAgent") -class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): +class RandomAgent(AbstractScriptedAgent, identifier="RandomAgent"): """Agent that ignores its observation and acts completely at random.""" + config: "RandomAgent.ConfigSchema" = Field(default_factory=lambda: RandomAgent.ConfigSchema()) + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Random Agents.""" @@ -30,10 +33,10 @@ class RandomAgent(AbstractScriptedAgent, identifier="Random_Agent"): return self.action_manager.get_action(self.action_manager.space.sample()) -class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): +class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): """Agent that does nothing most of the time, but executes application at regular intervals (with variance).""" - config: "PeriodicAgent.ConfigSchema" = {} + config: "PeriodicAgent.ConfigSchema" = Field(default_factory=lambda: PeriodicAgent.ConfigSchema()) class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" @@ -45,25 +48,9 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): "Maximum number of times the agent can execute its action." num_executions: int = 0 """Number of times the agent has executed an action.""" - # TODO: Also in abstract_tap - move up and inherit? Add to AgentStartSettings? next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" - @property - def start_step(self) -> int: - """Return the timestep at which an agent begins performing it's actions.""" - return self.config.agent_settings.start_settings.start_step - - @property - def start_variance(self) -> int: - """Returns the deviation around the start step.""" - return self.config.agent_settings.start_settings.variance - - @property - def frequency(self) -> int: - """Returns the number of timesteps to wait between performing actions.""" - return self.config.agent_settings.start_settings.frequency - def _set_next_execution_timestep(self, timestep: int, variance: int) -> None: """Set the next execution timestep with a configured random variance. @@ -79,8 +66,8 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="Periodic_Agent"): """Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" if timestep == self.next_execution_timestep and self.num_executions < self.max_executions: self.num_executions += 1 - self._set_next_execution_timestep(timestep + self.frequency, self.start_variance) + self._set_next_execution_timestep(timestep + self.config.frequency, self.config.variance) self.target_node = self.action_manager.node_names[0] return "node_application_execute", {"node_name": self.target_node, "application_name": 0} - return "DONOTHING", {} + return "do_nothing", {} diff --git a/src/primaite/game/agent/scripted_agents/tap001.py b/src/primaite/game/agent/scripted_agents/tap001.py deleted file mode 100644 index e9694a45..00000000 --- a/src/primaite/game/agent/scripted_agents/tap001.py +++ /dev/null @@ -1,78 +0,0 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -import random -from typing import Dict, Tuple - -from gymnasium.core import ObsType - -from primaite.game.agent.interface import AbstractScriptedAgent - - -class TAP001(AbstractScriptedAgent): - """ - TAP001 | Mobile Malware -- Ransomware Variant. - - Scripted Red Agent. Capable of one action; launching the kill-chain (Ransomware Application) - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setup_agent() - - next_execution_timestep: int = 0 - starting_node_idx: int = 0 - installed: bool = False - - def _set_next_execution_timestep(self, timestep: int) -> None: - """Set the next execution timestep with a configured random variance. - - :param timestep: The timestep to add variance to. - """ - random_timestep_increment = random.randint( - -self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance - ) - self.next_execution_timestep = timestep + random_timestep_increment - - def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: - """Waits until a specific timestep, then attempts to execute the ransomware application. - - This application acts a wrapper around the kill-chain, similar to green-analyst and - the previous UC2 data manipulation bot. - - :param obs: Current observation for this agent. - :type obs: ObsType - :param timestep: The current simulation timestep, used for scheduling actions - :type timestep: int - :return: Action formatted in CAOS format - :rtype: Tuple[str, Dict] - """ - if timestep < self.next_execution_timestep: - return "DONOTHING", {} - - self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) - - if not self.installed: - self.installed = True - return "NODE_APPLICATION_INSTALL", { - "node_id": self.starting_node_idx, - "application_name": "RansomwareScript", - } - - return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} - - def setup_agent(self) -> None: - """Set the next execution timestep when the episode resets.""" - self._select_start_node() - self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) - for n, act in self.action_manager.action_map.items(): - if not act[0] == "NODE_APPLICATION_INSTALL": - continue - if act[1]["node_id"] == self.starting_node_idx: - self.ip_address = act[1]["ip_address"] - return - raise RuntimeError("TAP001 agent could not find database server ip address in action map") - - def _select_start_node(self) -> None: - """Set the starting starting node of the agent to be a random node from this agent's action manager.""" - # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.action_manager.node_names) - self.starting_node_idx = random.randint(0, num_nodes - 1) diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index dd5def9e..07881131 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -444,7 +444,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index f27735d1..42400253 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -63,8 +63,8 @@ agents: frequency: 4 variance: 3 action_probabilities: - 0: 0.6 - 1: 0.4 + 0: 0.4 + 1: 0.6 From c16abdfd306a4c051930fffa7fa7effeb06a20d0 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 10 Jan 2025 14:39:03 +0000 Subject: [PATCH 117/224] #2869 - Remove agent_name from ConfigSchema and replace with type --- src/primaite/game/agent/scripted_agents/abstract_tap.py | 2 +- .../game/agent/scripted_agents/data_manipulation_bot.py | 2 +- src/primaite/game/agent/scripted_agents/random_agent.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index 2c0101f8..21323578 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -17,12 +17,12 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="AbstractTAP"): """Base class for TAP agents to inherit from.""" config: "AbstractTAPAgent.ConfigSchema" = Field(default_factory=lambda: AbstractTAPAgent.ConfigSchema()) - agent_name: str = "Abstract_TAP" next_execution_timestep: int = 0 class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" + type: str = "AbstractTAP" starting_node_name: Optional[str] = None @abstractmethod diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 66c744aa..b9d57a8b 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -13,11 +13,11 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingA """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" config: "DataManipulationAgent.ConfigSchema" = Field(default_factory=lambda: DataManipulationAgent.ConfigSchema()) - agent_name: str = "Data_Manipulation_Agent" class ConfigSchema(AbstractTAPAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" + type: str = "RedDatabaseCorruptingAgent" starting_application_name: Optional[str] = None @property diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index f8681cee..daf810a8 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -18,7 +18,7 @@ class RandomAgent(AbstractScriptedAgent, identifier="RandomAgent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Random Agents.""" - agent_name: str = "Random_Agent" + type: str = "RandomAgent" def get_action(self) -> Tuple[str, Dict]: """Sample the action space randomly. @@ -41,7 +41,7 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" - agent_name: str = "Periodic_Agent" + type: str = "PeriodicAgent" """Name of the agent.""" max_executions: int = 999999 From 511abea59c427338826024126cad0b9be0138f22 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 10:26:48 +0000 Subject: [PATCH 118/224] #2869 - Actioning review comments --- docs/source/how_to_guides/extensible_agents.rst | 4 +--- src/primaite/game/agent/agent_log.py | 5 +++-- src/primaite/game/agent/interface.py | 2 ++ .../game/agent/scripted_agents/probabilistic_agent.py | 2 +- .../_primaite/_game/_agent/test_probabilistic_agent.py | 10 ---------- 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index c653bd05..2dc70ca6 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -20,7 +20,6 @@ The inheritance structure of agents within PrimAITE are shown below. When develo All agent types within PrimAITE are listed under the ``_registry`` attribute of the parent class, ``AbstractAgent``. # TODO: Turn this into an inheritance diagram -# TODO: Would this be necessary? AbstractAgent | @@ -61,7 +60,7 @@ AbstractAgent class ConfigSchema(AbstractAgent.ConfigSchema): """ExampleAgent configuration schema""" - agent_name: str = "ExampleAgent + type: str = "ExampleAgent """Name of agent""" starting_host: int """Host node that this agent should start from in the given environment.""" @@ -97,7 +96,6 @@ AbstractAgent start_step: 25 frequency: 20 variance: 5 - agent_name: "Example Agent" starting_host: "Server_1" diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 98c6a337..31d74176 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -21,9 +21,10 @@ class _NotJSONFilter(logging.Filter): class AgentLog: """ - A Agent Log class is a simple logger dedicated to managing and writing logging updates and information for an agent. + An Agent Log class is a simple logger dedicated to managing and writing updates and information for an agent. - Each log message is written to a file located at: /agent_name/agent_name.log + Each log message is written to a file located at: + /agent_name/agent_name.log """ def __init__(self, agent_name: Optional[str]): diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 794ce511..370e6bbb 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -115,6 +115,8 @@ class AbstractAgent(BaseModel): @classmethod def from_config(cls, config: Dict) -> "AbstractAgent": """Creates an agent component from a configuration dictionary.""" + if config["type"] not in cls._registry: + return ValueError(f"Invalid Agent Type: {config['type']}") obj = cls( config=cls.ConfigSchema(**config["agent_settings"]), action_manager=ActionManager.from_config(config["game"], config["action_manager"]), diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 455c996b..f3d9ee08 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -21,7 +21,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Probabilistic Agent.""" - agent_name: str = "ProbabilisticAgent" + type: str = "ProbabilisticAgent" action_probabilities: Dict[int, float] = None """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 69540f0a..6e5fb94d 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -61,16 +61,6 @@ def test_probabilistic_agent(): reward_function_cfg = {} - # pa = ProbabilisticAgent( - # agent_name="test_agent", - # action_space=action_space, - # observation_space=observation_space, - # reward_function=reward_function, - # settings={ - # "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, - # }, - # ) - pa_config = { "agent_name": "test_agent", "game": PrimaiteGame(), From 32fc970cfed731787ec74f18d3c71f811a4b9f2b Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 10:51:30 +0000 Subject: [PATCH 119/224] #2869 - Update Config for some agent classes to use pydantic.Field, amend some identifiers and agent_name variables --- src/primaite/game/agent/interface.py | 18 ++++++++---------- src/primaite/game/game.py | 2 +- tests/conftest.py | 7 ++++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 370e6bbb..26445830 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -6,7 +6,7 @@ from abc import abstractmethod from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING from gymnasium.core import ActType, ObsType -from pydantic import BaseModel, ConfigDict, model_validator +from pydantic import BaseModel, ConfigDict, Field, model_validator from primaite.game.agent.actions import ActionManager from primaite.game.agent.agent_log import AgentLog @@ -50,7 +50,7 @@ class AbstractAgent(BaseModel): logger: AgentLog = AgentLog(agent_name="Abstract_Agent") history: List[AgentHistoryItem] = [] - config: "AbstractAgent.ConfigSchema" + config: "AbstractAgent.ConfigSchema" = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) action_manager: "ActionManager" observation_manager: "ObservationManager" reward_function: "RewardFunction" @@ -62,8 +62,6 @@ class AbstractAgent(BaseModel): :param type: Type of agent being generated. :type type: str - :param agent_name: Unique string identifier for the agent, for reporting and multi-agent purposes. - :type agent_name: str :param observation_space: Observation space for the agent. :type observation_space: Optional[ObservationSpace] :param reward_function: Reward function for the agent. @@ -73,7 +71,7 @@ class AbstractAgent(BaseModel): """ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - agent_name: str = "Abstract_Agent" + type: str = "AbstractAgent" flatten_obs: bool = True "Whether to flatten the observation space before passing it to the agent. True by default." action_masking: bool = False @@ -185,15 +183,15 @@ class AbstractAgent(BaseModel): self.history[-1].reward = self.reward_function.current_reward -class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent"): +class AbstractScriptedAgent(AbstractAgent, identifier="AbstractScriptedAgent"): """Base class for actors which generate their own behaviour.""" - config: "AbstractScriptedAgent.ConfigSchema" + config: "AbstractScriptedAgent.ConfigSchema" = Field(default_factory=lambda: AbstractScriptedAgent.ConfigSchema()) class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for AbstractScriptedAgents.""" - agent_name: str = "Abstract_Scripted_Agent" + type: str = "AbstractScriptedAgent" @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -204,13 +202,13 @@ class AbstractScriptedAgent(AbstractAgent, identifier="Abstract_Scripted_Agent") class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): """Agent that sends observations to an RL model and receives actions from that model.""" - config: "ProxyAgent.ConfigSchema" + config: "ProxyAgent.ConfigSchema" = Field(default_factory=lambda: ProxyAgent.ConfigSchema()) most_recent_action: ActType = None class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Proxy Agent.""" - agent_name: str = "Proxy_Agent" + type: str = "Proxy_Agent" flatten_obs: bool = False action_masking: bool = False diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 69e294ae..f2b1de4c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -533,7 +533,7 @@ class PrimaiteGame: agent_settings = agent_cfg["agent_settings"] agent_config = { - "agent_name": agent_name, + "type": agent_type, "action_manager": action_space_cfg, "observation_manager": observation_space_cfg, "reward_function": reward_function_cfg, diff --git a/tests/conftest.py b/tests/conftest.py index 9d18a18b..b4b72e55 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Tuple import pytest import yaml +from pydantic import Field from ray import init as rayinit from primaite import getLogger, PRIMAITE_PATHS @@ -265,16 +266,16 @@ def example_network() -> Network: return network -class ControlledAgent(AbstractAgent, identifier="Controlled_Agent"): +class ControlledAgent(AbstractAgent, identifier="ControlledAgent"): """Agent that can be controlled by the tests.""" - config: "ControlledAgent.ConfigSchema" + config: "ControlledAgent.ConfigSchema" = Field(default_factory=lambda: ControlledAgent.ConfigSchema()) most_recent_action: Optional[Tuple[str, Dict]] = None class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Abstract Agent used in tests.""" - agent_name: str = "Controlled_Agent" + type: str = "ControlledAgent" def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: """Return the agent's most recent action, formatted in CAOS format.""" From edd2668ea4f7b53d5e418f1db7d9cc0edf0ac054 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 15:08:48 +0000 Subject: [PATCH 120/224] #2869 - Update type hints and ConfigSchema variables in some agent classes --- src/primaite/game/agent/interface.py | 28 +------------------ .../scripted_agents/probabilistic_agent.py | 5 ++-- .../agent/scripted_agents/random_agent.py | 24 +++++++++++++++- .../_game/_agent/test_probabilistic_agent.py | 11 ++++---- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 26445830..ac76a425 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -6,7 +6,7 @@ from abc import abstractmethod from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING from gymnasium.core import ActType, ObsType -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field from primaite.game.agent.actions import ActionManager from primaite.game.agent.agent_log import AgentLog @@ -72,32 +72,6 @@ class AbstractAgent(BaseModel): model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) type: str = "AbstractAgent" - flatten_obs: bool = True - "Whether to flatten the observation space before passing it to the agent. True by default." - action_masking: bool = False - "Whether to return action masks at each step." - start_step: int = 5 - "The timestep at which an agent begins performing it's actions" - frequency: int = 5 - "The number of timesteps to wait between performing actions" - variance: int = 0 - "The amount the frequency can randomly change to" - - @model_validator(mode="after") - def check_variance_lt_frequency(self) -> "AbstractAgent.ConfigSchema": - """ - Make sure variance is equal to or lower than frequency. - - This is because the calculation for the next execution time is now + (frequency +- variance). - If variance were greater than frequency, sometimes the bracketed term would be negative - and the attack would never happen again. - """ - if self.variance > self.frequency: - raise ValueError( - f"Agent start settings error: variance must be lower than frequency " - f"{self.variance=}, {self.frequency=}" - ) - return self def __init_subclass__(cls, identifier: str, **kwargs: Any) -> None: if identifier in cls._registry: diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index f3d9ee08..8e714f55 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -1,10 +1,11 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Agents with predefined behaviours.""" -from typing import Any, Dict, Tuple +from typing import Dict, Tuple import numpy as np import pydantic from gymnasium.core import ObsType +from numpy.random import Generator from pydantic import Field from primaite.game.agent.interface import AbstractScriptedAgent @@ -16,7 +17,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" config: "ProbabilisticAgent.ConfigSchema" = Field(default_factory=lambda: ProbabilisticAgent.ConfigSchema()) - rng: Any = np.random.default_rng(np.random.randint(0, 65535)) + rng: Generator = np.random.default_rng(np.random.randint(0, 65535)) class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Probabilistic Agent.""" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index daf810a8..b5601a58 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -3,7 +3,7 @@ import random from typing import Dict, Tuple from gymnasium.core import ObsType -from pydantic import Field +from pydantic import Field, model_validator from primaite.game.agent.interface import AbstractScriptedAgent @@ -43,6 +43,28 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): type: str = "PeriodicAgent" """Name of the agent.""" + start_step: int = 5 + "The timestep at which an agent begins performing it's actions" + frequency: int = 5 + "The number of timesteps to wait between performing actions" + variance: int = 0 + "The amount the frequency can randomly change to" + + @model_validator(mode="after") + def check_variance_lt_frequency(self) -> "PeriodicAgent.ConfigSchema": + """ + Make sure variance is equal to or lower than frequency. + + This is because the calculation for the next execution time is now + (frequency +- variance). + If variance were greater than frequency, sometimes the bracketed term would be negative + and the attack would never happen again. + """ + if self.variance > self.frequency: + raise ValueError( + f"Agent start settings error: variance must be lower than frequency " + f"{self.variance=}, {self.frequency=}" + ) + return self max_executions: int = 999999 "Maximum number of times the agent can execute its action." diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 6e5fb94d..7035e98f 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -3,7 +3,7 @@ from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations.observation_manager import NestedObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent -from primaite.game.game import PrimaiteGame +from primaite.game.game import PrimaiteGame, PrimaiteGameOptions def test_probabilistic_agent(): @@ -54,16 +54,17 @@ def test_probabilistic_agent(): }, "options": {}, } - observation_space = ObservationManager(NestedObservation(components={})) - reward_function = RewardFunction() + + game = PrimaiteGame() + game.options = PrimaiteGameOptions(ports=[], protocols=[]) observation_space_cfg = None reward_function_cfg = {} pa_config = { - "agent_name": "test_agent", - "game": PrimaiteGame(), + "type": "ProbabilisticAgent", + "game": game, "action_manager": action_space_cfg, "observation_manager": observation_space_cfg, "reward_function": reward_function_cfg, From ea9c13b5f4e361e5e2edf5444e83fb1695626d9c Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 13 Jan 2025 15:38:11 +0000 Subject: [PATCH 121/224] #2888: self.X -> self.config.X --- src/primaite/simulator/system/applications/web_browser.py | 5 +---- .../game_layer/observations/test_nic_observations.py | 2 +- tests/integration_tests/game_layer/test_actions.py | 8 ++++---- tests/integration_tests/game_layer/test_rewards.py | 2 +- tests/integration_tests/system/test_web_client_server.py | 8 ++++---- .../system/test_web_client_server_and_database.py | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ad20640f..49f303b5 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -38,8 +38,6 @@ class WebBrowser(Application, identifier="WebBrowser"): config: "WebBrowser.ConfigSchema" = Field(default_factory=lambda: WebBrowser.ConfigSchema()) - target_url: Optional[str] = None - domain_name_ip_address: Optional[IPv4Address] = None "The IP address of the domain name for the webpage." @@ -57,7 +55,6 @@ class WebBrowser(Application, identifier="WebBrowser"): kwargs["port"] = PORT_LOOKUP["HTTP"] super().__init__(**kwargs) - self.target_url = self.config.target_url self.run() def _init_request_manager(self) -> RequestManager: @@ -95,7 +92,7 @@ class WebBrowser(Application, identifier="WebBrowser"): :param: url: The address of the web page the browser requests :type: url: str """ - url = url or self.target_url + url = url or self.config.target_url if not self._can_perform_action(): return False diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 0ad03198..bd9417ba 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -191,7 +191,7 @@ def test_nic_monitored_traffic(simulation): # send a database query browser: WebBrowser = pc.software_manager.software.get("WebBrowser") - browser.target_url = f"http://arcd.com/" + browser.config.target_url = f"http://arcd.com/" browser.get_webpage() traffic_obs = nic_obs.observe(simulation.describe_state()).get("TRAFFIC") diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index beb7b6a8..b6176c59 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -181,7 +181,7 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() - browser.target_url = "http://www.example.com" + browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com before we block it # 2: Remove rule that allows HTTP traffic across the network @@ -214,7 +214,7 @@ def test_host_nic_disable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() - browser.target_url = "http://www.example.com" + browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com before we block it # 2: Disable the NIC on client_1 @@ -413,7 +413,7 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() - browser.target_url = "http://www.example.com" + browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com before we block it # 2: Disable the NIC on client_1 @@ -473,7 +473,7 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() - browser.target_url = "http://www.example.com" + browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com assert browser.health_state_actual == SoftwareHealthState.GOOD diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index dc7ed132..a674d864 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -27,7 +27,7 @@ def test_WebpageUnavailablePenalty(game_and_agent): client_1 = game.simulation.network.get_node_by_hostname("client_1") browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") browser.run() - browser.target_url = "http://www.example.com" + browser.config.target_url = "http://www.example.com" agent.reward_function.register_component(comp, 0.7) # Check that before trying to fetch the webpage, the reward is 0.0 diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index c1028e8e..8aea34c1 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -51,7 +51,7 @@ def test_web_page_get_users_page_request_with_domain_name(web_client_and_web_ser web_browser_app, computer, web_server_service, server = web_client_and_web_server web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address - web_browser_app.target_url = f"http://arcd.com/" + web_browser_app.config.target_url = f"http://arcd.com/" assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING assert web_browser_app.get_webpage() is True @@ -66,7 +66,7 @@ def test_web_page_get_users_page_request_with_ip_address(web_client_and_web_serv web_browser_app, computer, web_server_service, server = web_client_and_web_server web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address - web_browser_app.target_url = f"http://{web_server_ip}/" + web_browser_app.config.target_url = f"http://{web_server_ip}/" assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING assert web_browser_app.get_webpage() is True @@ -81,7 +81,7 @@ def test_web_page_request_from_shut_down_server(web_client_and_web_server): web_browser_app, computer, web_server_service, server = web_client_and_web_server web_server_ip = server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address - web_browser_app.target_url = f"http://arcd.com/" + web_browser_app.config.target_url = f"http://arcd.com/" assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING assert web_browser_app.get_webpage() is True @@ -108,7 +108,7 @@ def test_web_page_request_from_closed_web_browser(web_client_and_web_server): web_browser_app, computer, web_server_service, server = web_client_and_web_server assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING - web_browser_app.target_url = f"http://arcd.com/" + web_browser_app.config.target_url = f"http://arcd.com/" assert web_browser_app.get_webpage() is True # latest response should have status code 200 diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index b53c02ac..41f1a837 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -74,7 +74,7 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer, # Install Web Browser on computer computer.software_manager.install(WebBrowser) web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") - web_browser.target_url = "http://arcd.com/users/" + web_browser.config.target_url = "http://arcd.com/users/" web_browser.run() # Install DNS Client service on computer From 23736f77388a6bf390776964a3c28e4f3f59cfc2 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 15:59:10 +0000 Subject: [PATCH 122/224] #2869 - Documentation changes --- .../how_to_guides/extensible_agents.rst | 40 ++++--------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 2dc70ca6..5bbca13a 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -7,44 +7,20 @@ Extensible Agents ***************** -Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents. +Agents defined within PrimAITE have been updated to allow for easier creation of new bespoke agents for use in custom environments. Developing Agents for PrimAITE ============================== -Agents within PrimAITE, follow the shown inheritance structure below. - -The inheritance structure of agents within PrimAITE are shown below. When developing custom agents for use with PrimAITE, please see the relevant documentation for each agent type to determine which is most relevant for your implementation. - -All agent types within PrimAITE are listed under the ``_registry`` attribute of the parent class, ``AbstractAgent``. - -# TODO: Turn this into an inheritance diagram - -AbstractAgent - | - | - AbstractScriptedAgent - | | - | | - AbstractTAPAgent - | | | - | | | - DataManipulationAgent - | | - | | - | | - RandomAgent - | | - | | - PeriodicAgent - | | - | | - RandomAgent - | - | - ProxyAgent - | - | - ControlledAgent +All agent types within PrimAITE must be subclassed from ``AbstractAgent`` in order to be used from configuration YAML files. This then allows you to implement any custom agent logic for the new agent in your training scenario. Examples of implementing custom agent logic can be seen in pre-existing agents, such as the ``DataManipulationBot`` and ``RandomAgent``. +The core features that should be implemented in any new agent are detailed below: #. **ConfigSchema**: - Configurable items within a new agent within PrimAITE should contain a ``ConfigSchema`` which holds all configurable variables of the agent. This should not include parameters related to its *state*. - Agent generation will fail if incorrect or invalid parameters are passed to the ConfigSchema, of the chosen Agent. + Configurable items within a new agent within PrimAITE should contain a ``ConfigSchema`` which holds all configurable variables of the agent. This should not include parameters related to its *state*, these would be listed seperately. + Agent generation will fail pydantic checks if incorrect or invalid parameters are passed to the ConfigSchema of the chosen Agent. .. code-block:: python @@ -52,7 +28,7 @@ AbstractAgent class ExampleAgent(AbstractAgent, identifier = "ExampleAgent"): """An example agent for demonstration purposes.""" - config: "ExampleAgent.ConfigSchema" + config: "ExampleAgent.ConfigSchema" = Field(default_factory= lambda: ExampleAgent.ConfigSchema()) """Agent configuration""" num_executions: int = 0 """Number of action executions by agent""" @@ -110,6 +86,4 @@ PrimAITE v4.0.0 introduces some breaking changes to how environment configuratio Agents now follow a more standardised settings definition, so should be more consistent across YAML files and the available agent types with PrimAITE. -# TODO: Show changes to YAML config needed here - -All configurable items for agents sit under the ``agent_settings`` heading within your YAML files. There is no need for the inclusion of a ``start_settings``. +All configurable items for agents sit under the ``agent_settings`` heading within your YAML files. There is no need for the inclusion of a ``start_settings``. Please see the above YAML example for full changes to agents. From 3cca3d4a5ccfce7d6db40acc2bd2795ce5ca7e81 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 16:12:16 +0000 Subject: [PATCH 123/224] #2912 - Actioning review comments. Identifiers have been removed from AbstractActions, _legacy folder has been deleted and correction to IPV4Address type hints --- src/primaite/_legacy/actions.py | 1250 ----------------- src/primaite/game/agent/actions/abstract.py | 2 +- src/primaite/game/agent/actions/acl.py | 10 +- .../game/agent/actions/application.py | 3 +- src/primaite/game/agent/actions/file.py | 3 +- src/primaite/game/agent/actions/folder.py | 3 +- src/primaite/game/agent/actions/host_nic.py | 3 +- src/primaite/game/agent/actions/manager.py | 3 - .../simulator/system/core/software_manager.py | 4 - src/primaite/utils/validation/ipv4_address.py | 2 +- .../_primaite/_game/_agent/test_actions.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 2 +- 12 files changed, 17 insertions(+), 1270 deletions(-) delete mode 100644 src/primaite/_legacy/actions.py diff --git a/src/primaite/_legacy/actions.py b/src/primaite/_legacy/actions.py deleted file mode 100644 index d2457a20..00000000 --- a/src/primaite/_legacy/actions.py +++ /dev/null @@ -1,1250 +0,0 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -""" -This module contains the ActionManager class which belongs to the Agent class. - -An agent's action space is made up of a collection of actions. Each action is an instance of a subclass of -AbstractAction. The ActionManager is responsible for: - 1. Creating the action space from a list of action types. - 2. Converting an integer action choice into a specific action and parameter choice. - 3. Converting an action and parameter choice into a request which can be ingested by the PrimAITE simulation. This - ensures that requests conform to the simulator's request format. -""" -from abc import abstractmethod -from typing import Dict, List, Literal, Optional, Union - -from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationInfo - -from primaite import getLogger -from primaite.game.agent.actions.manager import AbstractAction, ActionManager -from primaite.game.agent.actions.service import NodeServiceAbstractAction -from primaite.interface.request import RequestFormat - -_LOGGER = getLogger(__name__) - - -class NodeServiceScanAction(NodeServiceAbstractAction): - """Action which scans a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "scan" - - -class NodeServiceStopAction(NodeServiceAbstractAction): - """Action which stops a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "stop" - - -class NodeServiceStartAction(NodeServiceAbstractAction): - """Action which starts a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "start" - - -class NodeServicePauseAction(NodeServiceAbstractAction): - """Action which pauses a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "pause" - - -class NodeServiceResumeAction(NodeServiceAbstractAction): - """Action which resumes a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "resume" - - -class NodeServiceRestartAction(NodeServiceAbstractAction): - """Action which restarts a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "restart" - - -class NodeServiceDisableAction(NodeServiceAbstractAction): - """Action which disables a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "disable" - - -class NodeServiceEnableAction(NodeServiceAbstractAction): - """Action which enables a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "enable" - - -class NodeServiceFixAction(NodeServiceAbstractAction): - """Action which fixes a service.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb: str = "fix" - - -class NodeApplicationAbstractAction(AbstractAction): - """ - Base class for application actions. - - Any action which applies to an application and uses node_id and application_id as its only two parameters can - inherit from this base class. - """ - - @abstractmethod - def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes, "application_id": num_applications} - self.verb: str # define but don't initialise: defends against children classes not defining this - - def form_request(self, node_id: int, application_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - application_name = self.manager.get_application_name_by_idx(node_id, application_id) - if node_name is None or application_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "application", application_name, self.verb] - - -class NodeApplicationExecuteAction(NodeApplicationAbstractAction): - """Action which executes an application.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications) - self.verb: str = "execute" - - -class NodeApplicationScanAction(NodeApplicationAbstractAction): - """Action which scans an application.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications) - self.verb: str = "scan" - - -class NodeApplicationCloseAction(NodeApplicationAbstractAction): - """Action which closes an application.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications) - self.verb: str = "close" - - -class NodeApplicationFixAction(NodeApplicationAbstractAction): - """Action which fixes an application.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications) - self.verb: str = "fix" - - -class NodeApplicationInstallAction(AbstractAction): - """Action which installs an application.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes} - - def form_request(self, node_id: int, application_name: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - return [ - "network", - "node", - node_name, - "software_manager", - "application", - "install", - application_name, - ] - - -class ConfigureDatabaseClientAction(AbstractAction): - """Action which sets config parameters for a database client on a node.""" - - class _Opts(BaseModel): - """Schema for options that can be passed to this action.""" - - model_config = ConfigDict(extra="forbid") - server_ip_address: Optional[str] = None - server_password: Optional[str] = None - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, config: Dict) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - ConfigureDatabaseClientAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "DatabaseClient", "configure", config] - - -class ConfigureRansomwareScriptAction(AbstractAction): - """Action which sets config parameters for a ransomware script on a node.""" - - class _Opts(BaseModel, AbstractAction.ConfigSchema): - """Schema for options that can be passed to this option.""" - - model_config = ConfigDict(extra="forbid") - server_ip_address: Optional[str] = None - server_password: Optional[str] = None - payload: Optional[str] = None - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, config: _Opts) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - if config.node_name is None: - return ["do_nothing"] - ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema - return [ - "network", - "node", - config.node_name, - "application", - "RansomwareScript", - "configure", - config.model_config, - ] - - -class ConfigureDoSBotAction(AbstractAction): - """Action which sets config parameters for a DoS bot on a node.""" - - class _Opts(BaseModel): - """Schema for options that can be passed to this action.""" - - model_config = ConfigDict(extra="forbid") - target_ip_address: Optional[str] = None - target_port: Optional[str] = None - payload: Optional[str] = None - repeat: Optional[bool] = None - port_scan_p_of_success: Optional[float] = None - dos_intensity: Optional[float] = None - max_sessions: Optional[int] = None - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, config: Dict) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - self._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "DoSBot", "configure", config] - - -class NodeApplicationRemoveAction(AbstractAction): - """Action which removes/uninstalls an application.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes} - - def form_request(self, node_id: int, application_name: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "software_manager", "application", "uninstall", application_name] - - -class NodeFolderAbstractAction(AbstractAction): - """ - Base class for folder actions. - - Any action which applies to a folder and uses node_id and folder_id as its only two parameters can inherit from - this base class. - """ - - @abstractmethod - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes, "folder_id": num_folders} - self.verb: str # define but don't initialise: defends against children classes not defining this - - def form_request(self, node_id: int, folder_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - folder_name = self.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) - if node_name is None or folder_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "file_system", "folder", folder_name, self.verb] - - -class NodeFolderScanAction(NodeFolderAbstractAction): - """Action which scans a folder.""" - - def __init__(self, manager: "ActionManager", node_name: str, folder_name: str, **kwargs) -> None: - super().__init__(manager, node_name=node_name, folder_name=folder_name, **kwargs) - self.verb: str = "scan" - - -class NodeFolderCheckhashAction(NodeFolderAbstractAction): - """Action which checks the hash of a folder.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) - self.verb: str = "checkhash" - - -class NodeFolderRepairAction(NodeFolderAbstractAction): - """Action which repairs a folder.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) - self.verb: str = "repair" - - -class NodeFolderRestoreAction(NodeFolderAbstractAction): - """Action which restores a folder.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) - self.verb: str = "restore" - - -class NodeFileCreateAction(AbstractAction): - """Action which creates a new file in a given folder.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) - self.verb: str = "create" - - def form_request( - self, node_id: int, folder_name: str, file_name: str, force: Optional[bool] = False - ) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None or folder_name is None or file_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "file_system", "create", "file", folder_name, file_name, force] - - -class NodeFolderCreateAction(AbstractAction): - """Action which creates a new folder.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) - self.verb: str = "create" - - def form_request(self, node_id: int, folder_name: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None or folder_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "file_system", "create", "folder", folder_name] - - -class NodeFileAbstractAction(AbstractAction): - """Abstract base class for file actions. - - Any action which applies to a file and uses node_id, folder_id, and file_id as its only three parameters can inherit - from this base class. - """ - - @abstractmethod - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes, "folder_id": num_folders, "file_id": num_files} - self.verb: str # define but don't initialise: defends against children classes not defining this - - def form_request(self, node_id: int, folder_id: int, file_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - folder_name = self.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) - file_name = self.manager.get_file_name_by_idx(node_idx=node_id, folder_idx=folder_id, file_idx=file_id) - if node_name is None or folder_name is None or file_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "file_system", "folder", folder_name, "file", file_name, self.verb] - - -class NodeFileScanAction(NodeFileAbstractAction): - """Action which scans a file.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb: str = "scan" - - -class NodeFileCheckhashAction(NodeFileAbstractAction): - """Action which checks the hash of a file.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb: str = "checkhash" - - -class NodeFileDeleteAction(NodeFileAbstractAction): - """Action which deletes a file.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb: str = "delete" - - def form_request(self, node_id: int, folder_id: int, file_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - folder_name = self.manager.get_folder_name_by_idx(node_idx=node_id, folder_idx=folder_id) - file_name = self.manager.get_file_name_by_idx(node_idx=node_id, folder_idx=folder_id, file_idx=file_id) - if node_name is None or folder_name is None or file_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "file_system", "delete", "file", folder_name, file_name] - - -class NodeFileRepairAction(NodeFileAbstractAction): - """Action which repairs a file.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb: str = "repair" - - -class NodeFileRestoreAction(NodeFileAbstractAction): - """Action which restores a file.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb: str = "restore" - - -class NodeFileCorruptAction(NodeFileAbstractAction): - """Action which corrupts a file.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb: str = "corrupt" - - -class NodeFileAccessAction(AbstractAction): - """Action which increases a file's access count.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: - super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, **kwargs) - self.verb: str = "access" - - def form_request(self, node_id: int, folder_name: str, file_name: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None or folder_name is None or file_name is None: - return ["do_nothing"] - return ["network", "node", node_name, "file_system", "access", folder_name, file_name] - - -class NodeAbstractAction(AbstractAction): - """ - Abstract base class for node actions. - - Any action which applies to a node and uses node_id as its only parameter can inherit from this base class. - """ - - config: "NodeAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): - """Configuration schema for NodeAbstractAction.""" - - verb: str = "Node_Abstract_Action" - - def form_request(self, node_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - return ["network", "node", node_name, self.verb] - - -class NodeOSScanAction(NodeAbstractAction): - """Action which scans a node's OS.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes) - self.verb: str = "scan" - - -class NodeShutdownAction(NodeAbstractAction): - """Action which shuts down a node.""" - - config: "NodeShutdownAction.ConfigSchema" - - class ConfigSchema(NodeAbstractAction.ConfigSchema): - """Configuration Schema for NodeShutdownAction.""" - - verb: str = "shutdown" - - -class NodeStartupAction(NodeAbstractAction): - """Action which starts up a node.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes) - self.verb: str = "startup" - - -class NodeResetAction(NodeAbstractAction): - """Action which resets a node.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes) - self.verb: str = "reset" - - -class RouterACLAddRuleAction(AbstractAction): - """Action which adds a rule to a router's ACL.""" - - class ACLRuleOptions(BaseModel): - """Validator for ACL_ADD_RULE options.""" - - target_router: str - """On which router to add the rule, must be specified.""" - position: int - """At what position to add the rule, must be specified.""" - permission: Literal[1, 2] - """Whether to allow or deny traffic, must be specified. 1 = PERMIT, 2 = DENY.""" - source_ip_id: int = Field(default=1, ge=1) - """Rule source IP address. By default, all ip addresses.""" - source_wildcard_id: int = Field(default=0, ge=0) - """Rule source IP wildcard. By default, use the wildcard at index 0 from action manager.""" - source_port_id: int = Field(default=1, ge=1) - """Rule source port. By default, all source ports.""" - dest_ip_id: int = Field(default=1, ge=1) - """Rule destination IP address. By default, all ip addresses.""" - dest_wildcard_id: int = Field(default=0, ge=0) - """Rule destination IP wildcard. By default, use the wildcard at index 0 from action manager.""" - dest_port_id: int = Field(default=1, ge=1) - """Rule destination port. By default, all destination ports.""" - protocol_id: int = Field(default=1, ge=1) - """Rule protocol. By default, all protocols.""" - - @field_validator( - "source_ip_id", - "source_port_id", - "source_wildcard_id", - "dest_ip_id", - "dest_port_id", - "dest_wildcard_id", - "protocol_id", - mode="before", - ) - @classmethod - def not_none(cls, v: str, info: ValidationInfo) -> int: - """If None is passed, use the default value instead.""" - if v is None: - return cls.model_fields[info.field_name].default - return v - - def __init__( - self, - manager: "ActionManager", - max_acl_rules: int, - num_ips: int, - num_ports: int, - num_protocols: int, - **kwargs, - ) -> None: - """Init method for RouterACLAddRuleAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - :param num_ips: Number of IP addresses in the simulation. - :type num_ips: int - :param num_ports: Number of ports in the simulation. - :type num_ports: int - :param num_protocols: Number of protocols in the simulation. - :type num_protocols: int - """ - super().__init__(manager=manager) - num_permissions = 3 - self.shape: Dict[str, int] = { - "position": max_acl_rules, - "permission": num_permissions, - "source_ip_id": num_ips, - "dest_ip_id": num_ips, - "source_port_id": num_ports, - "dest_port_id": num_ports, - "protocol_id": num_protocols, - } - - def form_request( - self, - target_router: str, - position: int, - permission: int, - source_ip_id: int, - source_wildcard_id: int, - dest_ip_id: int, - dest_wildcard_id: int, - source_port_id: int, - dest_port_id: int, - protocol_id: int, - ) -> List[str]: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - # Validate incoming data. - parsed_options = RouterACLAddRuleAction.ACLRuleOptions( - target_router=target_router, - position=position, - permission=permission, - source_ip_id=source_ip_id, - source_wildcard_id=source_wildcard_id, - dest_ip_id=dest_ip_id, - dest_wildcard_id=dest_wildcard_id, - source_port_id=source_port_id, - dest_port_id=dest_port_id, - protocol_id=protocol_id, - ) - if parsed_options.permission == 1: - permission_str = "PERMIT" - elif parsed_options.permission == 2: - permission_str = "DENY" - else: - _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") - - if parsed_options.protocol_id == 1: - protocol = "ALL" - else: - protocol = self.manager.get_internet_protocol_by_idx(parsed_options.protocol_id - 2) - # subtract 2 to account for UNUSED=0 and ALL=1. - - if parsed_options.source_ip_id == 1: - src_ip = "ALL" - else: - src_ip = self.manager.get_ip_address_by_idx(parsed_options.source_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - src_wildcard = self.manager.get_wildcard_by_idx(parsed_options.source_wildcard_id) - - if parsed_options.source_port_id == 1: - src_port = "ALL" - else: - src_port = self.manager.get_port_by_idx(parsed_options.source_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - if parsed_options.dest_ip_id == 1: - dst_ip = "ALL" - else: - dst_ip = self.manager.get_ip_address_by_idx(parsed_options.dest_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - dst_wildcard = self.manager.get_wildcard_by_idx(parsed_options.dest_wildcard_id) - - if parsed_options.dest_port_id == 1: - dst_port = "ALL" - else: - dst_port = self.manager.get_port_by_idx(parsed_options.dest_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - return [ - "network", - "node", - target_router, - "acl", - "add_rule", - permission_str, - protocol, - str(src_ip), - src_wildcard, - src_port, - str(dst_ip), - dst_wildcard, - dst_port, - position, - ] - - -class RouterACLRemoveRuleAction(AbstractAction): - """Action which removes a rule from a router's ACL.""" - - def __init__(self, manager: "ActionManager", max_acl_rules: int, **kwargs) -> None: - """Init method for RouterACLRemoveRuleAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"position": max_acl_rules} - - def form_request(self, target_router: str, position: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["network", "node", target_router, "acl", "remove_rule", position] - - -class FirewallACLAddRuleAction(AbstractAction): - """Action which adds a rule to a firewall port's ACL.""" - - def __init__( - self, - manager: "ActionManager", - max_acl_rules: int, - num_ips: int, - num_ports: int, - num_protocols: int, - **kwargs, - ) -> None: - """Init method for FirewallACLAddRuleAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - :param num_ips: Number of IP addresses in the simulation. - :type num_ips: int - :param num_ports: Number of ports in the simulation. - :type num_ports: int - :param num_protocols: Number of protocols in the simulation. - :type num_protocols: int - """ - super().__init__(manager=manager) - num_permissions = 3 - self.shape: Dict[str, int] = { - "position": max_acl_rules, - "permission": num_permissions, - "source_ip_id": num_ips, - "dest_ip_id": num_ips, - "source_port_id": num_ports, - "dest_port_id": num_ports, - "protocol_id": num_protocols, - } - - def form_request( - self, - target_firewall_nodename: str, - firewall_port_name: str, - firewall_port_direction: str, - position: int, - permission: int, - source_ip_id: int, - source_wildcard_id: int, - dest_ip_id: int, - dest_wildcard_id: int, - source_port_id: int, - dest_port_id: int, - protocol_id: int, - ) -> List[str]: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if permission == 0: - permission_str = "UNUSED" - return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - elif permission == 1: - permission_str = "PERMIT" - elif permission == 2: - permission_str = "DENY" - else: - _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") - - if protocol_id == 0: - return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS - - if protocol_id == 1: - protocol = "ALL" - else: - protocol = self.manager.get_internet_protocol_by_idx(protocol_id - 2) - # subtract 2 to account for UNUSED=0 and ALL=1. - - if source_ip_id == 0: - return ["do_nothing"] # invalid formulation - elif source_ip_id == 1: - src_ip = "ALL" - else: - src_ip = self.manager.get_ip_address_by_idx(source_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - if source_port_id == 0: - return ["do_nothing"] # invalid formulation - elif source_port_id == 1: - src_port = "ALL" - else: - src_port = self.manager.get_port_by_idx(source_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - if dest_ip_id == 0: - return ["do_nothing"] # invalid formulation - elif dest_ip_id == 1: - dst_ip = "ALL" - else: - dst_ip = self.manager.get_ip_address_by_idx(dest_ip_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - - if dest_port_id == 0: - return ["do_nothing"] # invalid formulation - elif dest_port_id == 1: - dst_port = "ALL" - else: - dst_port = self.manager.get_port_by_idx(dest_port_id - 2) - # subtract 2 to account for UNUSED=0, and ALL=1 - src_wildcard = self.manager.get_wildcard_by_idx(source_wildcard_id) - dst_wildcard = self.manager.get_wildcard_by_idx(dest_wildcard_id) - - return [ - "network", - "node", - target_firewall_nodename, - firewall_port_name, - firewall_port_direction, - "acl", - "add_rule", - permission_str, - protocol, - str(src_ip), - src_wildcard, - src_port, - str(dst_ip), - dst_wildcard, - dst_port, - position, - ] - - -class FirewallACLRemoveRuleAction(AbstractAction): - """Action which removes a rule from a firewall port's ACL.""" - - def __init__(self, manager: "ActionManager", max_acl_rules: int, **kwargs) -> None: - """Init method for RouterACLRemoveRuleAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param max_acl_rules: Maximum number of ACL rules that can be added to the router. - :type max_acl_rules: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"position": max_acl_rules} - - def form_request( - self, target_firewall_nodename: str, firewall_port_name: str, firewall_port_direction: str, position: int - ) -> List[str]: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [ - "network", - "node", - target_firewall_nodename, - firewall_port_name, - firewall_port_direction, - "acl", - "remove_rule", - position, - ] - - -class HostNICAbstractAction(AbstractAction): - """ - Abstract base class for NIC actions. - - Any action which applies to a NIC and uses node_id and nic_id as its only two parameters can inherit from this base - class. - """ - - def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: - """Init method for HostNICAbstractAction. - - :param manager: Reference to the ActionManager which created this action. - :type manager: ActionManager - :param num_nodes: Number of nodes in the simulation. - :type num_nodes: int - :param max_nics_per_node: Maximum number of NICs per node. - :type max_nics_per_node: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"node_id": num_nodes, "nic_id": max_nics_per_node} - self.verb: str # define but don't initialise: defends against children classes not defining this - - def form_request(self, node_id: int, nic_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_idx=node_id) - nic_num = self.manager.get_nic_num_by_idx(node_idx=node_id, nic_idx=nic_id) - if node_name is None or nic_num is None: - return ["do_nothing"] - return ["network", "node", node_name, "network_interface", nic_num, self.verb] - - -class HostNICEnableAction(HostNICAbstractAction): - """Action which enables a NIC.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, max_nics_per_node=max_nics_per_node, **kwargs) - self.verb: str = "enable" - - -class HostNICDisableAction(HostNICAbstractAction): - """Action which disables a NIC.""" - - def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: - super().__init__(manager=manager, num_nodes=num_nodes, max_nics_per_node=max_nics_per_node, **kwargs) - self.verb: str = "disable" - - -class NetworkPortEnableAction(AbstractAction): - """Action which enables are port on a router or a firewall.""" - - def __init__(self, manager: "ActionManager", max_nics_per_node: int, **kwargs) -> None: - """Init method for NetworkPortEnableAction. - - :param max_nics_per_node: Maximum number of NICs per node. - :type max_nics_per_node: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"port_id": max_nics_per_node} - - def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if target_nodename is None or port_id is None: - return ["do_nothing"] - return ["network", "node", target_nodename, "network_interface", port_id, "enable"] - - -class NetworkPortDisableAction(AbstractAction): - """Action which disables are port on a router or a firewall.""" - - def __init__(self, manager: "ActionManager", max_nics_per_node: int, **kwargs) -> None: - """Init method for NetworkPortDisableAction. - - :param max_nics_per_node: Maximum number of NICs per node. - :type max_nics_per_node: int - """ - super().__init__(manager=manager) - self.shape: Dict[str, int] = {"port_id": max_nics_per_node} - - def form_request(self, target_nodename: str, port_id: int) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if target_nodename is None or port_id is None: - return ["do_nothing"] - return ["network", "node", target_nodename, "network_interface", port_id, "disable"] - - -class NodeNMAPPingScanAction(AbstractAction): - """Action which performs an NMAP ping scan.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request( - self, source_node: str, target_ip_address: Union[str, List[str]], show: Optional[bool] = False - ) -> List[str]: # noqa - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [ - "network", - "node", - source_node, - "application", - "NMAP", - "ping_scan", - {"target_ip_address": target_ip_address, "show": show}, - ] - - -class NodeNMAPPortScanAction(AbstractAction): - """Action which performs an NMAP port scan.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request( - self, - source_node: str, - target_ip_address: Union[str, List[str]], - target_protocol: Optional[Union[str, List[str]]] = None, - target_port: Optional[Union[str, List[str]]] = None, - show: Optional[bool] = False, - ) -> List[str]: # noqa - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [ - "network", - "node", - source_node, - "application", - "NMAP", - "port_scan", - { - "target_ip_address": target_ip_address, - "target_port": target_port, - "target_protocol": target_protocol, - "show": show, - }, - ] - - -class NodeNetworkServiceReconAction(AbstractAction): - """Action which performs an NMAP network service recon (ping scan followed by port scan).""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request( - self, - source_node: str, - target_ip_address: Union[str, List[str]], - target_protocol: Optional[Union[str, List[str]]] = None, - target_port: Optional[Union[str, List[str]]] = None, - show: Optional[bool] = False, - ) -> List[str]: # noqa - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return [ - "network", - "node", - source_node, - "application", - "NMAP", - "network_service_recon", - { - "target_ip_address": target_ip_address, - "target_port": target_port, - "target_protocol": target_protocol, - "show": show, - }, - ] - - -class ConfigureC2BeaconAction(AbstractAction): - """Action which configures a C2 Beacon based on the parameters given.""" - - class _Opts(BaseModel): - """Schema for options that can be passed to this action.""" - - c2_server_ip_address: str - keep_alive_frequency: int = Field(default=5, ge=1) - masquerade_protocol: str = Field(default="TCP") - masquerade_port: str = Field(default="HTTP") - - @field_validator( - "c2_server_ip_address", - "keep_alive_frequency", - "masquerade_protocol", - "masquerade_port", - mode="before", - ) - @classmethod - def not_none(cls, v: str, info: ValidationInfo) -> int: - """If None is passed, use the default value instead.""" - if v is None: - return cls.model_fields[info.field_name].default - return v - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, config: Dict) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - config = ConfigureC2BeaconAction._Opts( - c2_server_ip_address=config["c2_server_ip_address"], - keep_alive_frequency=config["keep_alive_frequency"], - masquerade_port=config["masquerade_port"], - masquerade_protocol=config["masquerade_protocol"], - ) - - ConfigureC2BeaconAction._Opts.model_validate(config) # check that options adhere to schema - - return ["network", "node", node_name, "application", "C2Beacon", "configure", config.__dict__] - - -class NodeAccountsChangePasswordAction(AbstractAction): - """Action which changes the password for a user.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: str, username: str, current_password: str, new_password: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - return [ - "network", - "node", - node_name, - "service", - "UserManager", - "change_password", - username, - current_password, - new_password, - ] - - -class NodeSessionsRemoteLoginAction(AbstractAction): - """Action which performs a remote session login.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: str, username: str, password: str, remote_ip: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - return [ - "network", - "node", - node_name, - "service", - "Terminal", - "ssh_to_remote", - username, - password, - remote_ip, - ] - - -class NodeSessionsRemoteLogoutAction(AbstractAction): - """Action which performs a remote session logout.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: str, remote_ip: str) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - return ["network", "node", node_name, "service", "Terminal", "remote_logoff", remote_ip] - - -class RansomwareConfigureC2ServerAction(AbstractAction): - """Action which sends a command from the C2 Server to the C2 Beacon which configures a local RansomwareScript.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, config: Dict) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - # Using the ransomware scripts model to validate. - ConfigureRansomwareScriptAction._Opts.model_validate(config) # check that options adhere to schema - return ["network", "node", node_name, "application", "C2Server", "ransomware_configure", config] - - -class RansomwareLaunchC2ServerAction(AbstractAction): - """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - # This action currently doesn't require any further configuration options. - return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] - - -class ExfiltrationC2ServerAction(AbstractAction): - """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" - - class _Opts(BaseModel): - """Schema for options that can be passed to this action.""" - - username: Optional[str] - password: Optional[str] - target_ip_address: str - target_file_name: str - target_folder_name: str - exfiltration_folder_name: Optional[str] - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request( - self, - node_id: int, - account: dict, - target_ip_address: str, - target_file_name: str, - target_folder_name: str, - exfiltration_folder_name: Optional[str], - ) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - - command_model = { - "target_file_name": target_file_name, - "target_folder_name": target_folder_name, - "exfiltration_folder_name": exfiltration_folder_name, - "target_ip_address": target_ip_address, - "username": account["username"], - "password": account["password"], - } - ExfiltrationC2ServerAction._Opts.model_validate(command_model) - return ["network", "node", node_name, "application", "C2Server", "exfiltrate", command_model] - - -class NodeSendRemoteCommandAction(AbstractAction): - """Action which sends a terminal command to a remote node via SSH.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, remote_ip: str, command: RequestFormat) -> RequestFormat: - """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - return [ - "network", - "node", - node_name, - "service", - "Terminal", - "send_remote_command", - remote_ip, - {"command": command}, - ] - - -class TerminalC2ServerAction(AbstractAction): - """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" - - class _Opts(BaseModel): - """Schema for options that can be passed to this action.""" - - commands: Union[List[RequestFormat], RequestFormat] - ip_address: Optional[str] - username: Optional[str] - password: Optional[str] - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int, commands: List, ip_address: Optional[str], account: dict) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - - command_model = { - "commands": commands, - "ip_address": ip_address, - "username": account["username"], - "password": account["password"], - } - - TerminalC2ServerAction._Opts.model_validate(command_model) - return ["network", "node", node_name, "application", "C2Server", "terminal_command", command_model] - - -class RansomwareLaunchC2ServerAction(AbstractAction): - """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" - - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, node_id: int) -> RequestFormat: - """Return the action formatted as a request that can be ingested by the simulation.""" - node_name = self.manager.get_node_name_by_idx(node_id) - if node_name is None: - return ["do_nothing"] - # This action currently doesn't require any further configuration options. - return ["network", "node", node_name, "application", "C2Server", "ransomware_launch"] diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 15c9b4cb..c97d2bc0 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict from primaite.interface.request import RequestFormat -class AbstractAction(BaseModel): +class AbstractAction(BaseModel, ABC): """Base class for actions.""" config: "AbstractAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 6fefeeda..6022f697 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from ipaddress import IPv4Address +from abc import ABC from typing import List from pydantic import field_validator @@ -9,7 +9,7 @@ from pydantic import field_validator from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat from primaite.utils.validation.ip_protocol import protocol_validator -from primaite.utils.validation.ipv4_address import ipv4_validator +from primaite.utils.validation.ipv4_address import ipv4_validator, IPV4Address from primaite.utils.validation.port import port_validator __all__ = ( @@ -20,7 +20,7 @@ __all__ = ( ) -class ACLAddRuleAbstractAction(AbstractAction, identifier="acl_add_rule_abstract_action"): +class ACLAddRuleAbstractAction(AbstractAction, ABC): """Base abstract class for ACL add rule actions.""" config: ConfigSchema = "ACLAddRuleAbstractAction.ConfigSchema" @@ -28,11 +28,11 @@ class ACLAddRuleAbstractAction(AbstractAction, identifier="acl_add_rule_abstract class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema base for ACL add rule abstract actions.""" - src_ip: IPv4Address + src_ip: IPV4Address protocol_name: str permission: str position: int - dst_ip: IPv4Address + dst_ip: IPV4Address src_port: int dst_port: int src_wildcard: int diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 96609f93..223effc4 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from abc import ABC from typing import ClassVar from primaite.game.agent.actions.abstract import AbstractAction @@ -14,7 +15,7 @@ __all__ = ( ) -class NodeApplicationAbstractAction(AbstractAction, identifier="node_application_abstract_action"): +class NodeApplicationAbstractAction(AbstractAction, ABC): """ Base class for application actions. diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index e5ca1c46..bcfb27ac 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from abc import ABC from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction @@ -16,7 +17,7 @@ __all__ = ( ) -class NodeFileAbstractAction(AbstractAction, identifier="node_file_abstract_action"): +class NodeFileAbstractAction(AbstractAction, ABC): """Abstract base class for file actions. Any action which applies to a file and uses node_name, folder_name, and file_name as its diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index d1fd5ef1..7fb90f75 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from abc import ABC from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction @@ -13,7 +14,7 @@ __all__ = ( ) -class NodeFolderAbstractAction(AbstractAction, identifier="node_folder_abstract"): +class NodeFolderAbstractAction(AbstractAction, ABC): """ Base class for folder actions. diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 7b290103..9e3cb71a 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from abc import ABC from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction @@ -7,7 +8,7 @@ from primaite.interface.request import RequestFormat __all__ = ("HostNICEnableAction", "HostNICDisableAction") -class HostNICAbstractAction(AbstractAction, identifier="host_nic_abstract"): +class HostNICAbstractAction(AbstractAction, ABC): """ Abstract base class for NIC actions. diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 625d8cec..c3e14379 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -127,9 +127,6 @@ class ActionManager: :return: The constructed ActionManager. :rtype: ActionManager """ - if "ip_list" not in cfg["options"]: - cfg["options"]["ip_list"] = [] - obj = cls( actions=cfg["action_list"], **cfg["options"], diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index 5e63f2ec..f0ee6f7c 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -207,10 +207,6 @@ class SoftwareManager: :param session_id: The Session ID from which the payload originates. Optional. :return: True if the payload was successfully sent, False otherwise. """ - print(payload) - print(dest_ip_address) - print(src_port) - print(session_id) return self.session_manager.receive_payload_from_software_manager( payload=payload, dst_ip_address=dest_ip_address, diff --git a/src/primaite/utils/validation/ipv4_address.py b/src/primaite/utils/validation/ipv4_address.py index c385ed1e..b2b8b72e 100644 --- a/src/primaite/utils/validation/ipv4_address.py +++ b/src/primaite/utils/validation/ipv4_address.py @@ -31,7 +31,7 @@ def ipv4_validator(v: Any) -> IPv4Address: IPV4Address: Final[Annotated] = Annotated[IPv4Address, BeforeValidator(ipv4_validator)] """ -IPv4Address with with IPv4Address with with pre-validation and auto-conversion from str using ipv4_validator.. +IPv4Address with pre-validation and auto-conversion from str using ipv4_validator.. This type is essentially an IPv4Address from the standard library's ipaddress module, but with added validation logic. If you use this custom type, the ipv4_validator function diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index 9021b8af..1699798a 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -5,7 +5,7 @@ import pytest from primaite.game.agent.actions import ( ActionManager, - do_nothingAction, + DoNothingAction, NodeServiceDisableAction, NodeServiceEnableAction, NodeServicePauseAction, diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 916b35c0..91d5c607 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -81,7 +81,7 @@ class TestWebpageUnavailabilitySticky: reward = WebpageUnavailablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} From 3528b712f12c98b67020a230ef62ff3edfa656b3 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 16:35:08 +0000 Subject: [PATCH 124/224] #2912 - Correct instances of verb: str to verb: ClassVar[str] where the parent class uses ClassVar[str] --- src/primaite/game/agent/actions/file.py | 16 ++++++++-------- src/primaite/game/agent/actions/folder.py | 10 +++++----- src/primaite/game/agent/actions/host_nic.py | 4 ++-- src/primaite/game/agent/actions/network.py | 4 ++-- src/primaite/game/agent/actions/node.py | 8 ++++---- src/primaite/game/agent/actions/service.py | 18 +++++++++--------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index bcfb27ac..ed666773 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -60,7 +60,7 @@ class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCreateAction.""" - verb: str = "create" + verb: ClassVar[str] = "create" force: bool = False @classmethod @@ -89,7 +89,7 @@ class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileScanAction.""" - verb: str = "scan" + verb: ClassVar[str] = "scan" class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete"): @@ -100,7 +100,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileDeleteAction.""" - verb: str = "delete" + verb: ClassVar[str] = "delete" @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -127,7 +127,7 @@ class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restor class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileRestoreAction.""" - verb: str = "restore" + verb: ClassVar[str] = "restore" class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrupt"): @@ -138,7 +138,7 @@ class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrup class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCorruptAction.""" - verb: str = "corrupt" + verb: ClassVar[str] = "corrupt" class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access"): @@ -149,7 +149,7 @@ class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileAccessAction.""" - verb: str = "access" + verb: ClassVar[str] = "access" @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: @@ -175,7 +175,7 @@ class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_chec class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration schema for NodeFileCheckhashAction.""" - verb: str = "checkhash" + verb: ClassVar[str] = "checkhash" class NodeFileRepairAction(NodeFileAbstractAction, identifier="node_file_repair"): @@ -186,4 +186,4 @@ class NodeFileRepairAction(NodeFileAbstractAction, identifier="node_file_repair" class ConfigSchema(NodeFileAbstractAction.ConfigSchema): """Configuration Schema for NodeFileRepairAction.""" - verb: str = "repair" + verb: ClassVar[str] = "repair" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index 7fb90f75..3e1136ac 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -55,7 +55,7 @@ class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_sca class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderScanAction.""" - verb: str = "scan" + verb: ClassVar[str] = "scan" class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folder_checkhash"): @@ -66,7 +66,7 @@ class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folde class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderCheckhashAction.""" - verb: str = "checkhash" + verb: ClassVar[str] = "checkhash" class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_repair"): @@ -77,7 +77,7 @@ class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_r class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderRepairAction.""" - verb: str = "repair" + verb: ClassVar[str] = "repair" class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_restore"): @@ -88,7 +88,7 @@ class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_ class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderRestoreAction.""" - verb: str = "restore" + verb: ClassVar[str] = "restore" class NodeFolderCreateAction(NodeFolderAbstractAction, identifier="node_folder_create"): @@ -99,7 +99,7 @@ class NodeFolderCreateAction(NodeFolderAbstractAction, identifier="node_folder_c class ConfigSchema(NodeFolderAbstractAction.ConfigSchema): """Configuration schema for NodeFolderCreateAction.""" - verb: str = "create" + verb: ClassVar[str] = "create" @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 9e3cb71a..b9206b9c 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -48,7 +48,7 @@ class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICEnableAction.""" - verb: str = "enable" + verb: ClassVar[str] = "enable" class HostNICDisableAction(HostNICAbstractAction, identifier="host_nic_disable"): @@ -59,4 +59,4 @@ class HostNICDisableAction(HostNICAbstractAction, identifier="host_nic_disable") class ConfigSchema(HostNICAbstractAction.ConfigSchema): """Configuration schema for HostNICDisableAction.""" - verb: str = "disable" + verb: ClassVar[str] = "disable" diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index fa1c4451..7f1e069a 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -43,7 +43,7 @@ class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_por class ConfigSchema(NetworkPortAbstractAction.ConfigSchema): """Configuration schema for NetworkPortEnableAction.""" - verb: str = "enable" + verb: ClassVar[str] = "enable" class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_port_disable"): @@ -54,4 +54,4 @@ class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_po class ConfigSchema(NetworkPortAbstractAction.ConfigSchema): """Configuration schema for NetworkPortDisableAction.""" - verb: str = "disable" + verb: ClassVar[str] = "disable" diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index c6b74f2e..4a7f725e 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -46,7 +46,7 @@ class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeOSScanAction.""" - verb: str = "scan" + verb: ClassVar[str] = "scan" class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): @@ -57,7 +57,7 @@ class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeShutdownAction.""" - verb: str = "shutdown" + verb: ClassVar[str] = "shutdown" class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): @@ -68,7 +68,7 @@ class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeStartupAction.""" - verb: str = "startup" + verb: ClassVar[str] = "startup" class NodeResetAction(NodeAbstractAction, identifier="node_reset"): @@ -79,7 +79,7 @@ class NodeResetAction(NodeAbstractAction, identifier="node_reset"): class ConfigSchema(NodeAbstractAction.ConfigSchema): """Configuration schema for NodeResetAction.""" - verb: str = "reset" + verb: ClassVar[str] = "reset" class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_action"): diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index fa47ffb1..4a483f28 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -44,7 +44,7 @@ class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_ class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceScanAction.""" - verb: str = "scan" + verb: ClassVar[str] = "scan" class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_stop"): @@ -55,7 +55,7 @@ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_ class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceStopAction.""" - verb: str = "stop" + verb: ClassVar[str] = "stop" class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service_start"): @@ -66,7 +66,7 @@ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceStartAction.""" - verb: str = "start" + verb: ClassVar[str] = "start" class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service_pause"): @@ -77,7 +77,7 @@ class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServicePauseAction.""" - verb: str = "pause" + verb: ClassVar[str] = "pause" class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_service_resume"): @@ -88,7 +88,7 @@ class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_servic class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceResumeAction.""" - verb: str = "resume" + verb: ClassVar[str] = "resume" class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_service_restart"): @@ -99,7 +99,7 @@ class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_servi class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceRestartAction.""" - verb: str = "restart" + verb: ClassVar[str] = "restart" class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_service_disable"): @@ -110,7 +110,7 @@ class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_servi class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceDisableAction.""" - verb: str = "disable" + verb: ClassVar[str] = "disable" class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_service_enable"): @@ -121,7 +121,7 @@ class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_servic class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceEnableAction.""" - verb: str = "enable" + verb: ClassVar[str] = "enable" class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_fix"): @@ -132,4 +132,4 @@ class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_f class ConfigSchema(NodeServiceAbstractAction.ConfigSchema): """Configuration Schema for NodeServiceFixAction.""" - verb: str = "fix" + verb: ClassVar[str] = "fix" From 1ac562ebc9c2de6f779a736f1bb085603a117d20 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 13 Jan 2025 17:09:03 +0000 Subject: [PATCH 125/224] #2912 - Initial layout of extensible_actions documentation page --- .../how_to_guides/extensible_actions.rst | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index f2e053aa..0064a3a7 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -1,3 +1,67 @@ .. only:: comment © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + + +Extensible Actions +****************** + + +Changes to Actions class Structure. +=================================== + +Actions within PrimAITE have been updated to inherit from a base class, AbstractAction, standardising their format and allowing for easier creation of custom actions. Actions now use a ``ConfigSchema`` to define the possible configuration variables, and use pydantic to enforce correct parameters are passed through. + + +Developing Custom Actions. +========================== + +Custom actions within PrimAITE must be a sub-class of `AbstractAction`, and contain 3 key items: + +#. ConfigSchema class + +#. Unique Identifier + +#. `from_request` method. + + +ConfigSchema +############ + +The ConfigSchema sub-class of the action must contain all `configurable` variables within the action, that would be specified within the environments configuration YAML file. + + +Unique Identifier +################# + +When declaring a custom class, it must have a unique identifier string, that allows PrimAITE to generate the correct action when needed. + +.. code:: Python + + class CreateDirectoryAction(AbstractAction, identifier="node_folder_create") + + config: CreateDirectoryAction.ConfigSchema + + class ConfigSchema(AbstractAction.ConfigSchema): + + verb: ClassVar[str] = "create" + node_name: str + directory_name: str + + def form_request(cls, config: ConfigSchema) -> RequestFormat: + return ["network", + "node", + config.node_name, + "file_system", + config.verb, + "folder", + config.directory_name, + ] + +The above action would fail pydantic validation as the identifier "node_folder_create" is already used by the `NodeFolderCreateAction`, and would create a duplicate listing within `AbstractAction._registry`. + + +from_request method +################### + +PrimAITE actions need to be have a `from_request` method, which can be passed to the `RequestManager` for processing. This allows the custom action to be actioned within the simulation environment. From a447c5f43c0c186779359eb23db1a07d3e1e279b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 14 Jan 2025 09:05:13 +0000 Subject: [PATCH 126/224] #2869 - Make periodic agent timing check stricter --- src/primaite/game/agent/scripted_agents/random_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index b5601a58..999669d8 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -59,7 +59,7 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): If variance were greater than frequency, sometimes the bracketed term would be negative and the attack would never happen again. """ - if self.variance > self.frequency: + if self.variance >= self.frequency: raise ValueError( f"Agent start settings error: variance must be lower than frequency " f"{self.variance=}, {self.frequency=}" From e7cfeeafc04c3862e6baac46c59030090e7079fb Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 14 Jan 2025 09:57:35 +0000 Subject: [PATCH 127/224] Make data manipulation agent inherit from periodic agent & fix it a bit --- .../_package_data/data_manipulation.yaml | 4 +- .../_package_data/data_manipulation_marl.yaml | 4 +- .../scripted_agents/data_manipulation_bot.py | 39 ++++++++----------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index d604192e..58986bce 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -150,7 +150,9 @@ agents: reward_components: - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + agent_settings: + possible_start_nodes: [client_1, client_2] + starting_application_name: DataManipulationBot start_step: 25 frequency: 20 variance: 5 diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index 00a34403..c4a3b562 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -149,7 +149,9 @@ agents: reward_components: - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + agent_settings: + possible_start_nodes: [client_1, client_2] + starting_application_name: DataManipulationBot start_step: 25 frequency: 20 variance: 5 diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index b9d57a8b..8fe0690b 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,29 +1,33 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from typing import Dict, Optional, Tuple +import random +from typing import Dict, List, Tuple from gymnasium.core import ObsType from pydantic import Field -from primaite.game.agent.scripted_agents.abstract_tap import AbstractTAPAgent +from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "DataManipulationAgent" -class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingAgent"): +class DataManipulationAgent(PeriodicAgent, identifier="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" - config: "DataManipulationAgent.ConfigSchema" = Field(default_factory=lambda: DataManipulationAgent.ConfigSchema()) - - class ConfigSchema(AbstractTAPAgent.ConfigSchema): + class ConfigSchema(PeriodicAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" type: str = "RedDatabaseCorruptingAgent" - starting_application_name: Optional[str] = None + starting_application_name: str = "DataManipulationBot" + possible_start_nodes: List[str] - @property - def starting_node_name(self) -> str: - """Returns the agents starting node name.""" - return self.config.starting_node_name + config: "DataManipulationAgent.ConfigSchema" = Field(default_factory=lambda: DataManipulationAgent.ConfigSchema()) + + start_node: str + + def __init__(self, **kwargs): + kwargs["start_node"] = random.choice(kwargs["config"].possible_start_nodes) + super().__init__(**kwargs) + self._set_next_execution_timestep(timestep=self.config.start_step, variance=0) def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Waits until a specific timestep, then attempts to execute its data manipulation application. @@ -35,22 +39,13 @@ class DataManipulationAgent(AbstractTAPAgent, identifier="RedDatabaseCorruptingA :return: Action formatted in CAOS format :rtype: Tuple[str, Dict] """ - if self.starting_node_name or self.config is None: - self.setup_agent() - self.get_action(obs=obs, timestep=timestep) - if timestep < self.next_execution_timestep: self.logger.debug(msg="Performing do nothing action") return "do_nothing", {} - self._set_next_execution_timestep(timestep + self.config.frequency) + self._set_next_execution_timestep(timestep=timestep + self.config.frequency, variance=self.config.variance) self.logger.info(msg="Performing a data manipulation attack!") return "node_application_execute", { - "node_name": self.config.starting_node_name, + "node_name": self.start_node, "application_name": self.config.starting_application_name, } - - def setup_agent(self) -> None: - """Set the next execution timestep when the episode resets.""" - self._select_start_node() - self._set_next_execution_timestep(self.config.start_step) From 9f5e16dd859ebad6793e6a01d55c33c18d07abaa Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 14 Jan 2025 10:58:34 +0000 Subject: [PATCH 128/224] #2869 - Edit test fixture to work with new agent system --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index b4b72e55..f4630c9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -500,10 +500,12 @@ def game_and_agent(): reward_function = RewardFunction() config = { + "type": "ControlledAgent", "agent_name": "test_agent", "action_manager": action_space, "observation_manager": observation_space, "reward_function": reward_function, + "agent_settings": {}, } test_agent = ControlledAgent.from_config(config=config) From 40d052141c92b0634f683d6ce830cd234a6bfc99 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 14 Jan 2025 13:48:18 +0000 Subject: [PATCH 129/224] #2869 - Remove outdated parameters from actionmanager (action map achieves the same result) [skip ci] --- docs/source/configuration/agents.rst | 17 -- .../how_to_guides/extensible_agents.rst | 13 +- .../applications/data_manipulation_bot.rst | 12 -- .../_package_data/data_manipulation.yaml | 104 ---------- .../_package_data/data_manipulation_marl.yaml | 176 ----------------- .../base_scenario.yaml | 18 -- .../scenario_with_placeholders/greens_1.yaml | 8 - .../scenario_with_placeholders/greens_2.yaml | 8 - .../scenario_with_placeholders/reds_1.yaml | 8 - .../scenario_with_placeholders/reds_2.yaml | 8 - .../scenario_with_placeholders/scenario.yaml | 19 -- src/primaite/game/agent/actions/manager.py | 58 +----- src/primaite/game/agent/interface.py | 4 +- src/primaite/game/game.py | 4 +- ...ommand-and-Control-E2E-Demonstration.ipynb | 72 ++----- ...a-Manipulation-Customising-Red-Agent.ipynb | 21 +- tests/assets/configs/action_penalty.yaml | 67 ------- .../assets/configs/bad_primaite_session.yaml | 78 -------- tests/assets/configs/basic_firewall.yaml | 12 -- .../configs/basic_switched_network.yaml | 30 --- tests/assets/configs/data_manipulation.yaml | 107 ----------- tests/assets/configs/dmz_network.yaml | 12 -- .../configs/eval_only_primaite_session.yaml | 78 -------- tests/assets/configs/extended_config.yaml | 105 ---------- .../configs/firewall_actions_network.yaml | 20 -- .../assets/configs/fix_duration_one_item.yaml | 30 --- .../configs/install_and_configure_apps.yaml | 13 -- tests/assets/configs/multi_agent_session.yaml | 179 ------------------ ...etwork_service_recon_red_agent_config.yaml | 11 -- .../nmap_ping_scan_red_agent_config.yaml | 11 -- .../nmap_port_scan_red_agent_config.yaml | 11 -- .../scenario_with_placeholders/greens_1.yaml | 8 - .../scenario_with_placeholders/greens_2.yaml | 8 - .../scenario_with_placeholders/reds_1.yaml | 8 - .../scenario_with_placeholders/reds_2.yaml | 8 - .../scenario_with_placeholders/scenario.yaml | 19 -- tests/assets/configs/shared_rewards.yaml | 107 ----------- .../assets/configs/software_fix_duration.yaml | 30 --- .../configs/test_application_install.yaml | 95 +--------- .../assets/configs/test_primaite_session.yaml | 90 --------- .../_primaite/_game/_agent/test_actions.py | 4 +- .../_game/_agent/test_probabilistic_agent.py | 24 +-- 42 files changed, 36 insertions(+), 1679 deletions(-) diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index d11f7892..cf2b618f 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -23,19 +23,6 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo observation_space: type: UC2GreenObservation action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 - reward_function: reward_components: - type: DUMMY @@ -91,10 +78,6 @@ For more information see :py:mod:`primaite.game.agent.observations` The action space is configured to be made up of individual action types. Once configured, the agent can select an action type and some optional action parameters at every step. For example: The ``NODE_SERVICE_SCAN`` action takes the parameters ``node_id`` and ``service_id``. -``action_list`` -^^^^^^^^^^^^^^^ - -A list of action modules. The options are listed in the :py:mod:`primaite.game.agent.actions.ActionManager.act_class_identifiers` module. ``action_map`` ^^^^^^^^^^^^^^ diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 5bbca13a..169af094 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -42,28 +42,17 @@ The core features that should be implemented in any new agent are detailed below """Host node that this agent should start from in the given environment.""" - .. code-block:: YAML + .. code-block:: yaml - ref: example_green_agent team: GREEN type: ExampleAgent observation_space: null action_space: - action_list: - - type: do_nothing action_map: 0: action: do_nothing options: {} - options: - nodes: - - node_name: client_1 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 - reward_function: reward_components: - type: DUMMY diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 91c33ede..49dc3baf 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -113,18 +113,6 @@ If not using the data manipulation bot manually, it needs to be used with a data folders: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_ref: data_manipulation_bot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - reward_function: reward_components: - type: DUMMY diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 58986bce..4869d5d1 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -32,19 +32,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -81,19 +68,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -131,20 +105,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -236,35 +196,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -685,41 +616,6 @@ agents: - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - reward_function: reward_components: diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index c4a3b562..512afc64 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -28,19 +28,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -77,19 +64,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -127,23 +101,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - - type: NODE_FILE_DELETE - - type: NODE_FILE_CORRUPT - - type: NODE_OS_SCAN - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -230,35 +187,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -678,42 +606,6 @@ agents: nic_id: 0 - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY @@ -810,39 +702,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - options: - target_router: router_1 - - type: ROUTER_ACL_REMOVERULE - options: - target_router: router_1 - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -1263,41 +1122,6 @@ agents: - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml index b4457a28..e461eccc 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml @@ -8,12 +8,6 @@ agents: type: ProxyAgent observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE action_map: 0: action: DONOTHING @@ -54,18 +48,6 @@ agents: options: node_id: 1 nic_id: 0 - options: - nodes: - - node_name: client_1 - - node_name: server - max_folders_per_node: 0 - max_files_per_folder: 0 - max_services_per_node: 0 - max_nics_per_node: 1 - max_acl_rules: 0 - ip_list: - - 192.168.1.2 - - 192.168.1.3 reward_function: reward_components: [] diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml index 98d2392a..ce670f5f 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml @@ -8,14 +8,6 @@ agents: &greens 1: 0.8 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DatabaseClient action_map: 0: action: DONOTHING diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml index 17a5977b..9ff099dd 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml @@ -8,14 +8,6 @@ agents: &greens 1: 0.05 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DatabaseClient action_map: 0: action: DONOTHING diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml index b775cb24..b7e7560d 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml @@ -6,14 +6,6 @@ reds: &reds observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DataManipulationBot reward_function: reward_components: diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml index 4cae1ec6..1d9012d7 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml @@ -6,14 +6,6 @@ reds: &reds observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DataManipulationBot reward_function: reward_components: diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index 8c83bf79..0223beb6 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -54,12 +54,6 @@ agents: - server:eth-1<->switch_1:eth-2 action_space: - action_list: - - type: do_nothing - - type: node_shutdown - - type: node_startup - - type: host_nic_enable - - type: host_nic_enable action_map: 0: action: do_nothing @@ -100,19 +94,6 @@ agents: options: node_name: server nic_id: 0 - options: - nodes: - - node_name: client - - node_name: server - - max_folders_per_node: 0 - max_files_per_folder: 0 - max_services_per_node: 0 - max_nics_per_node: 1 - max_acl_rules: 0 - ip_list: - - 192.168.1.2 - - 192.168.1.3 reward_function: reward_components: diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index c3e14379..400d30e4 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -13,7 +13,7 @@ agents: from __future__ import annotations -from typing import Dict, List, Optional, Tuple +from typing import Dict, Optional, Tuple from gymnasium import spaces @@ -41,31 +41,12 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): class ActionManager: """Class which manages the action space for an agent.""" - def __init__( - self, - actions: List[Dict], # stores list of actions available to agent - nodes: List[Dict], # extra configuration for each node - act_map: Optional[ - Dict[int, Dict] - ] = None, # allows restricting set of possible actions - TODO: Refactor to be a list? - *args, - **kwargs, - ) -> None: + def __init__(self, act_map: Optional[Dict[int, Dict]] = None) -> None: """Init method for ActionManager. - :param game: Reference to the game to which the agent belongs. - :type game: PrimaiteGame - :param actions: List of action specs which should be made available to the agent. The keys of each spec are: - 'type' and 'options' for passing any options to the action class's init method - :type actions: List[dict] :param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions. :type act_map: Optional[Dict[int, Dict]] """ - self.actions: Dict[str, AbstractAction] = {} - for act_spec in actions: - act_type = act_spec.get("type") - self.actions[act_type] = AbstractAction._registry[act_type] - self.action_map: Dict[int, Tuple[str, Dict]] = {} """ Action mapping that converts an integer to a specific action and parameter choice. @@ -73,6 +54,7 @@ class ActionManager: For example : {0: ("node_service_scan", {node_name:"client_1", service_name:"WebBrowser"})} """ + # allows restricting set of possible actions - TODO: Refactor to be a list? if act_map is None: # raise RuntimeError("Action map must be specified in the config file.") pass @@ -100,39 +82,17 @@ class ActionManager: return spaces.Discrete(len(self.action_map)) @classmethod - def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": # noqa: F821 + def from_config(cls, cfg: Dict) -> "ActionManager": """ - Construct an ActionManager from a config definition. + Construct an ActionManager from a config dictionary. - The action space config supports the following three sections: - 1. ``action_list`` - ``action_list`` contains a list action components which need to be included in the action space. - Each action component has a ``type`` which maps to a subclass of AbstractAction, and additional options - which will be passed to the action class's __init__ method during initialisation. - 2. ``action_map`` - Since the agent uses a discrete action space which acts as a flattened version of the component-based - action space, action_map provides a mapping between an integer (chosen by the agent) and a meaningful - action and values of parameters. For example action 0 can correspond to do nothing, action 1 can - correspond to "node_service_scan" with ``node_name="server"`` and - ``service_name="WebBrowser"``, action 2 can be " - 3. ``options`` - ``options`` contains a dictionary of options which are passed to the ActionManager's __init__ method. - These options are used to calculate the shape of the action space, and to provide additional information - to the ActionManager which is required to convert the agent's action choice into a CAOS request. + The action space config supports must contain the following key: + ``action_map`` - List of actions available to the agent, formatted as a dictionary where the key is the + action number between 0 - N, and the value is the CAOS-formatted action. - :param game: The Primaite Game to which the agent belongs. - :type game: PrimaiteGame :param cfg: The action space config. :type cfg: Dict :return: The constructed ActionManager. :rtype: ActionManager """ - obj = cls( - actions=cfg["action_list"], - **cfg["options"], - protocols=game.options.protocols, - ports=game.options.ports, - act_map=cfg.get("action_map"), - ) - - return obj + return cls(**cfg.get("options", {}), act_map=cfg.get("action_map")) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index ec9d6c61..9d8f3f63 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -93,8 +93,8 @@ class AbstractAgent(BaseModel): return ValueError(f"Invalid Agent Type: {config['type']}") obj = cls( config=cls.ConfigSchema(**config["agent_settings"]), - action_manager=ActionManager.from_config(config["game"], config["action_manager"]), - observation_manager=ObservationManager.from_config(config["observation_manager"]), + action_manager=ActionManager.from_config(config["action_space"]), + observation_manager=ObservationManager.from_config(config["observation_space"]), reward_function=RewardFunction.from_config(config["reward_function"]), ) return obj diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index bf480d0e..5220e874 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -534,8 +534,8 @@ class PrimaiteGame: agent_config = { "type": agent_type, - "action_manager": action_space_cfg, - "observation_manager": observation_space_cfg, + "action_space": action_space_cfg, + "observation_space": observation_space_cfg, "reward_function": reward_function_cfg, "agent_settings": agent_settings, "game": game, diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index d2972fa9..1a5c8b87 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -53,22 +53,13 @@ " type: ProxyAgent\n", " observation_space: null\n", " action_space:\n", - " action_list:\n", - " - type: DONOTHING\n", - " - type: NODE_APPLICATION_INSTALL\n", - " - type: NODE_APPLICATION_EXECUTE\n", - " - type: CONFIGURE_C2_BEACON\n", - " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", - " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", - " - type: C2_SERVER_TERMINAL_COMMAND\n", - " - type: C2_SERVER_DATA_EXFILTRATE\n", " options:\n", " nodes:\n", " - node_name: web_server\n", - " applications: \n", + " applications:\n", " - application_name: C2Beacon\n", " - node_name: client_1\n", - " applications: \n", + " applications:\n", " - application_name: C2Server\n", " max_folders_per_node: 1\n", " max_files_per_folder: 1\n", @@ -102,7 +93,7 @@ " action: NODE_APPLICATION_EXECUTE\n", " options:\n", " node_id: 0\n", - " application_id: 0 \n", + " application_id: 0\n", " 4:\n", " action: C2_SERVER_TERMINAL_COMMAND\n", " options:\n", @@ -112,7 +103,7 @@ " username: admin\n", " password: admin\n", " commands:\n", - " - \n", + " -\n", " - software_manager\n", " - application\n", " - install\n", @@ -134,7 +125,7 @@ " target_ip_address: 192.168.1.14\n", " account:\n", " username: admin\n", - " password: admin \n", + " password: admin\n", "\n", " 7:\n", " action: C2_SERVER_RANSOMWARE_LAUNCH\n", @@ -177,7 +168,7 @@ " # removing all agents & adding the custom agent.\n", " cfg['agents'] = {}\n", " cfg['agents'] = c2_agent_yaml\n", - " \n", + "\n", "\n", "env = PrimaiteGymEnv(env_config=cfg)" ] @@ -230,10 +221,6 @@ "\n", "```yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: NODE_APPLICATION_INSTALL\n", - " ...\n", " options:\n", " nodes: # Node List\n", " - node_name: web_server\n", @@ -273,10 +260,6 @@ "\n", "```yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: CONFIGURE_C2_BEACON\n", - " ...\n", " options:\n", " nodes: # Node List\n", " - node_name: web_server\n", @@ -320,10 +303,6 @@ "\n", "```yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: NODE_APPLICATION_EXECUTE\n", - " ...\n", " options:\n", " nodes: # Node List\n", " - node_name: web_server\n", @@ -347,7 +326,7 @@ "metadata": {}, "outputs": [], "source": [ - "env.step(3) " + "env.step(3)" ] }, { @@ -390,10 +369,6 @@ "\n", "``` yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: C2_SERVER_TERMINAL_COMMAND\n", - " ...\n", " options:\n", " nodes: # Node List\n", " ...\n", @@ -451,10 +426,6 @@ "\n", "``` yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: C2_SERVER_RANSOMWARE_CONFIGURE\n", - " ...\n", " options:\n", " nodes: # Node List\n", " ...\n", @@ -507,10 +478,6 @@ "\n", "``` yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: C2_SERVER_DATA_EXFILTRATE\n", - " ...\n", " options:\n", " nodes: # Node List\n", " ...\n", @@ -577,10 +544,6 @@ "\n", "``` yaml\n", " action_space:\n", - " action_list:\n", - " ...\n", - " - type: C2_SERVER_RANSOMWARE_LAUNCH\n", - " ...\n", " options:\n", " nodes: # Node List\n", " ...\n", @@ -632,7 +595,7 @@ "metadata": {}, "outputs": [], "source": [ - "custom_blue_agent_yaml = \"\"\" \n", + "custom_blue_agent_yaml = \"\"\"\n", " - ref: defender\n", " team: BLUE\n", " type: ProxyAgent\n", @@ -715,13 +678,8 @@ " - type: \"NONE\"\n", " label: ICS\n", " options: {}\n", - " \n", + "\n", " action_space:\n", - " action_list:\n", - " - type: NODE_APPLICATION_REMOVE\n", - " - type: NODE_SHUTDOWN\n", - " - type: ROUTER_ACL_ADDRULE\n", - " - type: DONOTHING\n", " action_map:\n", " 0:\n", " action: DONOTHING\n", @@ -747,7 +705,7 @@ " dest_port_id: 2\n", " protocol_id: 1\n", " source_wildcard_id: 0\n", - " dest_wildcard_id: 0 \n", + " dest_wildcard_id: 0\n", "\n", "\n", " options:\n", @@ -796,7 +754,7 @@ " # removing all agents & adding the custom agent.\n", " cfg['agents'] = {}\n", " cfg['agents'] = custom_blue\n", - " \n", + "\n", "\n", "blue_env = PrimaiteGymEnv(env_config=cfg)" ] @@ -1468,7 +1426,7 @@ " # removing all agents & adding the custom agent.\n", " cfg['agents'] = {}\n", " cfg['agents'] = c2_agent_yaml\n", - " \n", + "\n", "\n", "c2_config_env = PrimaiteGymEnv(env_config=cfg)" ] @@ -1555,7 +1513,7 @@ "source": [ "for i in range(6):\n", " env.step(0)\n", - " \n", + "\n", "c2_server_1.show()" ] }, @@ -1676,7 +1634,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Comparing the OBS of the default frequency to a timestep frequency of 1 \n", + "# Comparing the OBS of the default frequency to a timestep frequency of 1\n", "for i in range(2):\n", " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", " display_obs_diffs(default_obs, keep_alive_obs, blue_config_env.game.step_counter)" @@ -1760,7 +1718,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Capturing default C2 Traffic \n", + "# Capturing default C2 Traffic\n", "for i in range(3):\n", " tcp_c2_obs, _, _, _, _ = blue_config_env.step(0)\n", "\n", diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 07881131..50bfa59f 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -147,12 +147,7 @@ " nodes: {}\n", "\n", " action_space:\n", - "\n", - " # The agent has two action choices, either do nothing, or execute a pre-scripted attack by using \n", - " action_list:\n", - " - type: DONOTHING\n", - " - type: NODE_APPLICATION_EXECUTE\n", - "\n", + " \n", " # The agent has access to the DataManipulationBoth on clients 1 and 2.\n", " options:\n", " nodes:\n", @@ -306,19 +301,9 @@ "outputs": [], "source": [ "change = yaml.safe_load(\"\"\"\n", - "action_space:\n", - " action_list:\n", - " - type: DONOTHING\n", - " - type: NODE_APPLICATION_EXECUTE\n", - " options:\n", - " nodes:\n", - " - node_name: client_1\n", - " applications:\n", - " - application_name: DataManipulationBot\n", - " max_folders_per_node: 1\n", - " max_files_per_folder: 1\n", - " max_services_per_node: 1\n", + "# TODO:\n", "\"\"\")\n", + "#TODO 2869 fix\n", "\n", "with open(data_manipulation_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n", diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 1771ba5f..2ebe1963 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -96,35 +96,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -543,44 +514,6 @@ agents: node_id: 6 nic_id: 0 - - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: ACTION_PENALTY diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 6a19c2fb..9f3e6da5 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -15,16 +15,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING - options: - nodes: - - node_name: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 reward_function: reward_components: @@ -42,20 +32,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - - type: NODE_FILE_DELETE - - type: NODE_FILE_CORRUPT - - type: NODE_OS_SCAN - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -140,34 +116,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE action_map: 0: @@ -490,32 +438,6 @@ agents: node_id: 6 nic_id: 0 - - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - - node_name: database_server - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index fe5e0099..09e070d5 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -29,9 +29,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE action_map: 0: action: DONOTHING @@ -41,15 +38,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 reward_function: reward_components: diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 8aa97a6b..453db4b0 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -32,9 +32,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE action_map: 0: action: DONOTHING @@ -44,15 +41,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 reward_function: reward_components: @@ -125,28 +113,10 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - action_map: 0: action: DONOTHING options: {} - options: - nodes: - - node_name: switch - - node_name: client_1 - - node_name: client_2 - - node_name: client_3 - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.23 reward_function: reward_components: diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index a2d9bb55..90d8f806 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -32,19 +32,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: do_nothing - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: do_nothing @@ -81,19 +68,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: do_nothing - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: do_nothing @@ -131,20 +105,6 @@ agents: observation_space: null action_space: - action_list: - - type: do_nothing - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -234,35 +194,6 @@ agents: options: {} action_space: - action_list: - - type: do_nothing - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: do_nothing @@ -681,44 +612,6 @@ agents: node_id: 6 nic_id: 0 - - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 41a530b0..b0876768 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -54,9 +54,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE action_map: 0: action: DONOTHING @@ -66,15 +63,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 reward_function: reward_components: diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index dc0acdaa..73930e7f 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -15,20 +15,10 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING action_map: 0: action: DONOTHING options: {} - options: - nodes: - - node_name: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 reward_function: reward_components: @@ -46,12 +36,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - - type: NODE_FILE_DELETE - - type: NODE_FILE_CORRUPT - - type: NODE_OS_SCAN action_map: 0: action: DONOTHING @@ -61,14 +45,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: - type: DUMMY @@ -152,34 +128,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE action_map: 0: @@ -502,32 +450,6 @@ agents: node_id: 6 nic_id: 0 - - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - - node_name: database_server - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index fc1b72dd..f8e86d31 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -32,19 +32,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -81,19 +68,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -131,20 +105,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -234,35 +194,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -683,42 +614,6 @@ agents: - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index d88942a8..ceb9c924 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -95,12 +95,6 @@ agents: options: {} action_space: - action_list: - - type: do_nothing - - type: firewall_acl_add_rule - - type: firewall_acl_remove_rule - - type: network_port_disable - - type: network_port_enable action_map: 0: action: do_nothing @@ -250,20 +244,6 @@ agents: type: network_port_enable target_nodename: firewall port_id: 3 - options: - nodes: - - node_name: client_1 - - node_name: dmz_server - - node_name: external_computer - ip_list: - - 192.168.0.10 - - 192.168.10.10 - - 192.168.20.10 - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 reward_function: reward_components: - type: DUMMY diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index 704616f6..26ee574a 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -29,9 +29,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE action_map: 0: action: DONOTHING @@ -41,15 +38,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 reward_function: reward_components: @@ -120,28 +108,10 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - action_map: 0: action: DONOTHING options: {} - options: - nodes: - - node_name: switch - - node_name: client_1 - - node_name: client_2 - - node_name: client_3 - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.23 reward_function: reward_components: diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index 18a9724b..efe4428a 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -20,13 +20,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_INSTALL - - type: CONFIGURE_DATABASE_CLIENT - - type: CONFIGURE_DOSBOT - - type: CONFIGURE_RANSOMWARE_SCRIPT - - type: NODE_APPLICATION_REMOVE action_map: 0: action: DONOTHING @@ -83,12 +76,6 @@ agents: options: node_id: 1 application_name: DatabaseClient - options: - nodes: - - node_name: client_1 - - node_name: client_2 - - node_name: client_3 - ip_list: [] reward_function: reward_components: - type: DUMMY diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 13cffab1..9f2cbd84 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -28,19 +28,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -77,19 +64,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -127,24 +101,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - - type: NODE_FILE_DELETE - - type: NODE_FILE_CORRUPT - - type: NODE_OS_SCAN - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - reward_function: reward_components: - type: DUMMY @@ -228,35 +184,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -675,43 +602,6 @@ agents: node_id: 6 nic_id: 0 - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY @@ -808,39 +698,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - options: - target_router: router_1 - - type: ROUTER_ACL_REMOVERULE - options: - target_router: router_1 - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -1260,42 +1117,6 @@ agents: nic_id: 0 - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml index ec50ecdf..a4deff6f 100644 --- a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml +++ b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml @@ -24,17 +24,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - options: - nodes: - - node_name: client_1 - applications: - - application_name: NMAP - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 - action_list: - - type: node_network_service_recon action_map: 0: action: node_network_service_recon diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index eb7b6752..ee6de2c5 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -24,17 +24,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - options: - nodes: - - node_name: client_1 - applications: - - application_name: NMAP - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 - action_list: - - type: node_nmap_ping_scan action_map: 0: action: node_nmap_ping_scan diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 15e2cb6a..47d34e52 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -24,17 +24,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - options: - nodes: - - node_name: client_1 - applications: - - application_name: NMAP - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 - action_list: - - type: node_nmap_port_scan action_map: 0: action: node_nmap_port_scan diff --git a/tests/assets/configs/scenario_with_placeholders/greens_1.yaml b/tests/assets/configs/scenario_with_placeholders/greens_1.yaml index 98d2392a..ce670f5f 100644 --- a/tests/assets/configs/scenario_with_placeholders/greens_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/greens_1.yaml @@ -8,14 +8,6 @@ agents: &greens 1: 0.8 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DatabaseClient action_map: 0: action: DONOTHING diff --git a/tests/assets/configs/scenario_with_placeholders/greens_2.yaml b/tests/assets/configs/scenario_with_placeholders/greens_2.yaml index 17a5977b..9ff099dd 100644 --- a/tests/assets/configs/scenario_with_placeholders/greens_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/greens_2.yaml @@ -8,14 +8,6 @@ agents: &greens 1: 0.05 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DatabaseClient action_map: 0: action: DONOTHING diff --git a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml index b775cb24..b7e7560d 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml @@ -6,14 +6,6 @@ reds: &reds observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DataManipulationBot reward_function: reward_components: diff --git a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml index 4cae1ec6..1d9012d7 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml @@ -6,14 +6,6 @@ reds: &reds observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client - applications: - - application_name: DataManipulationBot reward_function: reward_components: diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index ef930a1a..a61af830 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -54,12 +54,6 @@ agents: - server:eth-1<->switch_1:eth-2 action_space: - action_list: - - type: DONOTHING - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE action_map: 0: action: DONOTHING @@ -100,19 +94,6 @@ agents: options: node_id: 1 nic_id: 0 - options: - nodes: - - node_name: client - - node_name: server - - max_folders_per_node: 0 - max_files_per_folder: 0 - max_services_per_node: 0 - max_nics_per_node: 1 - max_acl_rules: 0 - ip_list: - - 192.168.1.2 - - 192.168.1.3 reward_function: reward_components: diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 3ba480ea..60e22366 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -31,19 +31,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -80,19 +67,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: action: DONOTHING @@ -126,20 +100,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -224,35 +184,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -671,44 +602,6 @@ agents: node_id: 6 nic_id: 0 - - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: SHARED_REWARD diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index d57b88dd..006328ba 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -29,9 +29,6 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE action_map: 0: action: DONOTHING @@ -41,15 +38,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 1 reward_function: reward_components: @@ -120,28 +108,10 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - action_map: 0: action: DONOTHING options: {} - options: - nodes: - - node_name: switch - - node_name: client_1 - - node_name: client_2 - - node_name: client_3 - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.23 reward_function: reward_components: diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index e8b080b7..c085fd63 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -31,9 +31,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE options: nodes: - node_name: client_2 @@ -80,9 +77,6 @@ agents: 2: 0.1 observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE options: nodes: - node_name: client_1 @@ -130,20 +124,7 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - - node_name: client_2 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 + action_map: reward_function: reward_components: @@ -228,39 +209,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - - type: NODE_APPLICATION_INSTALL - - type: NODE_APPLICATION_REMOVE - - type: NODE_APPLICATION_EXECUTE - - type: CONFIGURE_DOSBOT - action_map: 0: action: DONOTHING @@ -706,47 +654,6 @@ agents: target_ip_address: 192.168.1.14 target_port: POSTGRES_SERVER - - - - options: - nodes: - - node_name: domain_controller - applications: - - application_name: DoSBot - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index c59bbcbf..8c22fbed 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -23,21 +23,10 @@ agents: type: ProbabilisticAgent observation_space: null action_space: - action_list: - - type: DONOTHING action_map: 0: action: DONOTHING options: {} - options: - nodes: - - node_name: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 - reward_function: reward_components: - type: DUMMY @@ -56,12 +45,6 @@ agents: observation_space: null action_space: - action_list: - - type: DONOTHING - - type: NODE_APPLICATION_EXECUTE - - type: NODE_FILE_DELETE - - type: NODE_FILE_CORRUPT - - type: NODE_OS_SCAN action_map: 0: action: DONOTHING @@ -71,14 +54,6 @@ agents: options: node_id: 0 application_id: 0 - options: - nodes: - - node_name: client_1 - applications: - - application_name: DataManipulationBot - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 reward_function: reward_components: @@ -163,35 +138,6 @@ agents: options: {} action_space: - action_list: - - type: DONOTHING - - type: NODE_SERVICE_SCAN - - type: NODE_SERVICE_STOP - - type: NODE_SERVICE_START - - type: NODE_SERVICE_PAUSE - - type: NODE_SERVICE_RESUME - - type: NODE_SERVICE_RESTART - - type: NODE_SERVICE_DISABLE - - type: NODE_SERVICE_ENABLE - - type: NODE_SERVICE_FIX - - type: NODE_FILE_SCAN - - type: NODE_FILE_CHECKHASH - - type: NODE_FILE_DELETE - - type: NODE_FILE_REPAIR - - type: NODE_FILE_RESTORE - - type: NODE_FOLDER_SCAN - - type: NODE_FOLDER_CHECKHASH - - type: NODE_FOLDER_REPAIR - - type: NODE_FOLDER_RESTORE - - type: NODE_OS_SCAN - - type: NODE_SHUTDOWN - - type: NODE_STARTUP - - type: NODE_RESET - - type: ROUTER_ACL_ADDRULE - - type: ROUTER_ACL_REMOVERULE - - type: HOST_NIC_ENABLE - - type: HOST_NIC_DISABLE - action_map: 0: action: DONOTHING @@ -513,42 +459,6 @@ agents: node_id: 6 nic_id: 0 - - options: - nodes: - - node_name: domain_controller - - node_name: web_server - applications: - - application_name: DatabaseClient - services: - - service_name: WebServer - - node_name: database_server - folders: - - folder_name: database - files: - - file_name: database.db - services: - - service_name: DatabaseService - - node_name: backup_server - - node_name: security_suite - - node_name: client_1 - - node_name: client_2 - - max_folders_per_node: 2 - max_files_per_folder: 2 - max_services_per_node: 2 - max_nics_per_node: 8 - max_acl_rules: 10 - ip_list: - - 192.168.1.10 - - 192.168.1.12 - - 192.168.1.14 - - 192.168.1.16 - - 192.168.1.110 - - 192.168.10.21 - - 192.168.10.22 - - 192.168.10.110 - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index 79cf7e4b..cb2bb7a2 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -3,9 +3,7 @@ from unittest.mock import Mock import pytest -from primaite.game.agent.actions import ( # DoNothingAction,; NodeServiceDisableAction,; NodeServiceEnableAction,; NodeServicePauseAction,; NodeServiceRestartAction,; NodeServiceResumeAction,; NodeServiceScanAction,; NodeServiceStartAction,; NodeServiceStopAction, - ActionManager, -) +from primaite.game.agent.actions import ActionManager from primaite.game.agent.actions.manager import DoNothingAction from primaite.game.agent.actions.service import ( NodeServiceDisableAction, diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 7824e71e..94a77a10 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -27,26 +27,6 @@ def test_probabilistic_agent(): MAX_NODE_FILE_DELETE = 6250 action_space_cfg = { - "action_list": [ - {"type": "do_nothing"}, - {"type": "node_application_execute"}, - {"type": "node_file_delete"}, - ], - "nodes": [ - { - "node_name": "client_1", - "applications": [{"application_name": "WebBrowser"}], - "folders": [{"folder_name": "downloads", "files": [{"file_name": "cat.png"}]}], - }, - ], - "max_folders_per_node": 2, - "max_files_per_folder": 2, - "max_services_per_node": 2, - "max_applications_per_node": 2, - "max_nics_per_node": 2, - "max_acl_rules": 10, - "protocols": ["TCP", "UDP", "ICMP"], - "ports": ["HTTP", "DNS", "ARP"], "act_map": { 0: {"action": "do_nothing", "options": {}}, 1: {"action": "node_application_execute", "options": {"node_id": 0, "application_id": 0}}, @@ -65,8 +45,8 @@ def test_probabilistic_agent(): pa_config = { "type": "ProbabilisticAgent", "game": game, - "action_manager": action_space_cfg, - "observation_manager": observation_space_cfg, + "action_space": action_space_cfg, + "observation_space": observation_space_cfg, "reward_function": reward_function_cfg, "agent_settings": { "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, From b4b6c16872622e79176849f6d43ac1bc8863c434 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 15 Jan 2025 10:08:14 +0000 Subject: [PATCH 130/224] #2869 - Make observation and action managers use config schemas --- src/primaite/game/agent/actions/manager.py | 49 ++++++------- src/primaite/game/agent/interface.py | 52 +++++++------- .../agent/observations/observation_manager.py | 72 ++++++++++++++++--- 3 files changed, 115 insertions(+), 58 deletions(-) diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 400d30e4..0f7db2f3 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -13,9 +13,10 @@ agents: from __future__ import annotations -from typing import Dict, Optional, Tuple +from typing import Dict, Tuple from gymnasium import spaces +from pydantic import BaseModel, ConfigDict, Field, field_validator # from primaite.game.game import PrimaiteGame # TODO: Breaks things from primaite.game.agent.actions.abstract import AbstractAction @@ -38,35 +39,35 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): return ["do_nothing"] -class ActionManager: +class ActionManager(BaseModel): """Class which manages the action space for an agent.""" - def __init__(self, act_map: Optional[Dict[int, Dict]] = None) -> None: - """Init method for ActionManager. + class ConfigSchema(BaseModel): + """Config Schema for ActionManager.""" - :param act_map: Action map which maps integers to actions. Used for restricting the set of possible actions. - :type act_map: Optional[Dict[int, Dict]] - """ - self.action_map: Dict[int, Tuple[str, Dict]] = {} - """ - Action mapping that converts an integer to a specific action and parameter choice. + model_config = ConfigDict(extra="forbid") + action_map: Dict[int, Tuple[str, Dict]] = {} + """Mapping between integer action choices and CAOS actions.""" - For example : - {0: ("node_service_scan", {node_name:"client_1", service_name:"WebBrowser"})} - """ - # allows restricting set of possible actions - TODO: Refactor to be a list? - if act_map is None: - # raise RuntimeError("Action map must be specified in the config file.") - pass - else: - self.action_map = {i: (a["action"], a["options"]) for i, a in act_map.items()} - # make sure all numbers between 0 and N are represented as dict keys in action map - assert all([i in self.action_map.keys() for i in range(len(self.action_map))]) + @field_validator("action_map", mode="after") + def consecutive_action_nums(cls, v: Dict) -> Dict: + """Make sure all numbers between 0 and N are represented as dict keys in action map.""" + assert all([i in v.keys() for i in range(len(v))]) + + config: ActionManager.ConfigSchema = Field(default_factory=lambda: ActionManager.ConfigSchema()) + + @property + def action_map(self) -> Dict[int, Tuple[str, Dict]]: + """Convenience method for accessing the action map.""" + return self.config.action_map def get_action(self, action: int) -> Tuple[str, Dict]: - """Produce action in CAOS format.""" - """the agent chooses an action (as an integer), this is converted into an action in CAOS format""" - """The CAOS format is basically a action identifier, followed by parameters stored in a dictionary""" + """ + Produce action in CAOS format. + + The agent chooses an action (as an integer), this is converted into an action in CAOS format + The CAOS format is basically an action identifier, followed by parameters stored in a dictionary. + """ act_identifier, act_options = self.action_map[action] return act_identifier, act_options diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 9d8f3f63..0b55c1db 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -2,7 +2,7 @@ """Interface for agents.""" from __future__ import annotations -from abc import abstractmethod +from abc import ABC, abstractmethod from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING from gymnasium.core import ActType, ObsType @@ -43,35 +43,31 @@ class AgentHistoryItem(BaseModel): reward_info: Dict[str, Any] = {} -class AbstractAgent(BaseModel): +class AbstractAgent(BaseModel, ABC): """Base class for scripted and RL agents.""" - _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} - logger: AgentLog = AgentLog(agent_name="Abstract_Agent") - - history: List[AgentHistoryItem] = [] - config: "AbstractAgent.ConfigSchema" = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) - action_manager: "ActionManager" - observation_manager: "ObservationManager" - reward_function: "RewardFunction" model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - class ConfigSchema(BaseModel): - """ - Configuration Schema for AbstractAgents. - - :param type: Type of agent being generated. - :type type: str - :param observation_space: Observation space for the agent. - :type observation_space: Optional[ObservationSpace] - :param reward_function: Reward function for the agent. - :type reward_function: Optional[RewardFunction] - :param agent_settings: Configurable Options for Abstracted Agents. - :type agent_settings: Optional[AgentSettings] - """ + class ConfigSchema(BaseModel, ABC): + """Configuration Schema for AbstractAgents.""" model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) type: str = "AbstractAgent" + action_space: ActionManager.ConfigSchema = Field(default_factory=lambda: ActionManager.ConfigSchema()) + observation_space: ObservationManager.ConfigSchema = Field( + default_factory=lambda: ObservationManager.ConfigSchema() + ) + reward_function: RewardFunction.ConfigSchema = Field(default_factory=lambda: RewardFunction.ConfigSchema()) + + config: "AbstractAgent.ConfigSchema" = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) + + logger: AgentLog = AgentLog(agent_name="Abstract_Agent") + history: List[AgentHistoryItem] = [] + action_manager: "ActionManager" + observation_manager: "ObservationManager" + reward_function: "RewardFunction" + + _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -81,6 +77,14 @@ class AbstractAgent(BaseModel): raise ValueError(f"Cannot create a new agent under reserved name {identifier}") cls._registry[identifier] = cls + def __init__(self, config: ConfigSchema, **kwargs): + kwargs["action_manager"] = kwargs.get("action_manager") or ActionManager.from_config(config.action_space) + kwargs["observation_manager"] = kwargs.get("observation_manager") or ObservationManager( + config.observation_space + ) + kwargs["reward_function"] = kwargs.get("reward_function") or RewardFunction.from_config(config.reward_function) + super().__init__(config=config, **kwargs) + @property def flatten_obs(self) -> bool: """Return agent flatten_obs param.""" @@ -94,7 +98,7 @@ class AbstractAgent(BaseModel): obj = cls( config=cls.ConfigSchema(**config["agent_settings"]), action_manager=ActionManager.from_config(config["action_space"]), - observation_manager=ObservationManager.from_config(config["observation_space"]), + observation_manager=ObservationManager(config["observation_space"]), reward_function=RewardFunction.from_config(config["reward_function"]), ) return obj diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 71a60433..6964ce2c 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -1,11 +1,12 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations +from functools import cached_property from typing import Any, Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType -from pydantic import BaseModel, ConfigDict, model_validator, ValidationError +from pydantic import BaseModel, computed_field, ConfigDict, Field, model_validator, ValidationError from primaite.game.agent.observations.observations import AbstractObservation, WhereType @@ -140,7 +141,7 @@ class NullObservation(AbstractObservation, identifier="NONE"): return cls() -class ObservationManager: +class ObservationManager(BaseModel): """ Manage the observations of an Agent. @@ -150,15 +151,66 @@ class ObservationManager: 3. Formatting this information so an agent can use it to make decisions. """ - def __init__(self, obs: AbstractObservation) -> None: - """Initialise observation space. + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - :param observation: Observation object - :type observation: AbstractObservation - """ - self.obs: AbstractObservation = obs - self.current_observation: ObsType - """Cached copy of the observation at the time it was most recently calculated.""" + class ConfigSchema(BaseModel): + """Config Schema for Observation Manager.""" + + model_config = ConfigDict(extra="forbid") + type: str = "NONE" + """Identifier name for the top-level observation.""" + options: AbstractObservation.ConfigSchema = Field( + default_factory=lambda: NullObservation.ConfigSchema(), validate_default=True + ) + """Options to pass into the top-level observation during creation.""" + + @model_validator(mode="before") + @classmethod + def resolve_obs_options_type(cls, data: Any) -> Any: + """ + When constructing the model from a dict, resolve the correct observation class based on `type` field. + + Workaround: The `options` field is statically typed as AbstractObservation. Therefore, it falls over when + passing in data that adheres to a subclass schema rather than the plain AbstractObservation schema. There is + a way to do this properly using discriminated union, but most advice on the internet assumes that the full + list of types between which to discriminate is known ahead-of-time. That is not the case for us, because of + our plugin architecture. + + We may be able to revisit and implement a better solution when needed using the following resources as + research starting points: + https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions + https://github.com/pydantic/pydantic/issues/7366 + https://github.com/pydantic/pydantic/issues/7462 + https://github.com/pydantic/pydantic/pull/7983 + """ + if not isinstance(data, dict): + return data + + # (TODO: duplicate default definition between here and the actual model) + obs_type = data["type"] if "type" in data else "NONE" + obs_class = AbstractObservation._registry[obs_type] + + # if no options are passed in, try to create a default schema. Only works if there are no mandatory fields + if "options" not in data: + data["options"] = obs_class.ConfigSchema() + + # if options passed as a dict, convert to a schema + elif isinstance(data["options"], dict): + data["options"] = obs_class.ConfigSchema(**data["options"]) + + return data + + config: ConfigSchema = Field(default_factory=lambda: ObservationManager.ConfigSchema()) + + current_observation: ObsType = 0 + + @computed_field + @cached_property + def obs(self) -> AbstractObservation: + """Create the main observation component for the observation manager from the config.""" + obs_class = AbstractObservation._registry[self.config.type] + obs_instance = obs_class.from_config(config=self.config.options) + return obs_instance def update(self, state: Dict) -> Dict: """ From 582e7cfec784fdfaf240eeb28d58218a89b9d7eb Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 15 Jan 2025 11:21:18 +0000 Subject: [PATCH 131/224] #2887 - Initial commit of Node refactor for extensibility in version 4.0.0. Addition of ConfigSchema and changes to how Nodes are generated within Game.py --- src/primaite/game/game.py | 131 +++++++++--------- .../simulator/network/hardware/base.py | 110 ++++++++------- .../network/hardware/nodes/host/computer.py | 9 ++ .../network/hardware/nodes/host/host_node.py | 9 ++ .../hardware/nodes/network/firewall.py | 7 + .../network/hardware/nodes/network/router.py | 20 ++- .../network/hardware/nodes/network/switch.py | 15 +- .../hardware/nodes/network/wireless_router.py | 9 +- 8 files changed, 185 insertions(+), 125 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f369bc2b..48d9df87 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -19,14 +19,8 @@ from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT from primaite.simulator.network.creation import NetworkNodeAdder from primaite.simulator.network.hardware.base import NetworkInterface, Node, NodeOperatingState, UserManager -from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC -from primaite.simulator.network.hardware.nodes.host.server import Printer, Server -from primaite.simulator.network.hardware.nodes.network.firewall import Firewall -from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode -from primaite.simulator.network.hardware.nodes.network.router import Router +from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application @@ -277,68 +271,73 @@ class PrimaiteGame: for node_cfg in nodes_cfg: n_type = node_cfg["type"] + node_config: dict = node_cfg["config"] new_node = None + if n_type in Node._registry: + # simplify down Node creation: + new_node = Node._registry["n_type"].from_config(config=node_config) + # Default PrimAITE nodes - if n_type == "computer": - new_node = Computer( - hostname=node_cfg["hostname"], - ip_address=node_cfg["ip_address"], - subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - default_gateway=node_cfg.get("default_gateway"), - dns_server=node_cfg.get("dns_server", None), - operating_state=NodeOperatingState.ON - if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - elif n_type == "server": - new_node = Server( - hostname=node_cfg["hostname"], - ip_address=node_cfg["ip_address"], - subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - default_gateway=node_cfg.get("default_gateway"), - dns_server=node_cfg.get("dns_server", None), - operating_state=NodeOperatingState.ON - if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - elif n_type == "switch": - new_node = Switch( - hostname=node_cfg["hostname"], - num_ports=int(node_cfg.get("num_ports", "8")), - operating_state=NodeOperatingState.ON - if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - elif n_type == "router": - new_node = Router.from_config(node_cfg) - elif n_type == "firewall": - new_node = Firewall.from_config(node_cfg) - elif n_type == "wireless_router": - new_node = WirelessRouter.from_config(node_cfg, airspace=net.airspace) - elif n_type == "printer": - new_node = Printer( - hostname=node_cfg["hostname"], - ip_address=node_cfg["ip_address"], - subnet_mask=node_cfg["subnet_mask"], - operating_state=NodeOperatingState.ON - if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - # Handle extended nodes - elif n_type.lower() in Node._registry: - new_node = HostNode._registry[n_type]( - hostname=node_cfg["hostname"], - ip_address=node_cfg.get("ip_address"), - subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - default_gateway=node_cfg.get("default_gateway"), - dns_server=node_cfg.get("dns_server", None), - operating_state=NodeOperatingState.ON - if not (p := node_cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - elif n_type in NetworkNode._registry: - new_node = NetworkNode._registry[n_type](**node_cfg) + # if n_type == "computer": + # new_node = Computer( + # hostname=node_cfg["hostname"], + # ip_address=node_cfg["ip_address"], + # subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), + # default_gateway=node_cfg.get("default_gateway"), + # dns_server=node_cfg.get("dns_server", None), + # operating_state=NodeOperatingState.ON + # if not (p := node_cfg.get("operating_state")) + # else NodeOperatingState[p.upper()], + # ) + # elif n_type == "server": + # new_node = Server( + # hostname=node_cfg["hostname"], + # ip_address=node_cfg["ip_address"], + # subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), + # default_gateway=node_cfg.get("default_gateway"), + # dns_server=node_cfg.get("dns_server", None), + # operating_state=NodeOperatingState.ON + # if not (p := node_cfg.get("operating_state")) + # else NodeOperatingState[p.upper()], + # ) + # elif n_type == "switch": + # new_node = Switch( + # hostname=node_cfg["hostname"], + # num_ports=int(node_cfg.get("num_ports", "8")), + # operating_state=NodeOperatingState.ON + # if not (p := node_cfg.get("operating_state")) + # else NodeOperatingState[p.upper()], + # ) + # elif n_type == "router": + # new_node = Router.from_config(node_cfg) + # elif n_type == "firewall": + # new_node = Firewall.from_config(node_cfg) + # elif n_type == "wireless_router": + # new_node = WirelessRouter.from_config(node_cfg, airspace=net.airspace) + # elif n_type == "printer": + # new_node = Printer( + # hostname=node_cfg["hostname"], + # ip_address=node_cfg["ip_address"], + # subnet_mask=node_cfg["subnet_mask"], + # operating_state=NodeOperatingState.ON + # if not (p := node_cfg.get("operating_state")) + # else NodeOperatingState[p.upper()], + # ) + # # Handle extended nodes + # elif n_type.lower() in Node._registry: + # new_node = HostNode._registry[n_type]( + # hostname=node_cfg["hostname"], + # ip_address=node_cfg.get("ip_address"), + # subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), + # default_gateway=node_cfg.get("default_gateway"), + # dns_server=node_cfg.get("dns_server", None), + # operating_state=NodeOperatingState.ON + # if not (p := node_cfg.get("operating_state")) + # else NodeOperatingState[p.upper()], + # ) + # elif n_type in NetworkNode._registry: + # new_node = NetworkNode._registry[n_type](**node_cfg) else: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 8324715f..b003009b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1469,7 +1469,7 @@ class UserSessionManager(Service): return self.local_session is not None -class Node(SimComponent): +class Node(SimComponent, ABC): """ A basic Node class that represents a node on the network. @@ -1492,7 +1492,6 @@ class Node(SimComponent): "The Network Interfaces on the node by port id." dns_server: Optional[IPv4Address] = None "List of IP addresses of DNS servers used for name resolution." - accounts: Dict[str, Account] = {} "All accounts on the node." applications: Dict[str, Application] = {} @@ -1509,33 +1508,6 @@ class Node(SimComponent): session_manager: SessionManager software_manager: SoftwareManager - revealed_to_red: bool = False - "Informs whether the node has been revealed to a red agent." - - start_up_duration: int = 3 - "Time steps needed for the node to start up." - - start_up_countdown: int = 0 - "Time steps needed until node is booted up." - - shut_down_duration: int = 3 - "Time steps needed for the node to shut down." - - shut_down_countdown: int = 0 - "Time steps needed until node is shut down." - - is_resetting: bool = False - "If true, the node will try turning itself off then back on again." - - node_scan_duration: int = 10 - "How many timesteps until the whole node is scanned. Default 10 time steps." - - node_scan_countdown: int = 0 - "Time steps until scan is complete" - - red_scan_countdown: int = 0 - "Time steps until reveal to red scan is complete." - SYSTEM_SOFTWARE: ClassVar[Dict[str, Type[Software]]] = {} "Base system software that must be preinstalled." @@ -1545,6 +1517,46 @@ class Node(SimComponent): _identifier: ClassVar[str] = "unknown" """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" + config: Node.ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) + + class ConfigSchema: + """Configuration Schema for Node based classes.""" + + revealed_to_red: bool = False + "Informs whether the node has been revealed to a red agent." + + start_up_duration: int = 3 + "Time steps needed for the node to start up." + + start_up_countdown: int = 0 + "Time steps needed until node is booted up." + + shut_down_duration: int = 3 + "Time steps needed for the node to shut down." + + shut_down_countdown: int = 0 + "Time steps needed until node is shut down." + + is_resetting: bool = False + "If true, the node will try turning itself off then back on again." + + node_scan_duration: int = 10 + "How many timesteps until the whole node is scanned. Default 10 time steps." + + node_scan_countdown: int = 0 + "Time steps until scan is complete" + + red_scan_countdown: int = 0 + "Time steps until reveal to red scan is complete." + + def from_config(cls, config: Dict) -> Node: + """Create Node object from a given configuration.""" + if config["type"] not in cls._registry: + msg = f"Configuration contains an invalid Node type: {config['type']}" + return ValueError(msg) + obj = cls(config=cls.ConfigSchema(**config)) + return obj + def __init_subclass__(cls, identifier: str = "default", **kwargs: Any) -> None: """ Register a node type. @@ -1850,7 +1862,7 @@ class Node(SimComponent): "applications": {app.name: app.describe_state() for app in self.applications.values()}, "services": {svc.name: svc.describe_state() for svc in self.services.values()}, "process": {proc.name: proc.describe_state() for proc in self.processes.values()}, - "revealed_to_red": self.revealed_to_red, + "revealed_to_red": self.config.revealed_to_red, } ) return state @@ -1928,8 +1940,8 @@ class Node(SimComponent): network_interface.apply_timestep(timestep=timestep) # count down to boot up - if self.start_up_countdown > 0: - self.start_up_countdown -= 1 + if self.config.start_up_countdown > 0: + self.config.start_up_countdown -= 1 else: if self.operating_state == NodeOperatingState.BOOTING: self.operating_state = NodeOperatingState.ON @@ -1940,8 +1952,8 @@ class Node(SimComponent): self._start_up_actions() # count down to shut down - if self.shut_down_countdown > 0: - self.shut_down_countdown -= 1 + if self.config.shut_down_countdown > 0: + self.config.shut_down_countdown -= 1 else: if self.operating_state == NodeOperatingState.SHUTTING_DOWN: self.operating_state = NodeOperatingState.OFF @@ -1949,17 +1961,17 @@ class Node(SimComponent): self._shut_down_actions() # if resetting turn back on - if self.is_resetting: - self.is_resetting = False + if self.config.is_resetting: + self.config.is_resetting = False self.power_on() # time steps which require the node to be on if self.operating_state == NodeOperatingState.ON: # node scanning - if self.node_scan_countdown > 0: - self.node_scan_countdown -= 1 + if self.config.node_scan_countdown > 0: + self.config.node_scan_countdown -= 1 - if self.node_scan_countdown == 0: + if self.config.node_scan_countdown == 0: # scan everything! for process_id in self.processes: self.processes[process_id].scan() @@ -1975,10 +1987,10 @@ class Node(SimComponent): # scan file system self.file_system.scan(instant_scan=True) - if self.red_scan_countdown > 0: - self.red_scan_countdown -= 1 + if self.config.red_scan_countdown > 0: + self.config.red_scan_countdown -= 1 - if self.red_scan_countdown == 0: + if self.config.red_scan_countdown == 0: # scan processes for process_id in self.processes: self.processes[process_id].reveal_to_red() @@ -2035,7 +2047,7 @@ class Node(SimComponent): to the red agent. """ - self.node_scan_countdown = self.node_scan_duration + self.config.node_scan_countdown = self.config.node_scan_duration return True def reveal_to_red(self) -> bool: @@ -2051,12 +2063,12 @@ class Node(SimComponent): `revealed_to_red` to `True`. """ - self.red_scan_countdown = self.node_scan_duration + self.config.red_scan_countdown = self.config.node_scan_duration return True def power_on(self) -> bool: """Power on the Node, enabling its NICs if it is in the OFF state.""" - if self.start_up_duration <= 0: + if self.config.start_up_duration <= 0: self.operating_state = NodeOperatingState.ON self._start_up_actions() self.sys_log.info("Power on") @@ -2065,14 +2077,14 @@ class Node(SimComponent): return True if self.operating_state == NodeOperatingState.OFF: self.operating_state = NodeOperatingState.BOOTING - self.start_up_countdown = self.start_up_duration + self.config.start_up_countdown = self.config.start_up_duration return True return False def power_off(self) -> bool: """Power off the Node, disabling its NICs if it is in the ON state.""" - if self.shut_down_duration <= 0: + if self.config.shut_down_duration <= 0: self._shut_down_actions() self.operating_state = NodeOperatingState.OFF self.sys_log.info("Power off") @@ -2081,7 +2093,7 @@ class Node(SimComponent): for network_interface in self.network_interfaces.values(): network_interface.disable() self.operating_state = NodeOperatingState.SHUTTING_DOWN - self.shut_down_countdown = self.shut_down_duration + self.config.shut_down_countdown = self.config.shut_down_duration return True return False @@ -2093,7 +2105,7 @@ class Node(SimComponent): Applying more timesteps will eventually turn the node back on. """ if self.operating_state.ON: - self.is_resetting = True + self.config.is_resetting = True self.sys_log.info("Resetting") self.power_off() return True diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 11b925b9..1fb63b2e 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -1,6 +1,8 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from typing import ClassVar, Dict +from pydantic import Field + from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.system.services.ftp.ftp_client import FTPClient @@ -35,4 +37,11 @@ class Computer(HostNode, identifier="computer"): SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} + config: "Computer.ConfigSchema" = Field(default_factory=lambda: Computer.ConfigSchema()) + + class ConfigSchema(HostNode.ConfigSchema): + """Configuration Schema for Computer class.""" + + pass + pass diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index c51afbca..fa73bf10 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -4,6 +4,8 @@ from __future__ import annotations from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Optional +from pydantic import Field + from primaite import getLogger from primaite.simulator.network.hardware.base import ( IPWiredNetworkInterface, @@ -325,6 +327,13 @@ class HostNode(Node, identifier="HostNode"): network_interface: Dict[int, NIC] = {} "The NICs on the node by port id." + config: HostNode.ConfigSchema = Field(default_factory=lambda: HostNode.ConfigSchema()) + + class ConfigSchema(Node.ConfigSchema): + """Configuration Schema for HostNode class.""" + + pass + def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask)) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index f1ca4930..0a397c49 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -99,6 +99,13 @@ class Firewall(Router, identifier="firewall"): ) """Access Control List for managing traffic leaving towards an external network.""" + config: "Firewall.ConfigSchema" = Field(default_factory=lambda: Firewall.ConfigSchema()) + + class ConfigSchema(Router.ConfigSChema): + """Configuration Schema for Firewall 'Nodes' within PrimAITE.""" + + pass + def __init__(self, hostname: str, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(hostname) diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 4a049f99..132b1462 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import validate_call +from pydantic import Field, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -1207,7 +1207,6 @@ class Router(NetworkNode, identifier="router"): "Terminal": Terminal, } - num_ports: int network_interfaces: Dict[str, RouterInterface] = {} "The Router Interfaces on the node." network_interface: Dict[int, RouterInterface] = {} @@ -1215,6 +1214,15 @@ class Router(NetworkNode, identifier="router"): acl: AccessControlList route_table: RouteTable + config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSChema()) + + class ConfigSChema(NetworkNode.ConfigSchema): + """Configuration Schema for Router Objects.""" + + num_ports: int = 10 + hostname: str = "Router" + ports: list = [] + def __init__(self, hostname: str, num_ports: int = 5, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(hostname) @@ -1227,11 +1235,11 @@ class Router(NetworkNode, identifier="router"): self.session_manager.node = self self.software_manager.session_manager = self.session_manager self.session_manager.software_manager = self.software_manager - for i in range(1, self.num_ports + 1): + for i in range(1, self.config.num_ports + 1): network_interface = RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") self.connect_nic(network_interface) self.network_interface[i] = network_interface - + self.operating_state = NodeOperatingState.ON self._set_default_acl() def _install_system_software(self): @@ -1337,7 +1345,7 @@ class Router(NetworkNode, identifier="router"): :return: A dictionary representing the current state. """ state = super().describe_state() - state["num_ports"] = self.num_ports + state["num_ports"] = self.config.num_ports state["acl"] = self.acl.describe_state() return state @@ -1558,6 +1566,8 @@ class Router(NetworkNode, identifier="router"): ) print(table) + # TODO: Remove - Cover normal config items with ConfigSchema. Move additional setup components to __init__ ? + @classmethod def from_config(cls, cfg: dict, **kwargs) -> "Router": """Create a router based on a config dict. diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index db923f1a..b73af3cb 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable +from pydantic import Field from primaite import getLogger from primaite.exceptions import NetworkError @@ -94,8 +95,6 @@ class Switch(NetworkNode, identifier="switch"): :ivar num_ports: The number of ports on the switch. Default is 24. """ - num_ports: int = 24 - "The number of ports on the switch." network_interfaces: Dict[str, SwitchPort] = {} "The SwitchPorts on the Switch." network_interface: Dict[int, SwitchPort] = {} @@ -103,9 +102,17 @@ class Switch(NetworkNode, identifier="switch"): mac_address_table: Dict[str, SwitchPort] = {} "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." + config: "Switch.ConfigSchema" = Field(default_factory=lambda: Switch.ConfigSchema()) + + class ConfigSchema(NetworkNode.ConfigSchema): + """Configuration Schema for Switch nodes within PrimAITE.""" + + num_ports: int = 24 + "The number of ports on the switch." + def __init__(self, **kwargs): super().__init__(**kwargs) - for i in range(1, self.num_ports + 1): + for i in range(1, self.config.num_ports + 1): self.connect_nic(SwitchPort()) def _install_system_software(self): @@ -134,7 +141,7 @@ class Switch(NetworkNode, identifier="switch"): """ state = super().describe_state() state["ports"] = {port_num: port.describe_state() for port_num, port in self.network_interface.items()} - state["num_ports"] = self.num_ports # redundant? + state["num_ports"] = self.config.num_ports # redundant? state["mac_address_table"] = {mac: port.port_num for mac, port in self.mac_address_table.items()} return state diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 804a570e..0a527ab8 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -2,7 +2,7 @@ from ipaddress import IPv4Address from typing import Any, Dict, Optional, Union -from pydantic import validate_call +from pydantic import Field, validate_call from primaite.simulator.network.airspace import AirSpace, AirSpaceFrequency, FREQ_WIFI_2_4, IPWirelessNetworkInterface from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState @@ -124,6 +124,13 @@ class WirelessRouter(Router, identifier="wireless_router"): network_interface: Dict[int, Union[RouterInterface, WirelessAccessPoint]] = {} airspace: AirSpace + config: "WirelessRouter.ConfigSchema" = Field(default_factory=lambda: WirelessRouter.Configschema()) + + class ConfigSchema(Router.ConfigSChema): + """Configuration Schema for WirelessRouter nodes within PrimAITE.""" + + pass + def __init__(self, hostname: str, airspace: AirSpace, **kwargs): super().__init__(hostname=hostname, num_ports=0, airspace=airspace, **kwargs) From 70d9fe2fd97317f2516b1629ca222da4e8008147 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 15 Jan 2025 16:33:11 +0000 Subject: [PATCH 132/224] #2887 - End of day commit. Updates to ConfigSchema inheritance, and some initials changes to Router to remove the custom from_config method --- src/primaite/game/game.py | 61 ---------- .../simulator/network/hardware/base.py | 18 +-- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../hardware/nodes/network/firewall.py | 9 +- .../network/hardware/nodes/network/router.py | 112 ++++-------------- .../network/hardware/nodes/network/switch.py | 3 +- .../hardware/nodes/network/wireless_router.py | 4 +- tests/conftest.py | 3 + 9 files changed, 46 insertions(+), 168 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 48d9df87..6599430a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -277,67 +277,6 @@ class PrimaiteGame: if n_type in Node._registry: # simplify down Node creation: new_node = Node._registry["n_type"].from_config(config=node_config) - - # Default PrimAITE nodes - # if n_type == "computer": - # new_node = Computer( - # hostname=node_cfg["hostname"], - # ip_address=node_cfg["ip_address"], - # subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - # default_gateway=node_cfg.get("default_gateway"), - # dns_server=node_cfg.get("dns_server", None), - # operating_state=NodeOperatingState.ON - # if not (p := node_cfg.get("operating_state")) - # else NodeOperatingState[p.upper()], - # ) - # elif n_type == "server": - # new_node = Server( - # hostname=node_cfg["hostname"], - # ip_address=node_cfg["ip_address"], - # subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - # default_gateway=node_cfg.get("default_gateway"), - # dns_server=node_cfg.get("dns_server", None), - # operating_state=NodeOperatingState.ON - # if not (p := node_cfg.get("operating_state")) - # else NodeOperatingState[p.upper()], - # ) - # elif n_type == "switch": - # new_node = Switch( - # hostname=node_cfg["hostname"], - # num_ports=int(node_cfg.get("num_ports", "8")), - # operating_state=NodeOperatingState.ON - # if not (p := node_cfg.get("operating_state")) - # else NodeOperatingState[p.upper()], - # ) - # elif n_type == "router": - # new_node = Router.from_config(node_cfg) - # elif n_type == "firewall": - # new_node = Firewall.from_config(node_cfg) - # elif n_type == "wireless_router": - # new_node = WirelessRouter.from_config(node_cfg, airspace=net.airspace) - # elif n_type == "printer": - # new_node = Printer( - # hostname=node_cfg["hostname"], - # ip_address=node_cfg["ip_address"], - # subnet_mask=node_cfg["subnet_mask"], - # operating_state=NodeOperatingState.ON - # if not (p := node_cfg.get("operating_state")) - # else NodeOperatingState[p.upper()], - # ) - # # Handle extended nodes - # elif n_type.lower() in Node._registry: - # new_node = HostNode._registry[n_type]( - # hostname=node_cfg["hostname"], - # ip_address=node_cfg.get("ip_address"), - # subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), - # default_gateway=node_cfg.get("default_gateway"), - # dns_server=node_cfg.get("dns_server", None), - # operating_state=NodeOperatingState.ON - # if not (p := node_cfg.get("operating_state")) - # else NodeOperatingState[p.upper()], - # ) - # elif n_type in NetworkNode._registry: - # new_node = NetworkNode._registry[n_type](**node_cfg) else: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index b003009b..822714cb 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import BaseModel, Field, validate_call +from pydantic import BaseModel, ConfigDict, Field, validate_call from primaite import getLogger from primaite.exceptions import NetworkError @@ -1480,8 +1480,6 @@ class Node(SimComponent, ABC): :param operating_state: The node operating state, either ON or OFF. """ - hostname: str - "The node hostname on the network." default_gateway: Optional[IPV4Address] = None "The default gateway IP address for forwarding network traffic to other networks." operating_state: NodeOperatingState = NodeOperatingState.OFF @@ -1519,13 +1517,18 @@ class Node(SimComponent, ABC): config: Node.ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) - class ConfigSchema: + class ConfigSchema(BaseModel, ABC): """Configuration Schema for Node based classes.""" + model_config = ConfigDict(arbitrary_types_allowed=True) + """Configure pydantic to allow arbitrary types and to let the instance have attributes not present in the model.""" + hostname: str + "The node hostname on the network." + revealed_to_red: bool = False "Informs whether the node has been revealed to a red agent." - start_up_duration: int = 3 + start_up_duration: int = 0 "Time steps needed for the node to start up." start_up_countdown: int = 0 @@ -1549,8 +1552,9 @@ class Node(SimComponent, ABC): red_scan_countdown: int = 0 "Time steps until reveal to red scan is complete." - def from_config(cls, config: Dict) -> Node: - """Create Node object from a given configuration.""" + @classmethod + def from_config(cls, config: Dict) -> "Node": + """Create Node object from a given configuration dictionary.""" if config["type"] not in cls._registry: msg = f"Configuration contains an invalid Node type: {config['type']}" return ValueError(msg) diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 1fb63b2e..85857a44 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -42,6 +42,6 @@ class Computer(HostNode, identifier="computer"): class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Computer class.""" - pass + hostname: str = "Computer" pass diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index fa73bf10..00f21342 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -332,7 +332,7 @@ class HostNode(Node, identifier="HostNode"): class ConfigSchema(Node.ConfigSchema): """Configuration Schema for HostNode class.""" - pass + hostname: str = "HostNode" def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 0a397c49..c7e22d49 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -104,13 +104,14 @@ class Firewall(Router, identifier="firewall"): class ConfigSchema(Router.ConfigSChema): """Configuration Schema for Firewall 'Nodes' within PrimAITE.""" - pass + hostname: str = "Firewall" + num_ports: int = 0 - def __init__(self, hostname: str, **kwargs): + def __init__(self, **kwargs): if not kwargs.get("sys_log"): - kwargs["sys_log"] = SysLog(hostname) + kwargs["sys_log"] = SysLog(self.config.hostname) - super().__init__(hostname=hostname, num_ports=0, **kwargs) + super().__init__(hostname=self.config.hostname, num_ports=self.config.num_ports, **kwargs) self.connect_nic( RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", port_name="external") diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 132b1462..83fa066d 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1211,27 +1211,22 @@ class Router(NetworkNode, identifier="router"): "The Router Interfaces on the node." network_interface: Dict[int, RouterInterface] = {} "The Router Interfaces on the node by port id." - acl: AccessControlList - route_table: RouteTable - config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSChema()) + config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSchema()) - class ConfigSChema(NetworkNode.ConfigSchema): + class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Router Objects.""" - num_ports: int = 10 + num_ports: int = 5 hostname: str = "Router" ports: list = [] + sys_log: SysLog = SysLog(hostname) + acl: AccessControlList = AccessControlList(sys_log=sys_log, implicit_action=ACLAction.DENY, name=hostname) + route_table: RouteTable = RouteTable(sys_log=sys_log) - def __init__(self, hostname: str, num_ports: int = 5, **kwargs): - if not kwargs.get("sys_log"): - kwargs["sys_log"] = SysLog(hostname) - if not kwargs.get("acl"): - kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=hostname) - if not kwargs.get("route_table"): - kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"]) - super().__init__(hostname=hostname, num_ports=num_ports, **kwargs) - self.session_manager = RouterSessionManager(sys_log=self.sys_log) + def __init__(self, **kwargs): + super().__init__(hostname=self.config.hostname, num_ports=self.config.num_ports, **kwargs) + self.session_manager = RouterSessionManager(sys_log=self.config.sys_log) self.session_manager.node = self self.software_manager.session_manager = self.session_manager self.session_manager.software_manager = self.software_manager @@ -1265,10 +1260,10 @@ class Router(NetworkNode, identifier="router"): Initializes the router's ACL (Access Control List) with default rules, permitting essential protocols like ARP and ICMP, which are necessary for basic network operations and diagnostics. """ - self.acl.add_rule( + self.config.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) - self.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + self.config.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) def setup_for_episode(self, episode: int): """ @@ -1292,7 +1287,7 @@ class Router(NetworkNode, identifier="router"): More information in user guide and docstring for SimComponent._init_request_manager. """ rm = super()._init_request_manager() - rm.add_request("acl", RequestType(func=self.acl._request_manager)) + rm.add_request("acl", RequestType(func=self.config.acl._request_manager)) return rm def ip_is_router_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool: @@ -1346,7 +1341,7 @@ class Router(NetworkNode, identifier="router"): """ state = super().describe_state() state["num_ports"] = self.config.num_ports - state["acl"] = self.acl.describe_state() + state["acl"] = self.config.acl.describe_state() return state def check_send_frame_to_session_manager(self, frame: Frame) -> bool: @@ -1393,7 +1388,7 @@ class Router(NetworkNode, identifier="router"): return # Check if it's permitted - permitted, rule = self.acl.is_permitted(frame) + permitted, rule = self.config.acl.is_permitted(frame) if not permitted: at_port = self._get_port_of_nic(from_network_interface) @@ -1566,83 +1561,18 @@ class Router(NetworkNode, identifier="router"): ) print(table) - # TODO: Remove - Cover normal config items with ConfigSchema. Move additional setup components to __init__ ? - - @classmethod - def from_config(cls, cfg: dict, **kwargs) -> "Router": - """Create a router based on a config dict. - - Schema: - - hostname (str): unique name for this router. - - num_ports (int, optional): Number of network ports on the router. 8 by default - - ports (dict): Dict with integers from 1 - num_ports as keys. The values should be another dict specifying - ip_address and subnet_mask assigned to that ports (as strings) - - acl (dict): Dict with integers from 1 - max_acl_rules as keys. The key defines the position within the ACL - where the rule will be added (lower number is resolved first). The values should describe valid ACL - Rules as: - - action (str): either PERMIT or DENY - - src_port (str, optional): the named port such as HTTP, HTTPS, or POSTGRES_SERVER - - dst_port (str, optional): the named port such as HTTP, HTTPS, or POSTGRES_SERVER - - protocol (str, optional): the named IP protocol such as ICMP, TCP, or UDP - - src_ip_address (str, optional): IP address octet written in base 10 - - dst_ip_address (str, optional): IP address octet written in base 10 - - routes (list[dict]): List of route dicts with values: - - address (str): The destination address of the route. - - subnet_mask (str): The subnet mask of the route. - - next_hop_ip_address (str): The next hop IP for the route. - - metric (int): The metric of the route. Optional. - - default_route: - - next_hop_ip_address (str): The next hop IP for the route. - - Example config: - ``` - { - 'hostname': 'router_1', - 'num_ports': 5, - 'ports': { - 1: { - 'ip_address' : '192.168.1.1', - 'subnet_mask' : '255.255.255.0', - }, - 2: { - 'ip_address' : '192.168.0.1', - 'subnet_mask' : '255.255.255.252', - } - }, - 'acl' : { - 21: {'action': 'PERMIT', 'src_port': 'HTTP', dst_port: 'HTTP'}, - 22: {'action': 'PERMIT', 'src_port': 'ARP', 'dst_port': 'ARP'}, - 23: {'action': 'PERMIT', 'protocol': 'ICMP'}, - }, - 'routes' : [ - {'address': '192.168.0.0', 'subnet_mask': '255.255.255.0', 'next_hop_ip_address': '192.168.1.2'} - ], - 'default_route': {'next_hop_ip_address': '192.168.0.2'} - } - ``` - - :param cfg: Router config adhering to schema described in main docstring body - :type cfg: dict - :return: Configured router. - :rtype: Router - """ - router = Router( - hostname=cfg["hostname"], - num_ports=int(cfg.get("num_ports", "5")), - operating_state=NodeOperatingState.ON - if not (p := cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) + def setup_router(self, cfg: dict) -> Router: + """ TODO: This is the extra bit of Router's from_config metho. Needs sorting.""" if "ports" in cfg: for port_num, port_cfg in cfg["ports"].items(): - router.configure_port( + self.configure_port( port=port_num, ip_address=port_cfg["ip_address"], subnet_mask=IPv4Address(port_cfg.get("subnet_mask", "255.255.255.0")), ) if "acl" in cfg: for r_num, r_cfg in cfg["acl"].items(): - router.acl.add_rule( + self.config.acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], @@ -1655,7 +1585,7 @@ class Router(NetworkNode, identifier="router"): ) if "routes" in cfg: for route in cfg.get("routes"): - router.route_table.add_route( + self.config.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), @@ -1664,5 +1594,5 @@ class Router(NetworkNode, identifier="router"): if "default_route" in cfg: next_hop_ip_address = cfg["default_route"].get("next_hop_ip_address", None) if next_hop_ip_address: - router.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) - return router + self.config.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) + return self diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index b73af3cb..a2d0050b 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -107,8 +107,9 @@ class Switch(NetworkNode, identifier="switch"): class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Switch nodes within PrimAITE.""" + hostname: str = "Switch" num_ports: int = 24 - "The number of ports on the switch." + "The number of ports on the switch. Default is 24." def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 0a527ab8..2c4b5976 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -124,12 +124,12 @@ class WirelessRouter(Router, identifier="wireless_router"): network_interface: Dict[int, Union[RouterInterface, WirelessAccessPoint]] = {} airspace: AirSpace - config: "WirelessRouter.ConfigSchema" = Field(default_factory=lambda: WirelessRouter.Configschema()) + config: "WirelessRouter.ConfigSchema" = Field(default_factory=lambda: WirelessRouter.ConfigSchema()) class ConfigSchema(Router.ConfigSChema): """Configuration Schema for WirelessRouter nodes within PrimAITE.""" - pass + hostname: str = "WirelessRouter" def __init__(self, hostname: str, airspace: AirSpace, **kwargs): super().__init__(hostname=hostname, num_ports=0, airspace=airspace, **kwargs) diff --git a/tests/conftest.py b/tests/conftest.py index 0d2cc363..6cbcfa84 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -195,12 +195,14 @@ def example_network() -> Network: network = Network() # Router 1 + # router_1 = Router(hostname="router_1", start_up_duration=0) router_1 = Router(hostname="router_1", start_up_duration=0) router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") # Switch 1 + # switch_1_config = Switch.ConfigSchema() switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0) switch_1.power_on() @@ -208,6 +210,7 @@ def example_network() -> Network: router_1.enable_port(1) # Switch 2 + # switch_2_config = Switch.ConfigSchema() switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) switch_2.power_on() network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8]) From f8fb052dadfa3f6a4d43376c281e91f587b8c30c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 15 Jan 2025 16:44:17 +0000 Subject: [PATCH 133/224] #2869 - Make agent schema children work properly --- src/primaite/game/agent/actions/manager.py | 20 +++-- src/primaite/game/agent/interface.py | 35 +++------ .../agent/observations/observation_manager.py | 2 +- src/primaite/game/agent/rewards.py | 75 +++++++++++++++++-- .../_primaite/_game/_agent/test_agent.py | 50 +++++++++++++ 5 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 tests/unit_tests/_primaite/_game/_agent/test_agent.py diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 0f7db2f3..a6e235c5 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -39,6 +39,13 @@ class DoNothingAction(AbstractAction, identifier="do_nothing"): return ["do_nothing"] +class _ActionMapItem(BaseModel): + model_config = ConfigDict(extra="forbid") + + action: str + options: Dict + + class ActionManager(BaseModel): """Class which manages the action space for an agent.""" @@ -46,20 +53,23 @@ class ActionManager(BaseModel): """Config Schema for ActionManager.""" model_config = ConfigDict(extra="forbid") - action_map: Dict[int, Tuple[str, Dict]] = {} + action_map: Dict[int, _ActionMapItem] = {} """Mapping between integer action choices and CAOS actions.""" @field_validator("action_map", mode="after") def consecutive_action_nums(cls, v: Dict) -> Dict: """Make sure all numbers between 0 and N are represented as dict keys in action map.""" assert all([i in v.keys() for i in range(len(v))]) + return v config: ActionManager.ConfigSchema = Field(default_factory=lambda: ActionManager.ConfigSchema()) - @property - def action_map(self) -> Dict[int, Tuple[str, Dict]]: - """Convenience method for accessing the action map.""" - return self.config.action_map + action_map: Dict[int, Tuple[str, Dict]] = {} + """Init as empty, populate after model validation.""" + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.action_map = {n: (v.action, v.options) for n, v in self.config.action_map.items()} def get_action(self, action: int) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 0b55c1db..3311de66 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -52,7 +52,7 @@ class AbstractAgent(BaseModel, ABC): """Configuration Schema for AbstractAgents.""" model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) - type: str = "AbstractAgent" + type: str action_space: ActionManager.ConfigSchema = Field(default_factory=lambda: ActionManager.ConfigSchema()) observation_space: ObservationManager.ConfigSchema = Field( default_factory=lambda: ObservationManager.ConfigSchema() @@ -63,9 +63,10 @@ class AbstractAgent(BaseModel, ABC): logger: AgentLog = AgentLog(agent_name="Abstract_Agent") history: List[AgentHistoryItem] = [] - action_manager: "ActionManager" - observation_manager: "ObservationManager" - reward_function: "RewardFunction" + + action_manager: ActionManager = Field(default_factory=lambda: ActionManager()) + observation_manager: ObservationManager = Field(default_factory=lambda: ObservationManager()) + reward_function: RewardFunction = Field(default_factory=lambda: RewardFunction()) _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} @@ -77,32 +78,18 @@ class AbstractAgent(BaseModel, ABC): raise ValueError(f"Cannot create a new agent under reserved name {identifier}") cls._registry[identifier] = cls - def __init__(self, config: ConfigSchema, **kwargs): - kwargs["action_manager"] = kwargs.get("action_manager") or ActionManager.from_config(config.action_space) - kwargs["observation_manager"] = kwargs.get("observation_manager") or ObservationManager( - config.observation_space - ) - kwargs["reward_function"] = kwargs.get("reward_function") or RewardFunction.from_config(config.reward_function) - super().__init__(config=config, **kwargs) + def model_post_init(self, __context: Any) -> None: + """Overwrite the default empty action, observation, and rewards with ones defined through the config.""" + self.action_manager = ActionManager(config=self.config.action_space) + self.observation_manager = ObservationManager(config=self.config.observation_space) + self.reward_function = RewardFunction(config=self.config.reward_function) + return super().model_post_init(__context) @property def flatten_obs(self) -> bool: """Return agent flatten_obs param.""" return self.config.flatten_obs - @classmethod - def from_config(cls, config: Dict) -> "AbstractAgent": - """Creates an agent component from a configuration dictionary.""" - if config["type"] not in cls._registry: - return ValueError(f"Invalid Agent Type: {config['type']}") - obj = cls( - config=cls.ConfigSchema(**config["agent_settings"]), - action_manager=ActionManager.from_config(config["action_space"]), - observation_manager=ObservationManager(config["observation_space"]), - reward_function=RewardFunction.from_config(config["reward_function"]), - ) - return obj - def update_observation(self, state: Dict) -> ObsType: """ Convert a state from the simulator into an observation for the agent using the observation space. diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 6964ce2c..83d4a076 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -194,7 +194,7 @@ class ObservationManager(BaseModel): if "options" not in data: data["options"] = obs_class.ConfigSchema() - # if options passed as a dict, convert to a schema + # if options passed as a dict, validate against schema elif isinstance(data["options"], dict): data["options"] = obs_class.ConfigSchema(**data["options"]) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 50fdaba8..d4c8ef9b 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -30,7 +30,7 @@ the structure: from abc import ABC, abstractmethod from typing import Any, Callable, ClassVar, Dict, Iterable, List, Optional, Tuple, Type, TYPE_CHECKING, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field, model_validator from typing_extensions import Never from primaite import getLogger @@ -410,15 +410,74 @@ class ActionPenalty(AbstractReward, identifier="ACTION_PENALTY"): return self.config.action_penalty -class RewardFunction: +class _SingleComponentConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + type: str + options: AbstractReward.ConfigSchema + weight: float = 1.0 + + @model_validator(mode="before") + @classmethod + def resolve_obs_options_type(cls, data: Any) -> Any: + """ + When constructing the model from a dict, resolve the correct reward class based on `type` field. + + Workaround: The `options` field is statically typed as AbstractReward. Therefore, it falls over when + passing in data that adheres to a subclass schema rather than the plain AbstractReward schema. There is + a way to do this properly using discriminated union, but most advice on the internet assumes that the full + list of types between which to discriminate is known ahead-of-time. That is not the case for us, because of + our plugin architecture. + + We may be able to revisit and implement a better solution when needed using the following resources as + research starting points: + https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions + https://github.com/pydantic/pydantic/issues/7366 + https://github.com/pydantic/pydantic/issues/7462 + https://github.com/pydantic/pydantic/pull/7983 + """ + if not isinstance(data, dict): + return data + + assert "type" in data, ValueError('Reward component definition is missing the "type" key.') + rew_type = data["type"] + rew_class = AbstractReward._registry[rew_type] + + # if no options are passed in, try to create a default schema. Only works if there are no mandatory fields. + if "options" not in data: + data["options"] = rew_class.ConfigSchema() + + # if options are passed as a dict, validate against schema + elif isinstance(data["options"], dict): + data["options"] = rew_class.ConfigSchema(**data["options"]) + + return data + + +class RewardFunction(BaseModel): """Manages the reward function for the agent.""" - def __init__(self): - """Initialise the reward function object.""" - self.reward_components: List[Tuple[AbstractReward, float]] = [] - "attribute reward_components keeps track of reward components and the weights assigned to each." - self.current_reward: float = 0.0 - self.total_reward: float = 0.0 + model_config = ConfigDict(extra="forbid") + + class ConfigSchema(BaseModel): + """Config Schema for RewardFunction.""" + + model_config = ConfigDict(extra="forbid") + + reward_components: Iterable[_SingleComponentConfig] = [] + + config: ConfigSchema = Field(default_factory=lambda: RewardFunction.ConfigSchema()) + + reward_components: List[Tuple[AbstractReward, float]] = [] + + current_reward: float = 0.0 + total_reward: float = 0.0 + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + for rew_config in self.config.reward_components: + rew_class = AbstractReward._registry[rew_config.type] + rew_instance = rew_class(config=rew_config.options) + self.register_component(component=rew_instance, weight=rew_config.weight) def register_component(self, component: AbstractReward, weight: float = 1.0) -> None: """Add a reward component to the reward function. diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_agent.py new file mode 100644 index 00000000..5f3b4fc0 --- /dev/null +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent.py @@ -0,0 +1,50 @@ +from primaite.game.agent.observations.file_system_observations import FileObservation +from primaite.game.agent.observations.observation_manager import NullObservation +from primaite.game.agent.scripted_agents.random_agent import RandomAgent + + +def test_creating_empty_agent(): + agent = RandomAgent() + assert len(agent.action_manager.action_map) == 0 + assert isinstance(agent.observation_manager.obs, NullObservation) + assert len(agent.reward_function.reward_components) == 0 + + +def test_creating_agent_from_dict(): + action_config = { + "action_map": { + 0: {"action": "do_nothing", "options": {}}, + 1: { + "action": "node_application_execute", + "options": {"node_name": "client", "application_name": "database"}, + }, + } + } + observation_config = { + "type": "FILE", + "options": { + "file_name": "dog.pdf", + "include_num_access": False, + "file_system_requires_scan": False, + }, + } + reward_config = { + "reward_components": [ + { + "type": "DATABASE_FILE_INTEGRITY", + "weight": 0.3, + "options": {"node_hostname": "server", "folder_name": "database", "file_name": "database.db"}, + } + ] + } + agent = RandomAgent( + config={ + "action_space": action_config, + "observation_space": observation_config, + "reward_function": reward_config, + } + ) + + assert len(agent.action_manager.action_map) == 2 + assert isinstance(agent.observation_manager.obs, FileObservation) + assert len(agent.reward_function.reward_components) == 1 From 504f4bd134dcb8276e70857a809a1157886f5567 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 16 Jan 2025 15:17:42 +0000 Subject: [PATCH 134/224] #2869 - Refactor agent and action config system --- src/primaite/game/agent/actions/acl.py | 63 +++++------------ src/primaite/game/agent/actions/manager.py | 2 +- src/primaite/game/agent/interface.py | 37 +++++++--- src/primaite/game/agent/rewards.py | 2 +- .../agent/scripted_agents/abstract_tap.py | 29 +++++--- .../scripted_agents/data_manipulation_bot.py | 24 ++++--- .../scripted_agents/probabilistic_agent.py | 16 +++-- .../agent/scripted_agents/random_agent.py | 38 ++++++++--- src/primaite/game/game.py | 56 +++++++++------- src/primaite/session/environment.py | 2 +- src/primaite/session/ray_envs.py | 8 +-- tests/conftest.py | 67 +------------------ 12 files changed, 158 insertions(+), 186 deletions(-) diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 6022f697..ee5ed292 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -2,15 +2,13 @@ from __future__ import annotations from abc import ABC -from typing import List - -from pydantic import field_validator +from typing import List, Literal, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat -from primaite.utils.validation.ip_protocol import protocol_validator -from primaite.utils.validation.ipv4_address import ipv4_validator, IPV4Address -from primaite.utils.validation.port import port_validator +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.ipv4_address import IPV4Address +from primaite.utils.validation.port import Port __all__ = ( "RouterACLAddRuleAction", @@ -29,43 +27,14 @@ class ACLAddRuleAbstractAction(AbstractAction, ABC): """Configuration Schema base for ACL add rule abstract actions.""" src_ip: IPV4Address - protocol_name: str - permission: str + protocol_name: Union[IPProtocol, Literal["ALL"]] + permission: Literal["ALLOW", "DENY"] position: int - dst_ip: IPV4Address - src_port: int - dst_port: int - src_wildcard: int - dst_wildcard: int - - @field_validator( - "src_port", - "dst_port", - mode="before", - ) - @classmethod - def valid_port(cls, v: str) -> int: - """Check that inputs are valid.""" - return port_validator(v) - - @field_validator( - "src_ip", - "dst_ip", - mode="before", - ) - @classmethod - def valid_ip(cls, v: str) -> str: - """Check that a valid IP has been provided for src and dst.""" - return ipv4_validator(v) - - @field_validator( - "protocol_name", - mode="before", - ) - @classmethod - def is_valid_protocol(cls, v: str) -> bool: - """Check that we are using a valid protocol.""" - return protocol_validator(v) + dst_ip: Union[IPV4Address, Literal["ALL"]] + src_port: Union[Port, Literal["ALL"]] + dst_port: Union[Port, Literal["ALL"]] + src_wildcard: Union[IPV4Address, Literal["NONE"]] + dst_wildcard: Union[IPV4Address, Literal["NONE"]] class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_abstract_action"): @@ -100,10 +69,10 @@ class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_ad "add_rule", config.permission, config.protocol_name, - config.src_ip, + str(config.src_ip), config.src_wildcard, config.src_port, - config.dst_ip, + str(config.dst_ip), config.dst_wildcard, config.dst_port, config.position, @@ -139,7 +108,7 @@ class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_ac firewall_port_direction: str @classmethod - def form_request(cls, config: ConfigSchema) -> List[str]: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", @@ -151,10 +120,10 @@ class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_ac "add_rule", config.permission, config.protocol_name, - config.src_ip, + str(config.src_ip), config.src_wildcard, config.src_port, - config.dst_ip, + str(config.dst_ip), config.dst_wildcard, config.dst_port, config.position, diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index a6e235c5..fefa22b8 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -84,7 +84,7 @@ class ActionManager(BaseModel): def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" act_class = AbstractAction._registry[action_identifier] - config = act_class.ConfigSchema(**action_options) + config = act_class.ConfigSchema(type=action_identifier, **action_options) return act_class.form_request(config=config) @property diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 3311de66..f5714644 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TYPE_CHECKING +from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type, TYPE_CHECKING from gymnasium.core import ActType, ObsType from pydantic import BaseModel, ConfigDict, Field @@ -48,11 +48,20 @@ class AbstractAgent(BaseModel, ABC): model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) + class AgentSettingsSchema(BaseModel, ABC): + """Schema for the 'agent_settings' key.""" + + model_config = ConfigDict(extra="forbid") + class ConfigSchema(BaseModel, ABC): """Configuration Schema for AbstractAgents.""" model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) type: str + ref: str + """name of the agent.""" + team: Optional[Literal["BLUE", "GREEN", "RED"]] + agent_settings: AbstractAgent.AgentSettingsSchema = Field(default=lambda: AbstractAgent.AgentSettingsSchema()) action_space: ActionManager.ConfigSchema = Field(default_factory=lambda: ActionManager.ConfigSchema()) observation_space: ObservationManager.ConfigSchema = Field( default_factory=lambda: ObservationManager.ConfigSchema() @@ -85,11 +94,6 @@ class AbstractAgent(BaseModel, ABC): self.reward_function = RewardFunction(config=self.config.reward_function) return super().model_post_init(__context) - @property - def flatten_obs(self) -> bool: - """Return agent flatten_obs param.""" - return self.config.flatten_obs - def update_observation(self, state: Dict) -> ObsType: """ Convert a state from the simulator into an observation for the agent using the observation space. @@ -149,6 +153,13 @@ class AbstractAgent(BaseModel, ABC): """Update the most recent history item with the reward value.""" self.history[-1].reward = self.reward_function.current_reward + @classmethod + def from_config(cls, config: Dict) -> AbstractAgent: + """Grab the relevatn agent class and construct an instance from a config dict.""" + agent_type = config["type"] + agent_class = cls._registry[agent_type] + return agent_class(config=config) + class AbstractScriptedAgent(AbstractAgent, identifier="AbstractScriptedAgent"): """Base class for actors which generate their own behaviour.""" @@ -172,12 +183,17 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): config: "ProxyAgent.ConfigSchema" = Field(default_factory=lambda: ProxyAgent.ConfigSchema()) most_recent_action: ActType = None + class AgentSettingsSchema(AbstractAgent.AgentSettingsSchema): + """Schema for the `agent_settings` part of the agent config.""" + + flatten_obs: bool = False + action_masking: bool = False + class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Proxy Agent.""" type: str = "Proxy_Agent" - flatten_obs: bool = False - action_masking: bool = False + agent_settings: ProxyAgent.AgentSettingsSchema = Field(default_factory=lambda: ProxyAgent.AgentSettingsSchema()) def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ @@ -199,3 +215,8 @@ class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): The environment is responsible for calling this method when it receives an action from the agent policy. """ self.most_recent_action = action + + @property + def flatten_obs(self) -> bool: + """Return agent flatten_obs param.""" + return self.config.agent_settings.flatten_obs diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index d4c8ef9b..2881f967 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -377,7 +377,7 @@ class SharedReward(AbstractReward, identifier="SHARED_REWARD"): class ActionPenalty(AbstractReward, identifier="ACTION_PENALTY"): - """Apply a negative reward when taking any action except DONOTHING.""" + """Apply a negative reward when taking any action except do_nothing.""" config: "ActionPenalty.ConfigSchema" diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index 21323578..e6ddd546 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -3,27 +3,36 @@ from __future__ import annotations import random from abc import abstractmethod -from typing import Dict, Optional, Tuple +from typing import Dict, List, Optional, Tuple from gymnasium.core import ObsType from pydantic import Field -from primaite.game.agent.interface import AbstractScriptedAgent +from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "AbstractTAPAgent" -class AbstractTAPAgent(AbstractScriptedAgent, identifier="AbstractTAP"): +class AbstractTAPAgent(PeriodicAgent, identifier="AbstractTAP"): """Base class for TAP agents to inherit from.""" config: "AbstractTAPAgent.ConfigSchema" = Field(default_factory=lambda: AbstractTAPAgent.ConfigSchema()) next_execution_timestep: int = 0 - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + class AgentSettingsSchema(PeriodicAgent.AgentSettingsSchema): + """Schema for the `agent_settings` part of the agent config.""" + + possible_starting_nodes: List[str] = Field(default_factory=list) + + class ConfigSchema(PeriodicAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" type: str = "AbstractTAP" - starting_node_name: Optional[str] = None + agent_settings: AbstractTAPAgent.AgentSettingsSchema = Field( + default_factory=lambda: AbstractTAPAgent.AgentSettingsSchema() + ) + + starting_node: Optional[str] = None @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -40,13 +49,13 @@ class AbstractTAPAgent(AbstractScriptedAgent, identifier="AbstractTAP"): :param timestep: The timestep to add variance to. """ - random_timestep_increment = random.randint(-self.config.variance, self.config.variance) + random_timestep_increment = random.randint( + -self.config.agent_settings.variance, self.config.agent_settings.variance + ) self.next_execution_timestep = timestep + random_timestep_increment def _select_start_node(self) -> None: """Set the starting starting node of the agent to be a random node from this agent's action manager.""" # we are assuming that every node in the node manager has a data manipulation application at idx 0 - num_nodes = len(self.action_manager.node_names) - starting_node_idx = random.randint(0, num_nodes - 1) - self.config.starting_node_name = self.action_manager.node_names[starting_node_idx] - self.logger.debug(f"Selected starting node: {self.config.starting_node_name}") + self.starting_node = random.choice(self.config.agent_settings.possible_starting_nodes) + self.logger.debug(f"Selected starting node: {self.starting_node}") diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index 8fe0690b..a7558d42 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -1,6 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -import random -from typing import Dict, List, Tuple +from typing import Dict, Tuple from gymnasium.core import ObsType from pydantic import Field @@ -13,21 +12,24 @@ __all__ = "DataManipulationAgent" class DataManipulationAgent(PeriodicAgent, identifier="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" + class AgentSettingsSchema(PeriodicAgent.AgentSettingsSchema): + """Schema for the `agent_settings` part of the agent config.""" + + target_application: str = "DataManipulationBot" + class ConfigSchema(PeriodicAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" type: str = "RedDatabaseCorruptingAgent" - starting_application_name: str = "DataManipulationBot" - possible_start_nodes: List[str] + agent_settings: "DataManipulationAgent.AgentSettingsSchema" = Field( + default_factory=lambda: DataManipulationAgent.AgentSettingsSchema() + ) config: "DataManipulationAgent.ConfigSchema" = Field(default_factory=lambda: DataManipulationAgent.ConfigSchema()) - start_node: str - def __init__(self, **kwargs): - kwargs["start_node"] = random.choice(kwargs["config"].possible_start_nodes) super().__init__(**kwargs) - self._set_next_execution_timestep(timestep=self.config.start_step, variance=0) + self._set_next_execution_timestep(timestep=self.config.agent_settings.start_step, variance=0) def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Waits until a specific timestep, then attempts to execute its data manipulation application. @@ -43,9 +45,11 @@ class DataManipulationAgent(PeriodicAgent, identifier="RedDatabaseCorruptingAgen self.logger.debug(msg="Performing do nothing action") return "do_nothing", {} - self._set_next_execution_timestep(timestep=timestep + self.config.frequency, variance=self.config.variance) + self._set_next_execution_timestep( + timestep=timestep + self.config.agent_settings.frequency, variance=self.config.agent_settings.variance + ) self.logger.info(msg="Performing a data manipulation attack!") return "node_application_execute", { "node_name": self.start_node, - "application_name": self.config.starting_application_name, + "application_name": self.config.agent_settings.target_application, } diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 8e714f55..20924a95 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -19,10 +19,8 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") config: "ProbabilisticAgent.ConfigSchema" = Field(default_factory=lambda: ProbabilisticAgent.ConfigSchema()) rng: Generator = np.random.default_rng(np.random.randint(0, 65535)) - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): - """Configuration schema for Probabilistic Agent.""" - - type: str = "ProbabilisticAgent" + class AgentSettingsSchema(AbstractScriptedAgent.AgentSettingsSchema): + """Schema for the `agent_settings` part of the agent config.""" action_probabilities: Dict[int, float] = None """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" @@ -46,10 +44,18 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") ) return v + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration schema for Probabilistic Agent.""" + + type: str = "ProbabilisticAgent" + agent_settings: "ProbabilisticAgent.AgentSettingsSchema" = Field( + default_factory=lambda: ProbabilisticAgent.AgentSettingsSchema() + ) + @property def probabilities(self) -> Dict[str, int]: """Convenience method to view the probabilities of the Agent.""" - return np.asarray(list(self.config.action_probabilities.values())) + return np.asarray(list(self.config.agent_settings.action_probabilities.values())) def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 999669d8..721b5293 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -1,9 +1,10 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK import random -from typing import Dict, Tuple +from functools import cached_property +from typing import Dict, List, Tuple from gymnasium.core import ObsType -from pydantic import Field, model_validator +from pydantic import computed_field, Field, model_validator from primaite.game.agent.interface import AbstractScriptedAgent @@ -38,17 +39,17 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): config: "PeriodicAgent.ConfigSchema" = Field(default_factory=lambda: PeriodicAgent.ConfigSchema()) - class ConfigSchema(AbstractScriptedAgent.ConfigSchema): - """Configuration Schema for Periodic Agent.""" + class AgentSettingsSchema(AbstractScriptedAgent.AgentSettingsSchema): + """Schema for the `agent_settings` part of the agent config.""" - type: str = "PeriodicAgent" - """Name of the agent.""" start_step: int = 5 "The timestep at which an agent begins performing it's actions" frequency: int = 5 "The number of timesteps to wait between performing actions" variance: int = 0 "The amount the frequency can randomly change to" + possible_start_nodes: List[str] + target_application: str @model_validator(mode="after") def check_variance_lt_frequency(self) -> "PeriodicAgent.ConfigSchema": @@ -66,6 +67,15 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): ) return self + class ConfigSchema(AbstractScriptedAgent.ConfigSchema): + """Configuration Schema for Periodic Agent.""" + + type: str = "PeriodicAgent" + """Name of the agent.""" + agent_settings: "PeriodicAgent.AgentSettingsSchema" = Field( + default_factory=lambda: PeriodicAgent.AgentSettingsSchema() + ) + max_executions: int = 999999 "Maximum number of times the agent can execute its action." num_executions: int = 0 @@ -73,6 +83,12 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" + @computed_field + @cached_property + def start_node(self) -> str: + """On instantiation, randomly select a start node.""" + return random.choice(self.config.agent_settings.possible_start_nodes) + def _set_next_execution_timestep(self, timestep: int, variance: int) -> None: """Set the next execution timestep with a configured random variance. @@ -88,8 +104,12 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): """Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" if timestep == self.next_execution_timestep and self.num_executions < self.max_executions: self.num_executions += 1 - self._set_next_execution_timestep(timestep + self.config.frequency, self.config.variance) - self.target_node = self.action_manager.node_names[0] - return "node_application_execute", {"node_name": self.target_node, "application_name": 0} + self._set_next_execution_timestep( + timestep + self.config.agent_settings.frequency, self.config.agent_settings.variance + ) + return "node_application_execute", { + "node_name": self.start_node, + "application_name": self.config.agent_settings.target_application, + } return "do_nothing", {} diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5220e874..650d6a10 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -525,33 +525,37 @@ class PrimaiteGame: agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: - agent_name = agent_cfg["ref"] # noqa: F841 - agent_type = agent_cfg["type"] - action_space_cfg = agent_cfg["action_space"] - observation_space_cfg = agent_cfg["observation_space"] - reward_function_cfg = agent_cfg["reward_function"] - agent_settings = agent_cfg["agent_settings"] - - agent_config = { - "type": agent_type, - "action_space": action_space_cfg, - "observation_space": observation_space_cfg, - "reward_function": reward_function_cfg, - "agent_settings": agent_settings, - "game": game, - } - - # CREATE AGENT - if agent_type in AbstractAgent._registry: - new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) - # If blue agent is created, add to game.rl_agents - if agent_type == "ProxyAgent": - game.rl_agents[agent_cfg["ref"]] = new_agent - else: - msg = f"Configuration error: {agent_type} is not a valid agent type." - _LOGGER.error(msg) - raise ValueError(msg) + new_agent = AbstractAgent.from_config(agent_cfg) game.agents[agent_cfg["ref"]] = new_agent + if isinstance(new_agent, ProxyAgent): + game.rl_agents[agent_cfg["ref"]] = new_agent + + # agent_name = agent_cfg["ref"] # noqa: F841 + # agent_type = agent_cfg["type"] + # action_space_cfg = agent_cfg["action_space"] + # observation_space_cfg = agent_cfg["observation_space"] + # reward_function_cfg = agent_cfg["reward_function"] + # agent_settings = agent_cfg["agent_settings"] + + # agent_config = { + # "type": agent_type, + # "action_space": action_space_cfg, + # "observation_space": observation_space_cfg, + # "reward_function": reward_function_cfg, + # "agent_settings": agent_settings, + # "game": game, + # } + + # # CREATE AGENT + # if agent_type in AbstractAgent._registry: + # new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) + # # If blue agent is created, add to game.rl_agents + # if agent_type == "ProxyAgent": + # game.rl_agents[agent_cfg["ref"]] = new_agent + # else: + # msg = f"Configuration error: {agent_type} is not a valid agent type." + # _LOGGER.error(msg) + # raise ValueError(msg) # Validate that if any agents are sharing rewards, they aren't forming an infinite loop. game.setup_reward_sharing() diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 29f7c33d..b7a9a042 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -89,7 +89,7 @@ class PrimaiteGymEnv(gymnasium.Env): :return: Action mask :rtype: List[bool] """ - if not self.agent.config.action_masking: + if not self.agent.config.agent_settings.action_masking: return np.asarray([True] * len(self.agent.action_manager.action_map)) else: return self.game.action_mask(self._agent_name) diff --git a/src/primaite/session/ray_envs.py b/src/primaite/session/ray_envs.py index 0c96714e..16c85cb3 100644 --- a/src/primaite/session/ray_envs.py +++ b/src/primaite/session/ray_envs.py @@ -44,7 +44,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): ) for agent_name in self._agent_ids: agent = self.game.rl_agents[agent_name] - if agent.config.action_masking: + if agent.config.agent_settings.action_masking: self.observation_space[agent_name] = spaces.Dict( { "action_mask": spaces.MultiBinary(agent.action_manager.space.n), @@ -143,7 +143,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): unflat_space = agent.observation_manager.space unflat_obs = agent.observation_manager.current_observation obs = gymnasium.spaces.flatten(unflat_space, unflat_obs) - if agent.config.action_masking: + if agent.config.agent_settings.action_masking: all_obs[agent_name] = {"action_mask": self.game.action_mask(agent_name), "observations": obs} else: all_obs[agent_name] = obs @@ -178,7 +178,7 @@ class PrimaiteRayEnv(gymnasium.Env): def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" super().reset() # Ensure PRNG seed is set everywhere - if self.env.agent.config.action_masking: + if self.env.agent.config.agent_settings.action_masking: obs, *_ = self.env.reset(seed=seed) new_obs = {"action_mask": self.env.action_masks(), "observations": obs} return new_obs, *_ @@ -187,7 +187,7 @@ class PrimaiteRayEnv(gymnasium.Env): def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict]: """Perform a step in the environment.""" # if action masking is enabled, intercept the step method and add action mask to observation - if self.env.agent.config.action_masking: + if self.env.agent.config.agent_settings.action_masking: obs, *_ = self.env.step(action) new_obs = {"action_mask": self.game.action_mask(self.env._agent_name), "observations": obs} return new_obs, *_ diff --git a/tests/conftest.py b/tests/conftest.py index c8c5e694..aa4a0ef0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -414,74 +414,13 @@ def game_and_agent(): sim = game.simulation install_stuff_to_sim(sim) - actions = [ - {"type": "do_nothing"}, - {"type": "node_service_scan"}, - {"type": "node_service_stop"}, - {"type": "node_service_start"}, - {"type": "node_service_pause"}, - {"type": "node_service_resume"}, - {"type": "node_service_restart"}, - {"type": "node_service_disable"}, - {"type": "node_service_enable"}, - {"type": "node_service_fix"}, - {"type": "node_application_execute"}, - {"type": "node_application_scan"}, - {"type": "node_application_close"}, - {"type": "node_application_fix"}, - {"type": "node_application_install"}, - {"type": "node_application_remove"}, - {"type": "node_file_create"}, - {"type": "node_file_scan"}, - {"type": "node_file_checkhash"}, - {"type": "node_file_delete"}, - {"type": "node_file_repair"}, - {"type": "node_file_restore"}, - {"type": "node_file_corrupt"}, - {"type": "node_file_access"}, - {"type": "node_folder_create"}, - {"type": "node_folder_scan"}, - {"type": "node_folder_checkhash"}, - {"type": "node_folder_repair"}, - {"type": "node_folder_restore"}, - {"type": "node_os_scan"}, - {"type": "node_shutdown"}, - {"type": "node_startup"}, - {"type": "node_reset"}, - {"type": "router_acl_add_rule"}, - {"type": "router_acl_remove_rule"}, - {"type": "host_nic_enable"}, - {"type": "host_nic_disable"}, - {"type": "network_port_enable"}, - {"type": "network_port_disable"}, - {"type": "configure_c2_beacon"}, - {"type": "c2_server_ransomware_launch"}, - {"type": "c2_server_ransomware_configure"}, - {"type": "c2_server_terminal_command"}, - {"type": "c2_server_data_exfiltrate"}, - {"type": "node_account_change_password"}, - {"type": "node_session_remote_login"}, - {"type": "node_session_remote_logoff"}, - {"type": "node_send_remote_command"}, - ] - - action_space = ActionManager( - actions=actions, # ALL POSSIBLE ACTIONS - act_map={}, - ) - observation_space = ObservationManager(NestedObservation(components={})) - reward_function = RewardFunction() - config = { "type": "ControlledAgent", - "agent_name": "test_agent", - "action_manager": action_space, - "observation_manager": observation_space, - "reward_function": reward_function, - "agent_settings": {}, + "ref": "test_agent", + "team": "BLUE", } - test_agent = ControlledAgent.from_config(config=config) + test_agent = ControlledAgent(config=config) game.agents["test_agent"] = test_agent From 03cab0fcec6ad6d4618011d0f267524e9fd9249e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 16 Jan 2025 15:18:13 +0000 Subject: [PATCH 135/224] Update configs to new action naming schema and remove redundant agent config --- docs/source/action_masking.rst | 9 +- .../how_to_guides/extensible_agents.rst | 2 +- .../_package_data/data_manipulation.yaml | 510 +++++---- .../_package_data/data_manipulation_marl.yaml | 977 +++++++++--------- .../base_scenario.yaml | 46 +- .../scenario_with_placeholders/greens_1.yaml | 10 +- .../scenario_with_placeholders/greens_2.yaml | 10 +- .../scenario_with_placeholders/reds_1.yaml | 10 +- .../scenario_with_placeholders/reds_2.yaml | 10 +- ...ommand-and-Control-E2E-Demonstration.ipynb | 74 +- ...a-Manipulation-Customising-Red-Agent.ipynb | 4 +- .../Data-Manipulation-E2E-Demonstration.ipynb | 6 +- .../notebooks/Using-Episode-Schedules.ipynb | 4 +- src/primaite/simulator/file_system/file.py | 4 +- src/primaite/simulator/file_system/folder.py | 4 +- tests/assets/configs/action_penalty.yaml | 480 ++++----- .../assets/configs/bad_primaite_session.yaml | 375 ++++--- tests/assets/configs/basic_firewall.yaml | 13 +- .../configs/basic_switched_network.yaml | 15 +- tests/assets/configs/data_manipulation.yaml | 505 +++++---- tests/assets/configs/dmz_network.yaml | 17 +- .../configs/eval_only_primaite_session.yaml | 373 +++---- tests/assets/configs/extended_config.yaml | 458 ++++---- .../configs/firewall_actions_network.yaml | 4 - .../assets/configs/fix_duration_one_item.yaml | 20 +- .../configs/install_and_configure_apps.yaml | 39 +- tests/assets/configs/multi_agent_session.yaml | 977 +++++++++--------- ...etwork_service_recon_red_agent_config.yaml | 7 +- .../nmap_ping_scan_red_agent_config.yaml | 6 +- .../nmap_port_scan_red_agent_config.yaml | 6 +- .../scenario_with_placeholders/greens_1.yaml | 10 +- .../scenario_with_placeholders/greens_2.yaml | 10 +- .../scenario_with_placeholders/reds_1.yaml | 10 +- .../scenario_with_placeholders/reds_2.yaml | 11 +- .../scenario_with_placeholders/scenario.yaml | 42 +- tests/assets/configs/shared_rewards.yaml | 458 ++++---- .../assets/configs/software_fix_duration.yaml | 20 +- .../configs/test_application_install.yaml | 543 +++++----- .../assets/configs/test_primaite_session.yaml | 378 ++++--- .../test_uc2_data_manipulation_scenario.py | 2 +- .../test_application_request_permission.py | 8 +- .../actions/test_c2_suite_actions.py | 12 +- .../actions/test_configure_actions.py | 24 +- .../actions/test_file_request_permission.py | 14 +- .../actions/test_folder_request_permission.py | 8 +- .../actions/test_nic_request_permission.py | 8 +- .../actions/test_node_request_permission.py | 8 +- .../test_service_request_permission.py | 18 +- .../actions/test_terminal_actions.py | 6 +- .../game_layer/test_action_mask.py | 65 +- .../game_layer/test_rewards.py | 12 +- .../_primaite/_game/_agent/test_actions.py | 27 +- .../_game/_agent/test_observations.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 40 +- .../_game/_agent/test_sticky_rewards.py | 38 +- .../_simulator/_file_system/test_file.py | 2 +- .../_file_system/test_file_actions.py | 2 +- .../_simulator/_file_system/test_folder.py | 2 +- .../_file_system/test_folder_actions.py | 2 +- 59 files changed, 3291 insertions(+), 3466 deletions(-) diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index dad6a484..c6e4ca59 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -20,6 +20,11 @@ Masking Logic ============= The following logic is applied: + +..only:: comment + + TODO: update table + +------------------------------------------+---------------------------------------------------------------------+ | Action | Action Mask Logic | +==========================================+=====================================================================+ @@ -119,9 +124,9 @@ The following logic is applied: +------------------------------------------+---------------------------------------------------------------------+ | **CONFIGURE_DATABASE_CLIENT** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **CONFIGURE_RANSOMWARE_SCRIPT** | Node is on. | +| **c2_server_ransomware_configure** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **CONFIGURE_DOSBOT** | Node is on. | +| **configure_dos_bot** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ | **CONFIGURE_C2_BEACON** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 169af094..4b6f8598 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -47,7 +47,7 @@ The core features that should be implemented in any new agent are detailed below - ref: example_green_agent team: GREEN type: ExampleAgent - observation_space: null + action_space: action_map: 0: diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 4869d5d1..2f6e24b3 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -30,22 +30,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -66,22 +66,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: WebBrowser reward_function: reward_components: @@ -102,17 +102,9 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: possible_start_nodes: [client_1, client_2] - starting_application_name: DataManipulationBot + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -198,421 +190,421 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index 512afc64..53ff0634 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -26,22 +26,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -62,22 +62,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: WebBrowser reward_function: reward_components: @@ -98,17 +98,10 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY agent_settings: possible_start_nodes: [client_1, client_2] - starting_application_name: DataManipulationBot + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -189,421 +182,421 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: @@ -704,421 +697,421 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml index e461eccc..e18de191 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml @@ -6,50 +6,48 @@ game: agents: - ref: RL_Agent type: ProxyAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_SHUTDOWN + action: node_shutdown options: - node_id: 0 + node_name: client_1 2: - action: NODE_SHUTDOWN + action: node_shutdown options: - node_id: 1 + node_name: server 3: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: client_1 4: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: server 5: - action: HOST_NIC_DISABLE + action: host_nic_disable options: - node_id: 0 - nic_id: 0 + node_name: client_1 + nic_num: 1 6: - action: HOST_NIC_DISABLE + action: host_nic_disable options: - node_id: 1 - nic_id: 0 + node_name: server + nic_num: 1 7: - action: HOST_NIC_ENABLE + action: host_nic_enable options: - node_id: 0 - nic_id: 0 + node_name: client_1 + nic_num: 1 8: - action: HOST_NIC_ENABLE + action: host_nic_enable options: - node_id: 1 - nic_id: 0 - reward_function: - reward_components: [] + node_name: server + nic_num: 1 simulation: network: diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml index ce670f5f..677cd5a5 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml @@ -6,17 +6,17 @@ agents: &greens action_probabilities: 0: 0.2 1: 0.8 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client + application_name: DatabaseClient reward_function: reward_components: diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml index 9ff099dd..eb7823f8 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml @@ -6,17 +6,17 @@ agents: &greens action_probabilities: 0: 0.95 1: 0.05 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client + application_name: DatabaseClient reward_function: reward_components: diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml index b7e7560d..0170143f 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml @@ -3,15 +3,9 @@ reds: &reds team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: + possible_start_nodes: [client,] + target_application: DataManipulationBot start_step: 10 frequency: 10 variance: 0 diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml index 1d9012d7..55bee3fb 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml @@ -3,15 +3,9 @@ reds: &reds team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: + possible_start_nodes: [client_1] + target_application: DataManipulationBot start_step: 3 frequency: 2 variance: 1 diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 1a5c8b87..278fb3dc 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -51,7 +51,7 @@ " - ref: CustomC2Agent\n", " team: RED\n", " type: ProxyAgent\n", - " observation_space: null\n", + "\n", " action_space:\n", " options:\n", " nodes:\n", @@ -73,15 +73,15 @@ " - 0.0.0.1\n", " action_map:\n", " 0:\n", - " action: DONOTHING\n", + " action: do_nothing\n", " options: {}\n", " 1:\n", - " action: NODE_APPLICATION_INSTALL\n", + " action: node_application_install\n", " options:\n", " node_id: 0\n", " application_name: C2Beacon\n", " 2:\n", - " action: CONFIGURE_C2_BEACON\n", + " action: configure_c2_beacon\n", " options:\n", " node_id: 0\n", " config:\n", @@ -90,7 +90,7 @@ " masquerade_protocol:\n", " masquerade_port:\n", " 3:\n", - " action: NODE_APPLICATION_EXECUTE\n", + " action: node_application_execute\n", " options:\n", " node_id: 0\n", " application_id: 0\n", @@ -109,14 +109,14 @@ " - install\n", " - RansomwareScript\n", " 5:\n", - " action: C2_SERVER_RANSOMWARE_CONFIGURE\n", + " action: c2_server_ransomware_configure\n", " options:\n", " node_id: 1\n", " config:\n", " server_ip_address: 192.168.1.14\n", " payload: ENCRYPT\n", " 6:\n", - " action: C2_SERVER_DATA_EXFILTRATE\n", + " action: c2_server_data_exfiltrate\n", " options:\n", " node_id: 1\n", " target_file_name: \"database.db\"\n", @@ -128,11 +128,11 @@ " password: admin\n", "\n", " 7:\n", - " action: C2_SERVER_RANSOMWARE_LAUNCH\n", + " action: c2_server_ransomware_launch\n", " options:\n", " node_id: 1\n", " 8:\n", - " action: CONFIGURE_C2_BEACON\n", + " action: configure_c2_beacon\n", " options:\n", " node_id: 0\n", " config:\n", @@ -141,7 +141,7 @@ " masquerade_protocol: TCP\n", " masquerade_port: DNS\n", " 9:\n", - " action: CONFIGURE_C2_BEACON\n", + " action: configure_c2_beacon\n", " options:\n", " node_id: 0\n", " config:\n", @@ -213,7 +213,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_INSTALL\n", + "### **Command and Control** | C2 Beacon Actions | node_application_install\n", "\n", "The custom proxy red agent defined at the start of this notebook has been configured to install the C2 Beacon as action ``1`` in it's action map. \n", "\n", @@ -230,7 +230,7 @@ " ...\n", " action_map:\n", " 1:\n", - " action: NODE_APPLICATION_INSTALL \n", + " action: node_application_install \n", " options:\n", " node_id: 0 # Index 0 at the node list.\n", " application_name: C2Beacon\n", @@ -252,7 +252,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Beacon Actions | CONFIGURE_C2_BEACON \n", + "### **Command and Control** | C2 Beacon Actions | configure_c2_beacon \n", "\n", "The custom proxy red agent defined at the start of this notebook can configure the C2 Beacon via action ``2`` in it's action map. \n", "\n", @@ -268,7 +268,7 @@ " action_map:\n", " ...\n", " 2:\n", - " action: CONFIGURE_C2_BEACON\n", + " action: configure_c2_beacon\n", " options:\n", " node_id: 0 # Node Index\n", " config: # Further information about these config options can be found at the bottom of this notebook.\n", @@ -295,9 +295,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Beacon Actions | NODE_APPLICATION_EXECUTE\n", + "### **Command and Control** | C2 Beacon Actions | node_application_execute\n", "\n", - "The final action is ``NODE_APPLICATION_EXECUTE`` which is used to establish a connection for the C2 application. This action can be called by the Red Agent via action ``3`` in it's action map. \n", + "The final action is ``node_application_execute`` which is used to establish a connection for the C2 application. This action can be called by the Red Agent via action ``3`` in it's action map. \n", "\n", "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", @@ -313,7 +313,7 @@ " action_map:\n", " ...\n", " 3:\n", - " action: NODE_APPLICATION_EXECUTE\n", + " action: node_application_execute\n", " options:\n", " node_id: 0\n", " application_id: 0\n", @@ -416,7 +416,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_CONFIGURE\n", + "### **Command and Control** | C2 Server Actions | c2_server_ransomware_configure\n", "\n", "Another action the C2 Server grants is the ability for a Red Agent to configure the RansomwareScript via the C2 Server rather than the note directly.\n", "\n", @@ -435,7 +435,7 @@ " ...\n", " action_map:\n", " 5:\n", - " action: C2_SERVER_RANSOMWARE_CONFIG\n", + " action: c2_server_ransomware_configure\n", " options:\n", " node_id: 1\n", " config:\n", @@ -468,9 +468,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | C2_SERVER_DATA_EXFILTRATE\n", + "### **Command and Control** | C2 Server Actions | c2_server_data_exfiltrate\n", "\n", - "The second to last action available is the ``C2_SERVER_DATA_EXFILTRATE`` which is indexed as action ``6`` in the action map.\n", + "The second to last action available is the ``c2_server_data_exfiltrate`` which is indexed as action ``6`` in the action map.\n", "\n", "This action can be used to exfiltrate a target file on a remote node to the C2 Beacon and the C2 Server's host file system via the ``FTP`` services.\n", "\n", @@ -487,7 +487,7 @@ " ...\n", " action_map:\n", " 6:\n", - " action: C2_SERVER_DATA_EXFILTRATE\n", + " action: c2_server_data_exfiltrate\n", " options:\n", " node_id: 1\n", " target_file_name: \"database.db\"\n", @@ -534,9 +534,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | C2_SERVER_RANSOMWARE_LAUNCH\n", + "### **Command and Control** | C2 Server Actions | c2_server_ransomware_launch\n", "\n", - "Finally, the last available action is for the C2_SERVER_RANSOMWARE_LAUNCH to start the ransomware script installed on the same node as the C2 beacon.\n", + "Finally, the last available action is for the c2_server_ransomware_launch to start the ransomware script installed on the same node as the C2 beacon.\n", "\n", "This action is indexed as action ``7``.\n", "\n", @@ -553,7 +553,7 @@ " ...\n", " action_map:\n", " 7:\n", - " action: C2_SERVER_RANSOMWARE_LAUNCH\n", + " action: c2_server_ransomware_launch\n", " options:\n", " node_id: 1\n", "```\n" @@ -682,19 +682,19 @@ " action_space:\n", " action_map:\n", " 0:\n", - " action: DONOTHING\n", + " action: do_nothing\n", " options: {}\n", " 1:\n", - " action: NODE_APPLICATION_REMOVE\n", + " action: node_application_remove\n", " options:\n", " node_id: 0\n", " application_name: C2Beacon\n", " 2:\n", - " action: NODE_SHUTDOWN\n", + " action: node_shutdown\n", " options:\n", " node_id: 0\n", " 3:\n", - " action: ROUTER_ACL_ADDRULE\n", + " action: router_acl_add_rule\n", " options:\n", " target_router: router_1\n", " position: 1\n", @@ -1079,7 +1079,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below uses the custom blue agent defined at the start of this section perform a NODE_APPLICATION_REMOVE on the C2 beacon:" + "The code cell below uses the custom blue agent defined at the start of this section perform a node_application_remove on the C2 beacon:" ] }, { @@ -1088,7 +1088,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Using CAOS ACTION: NODE_APPLICATION_REMOVE & capturing the OBS\n", + "# Using CAOS ACTION: node_application_remove & capturing the OBS\n", "post_blue_action_obs, _, _, _, _ = blue_env.step(1)" ] }, @@ -1174,7 +1174,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below uses the custom blue agent defined at the start of this section to perform a ``NODE_SHUT_DOWN`` action on the web server." + "The code cell below uses the custom blue agent defined at the start of this section to perform a ``node_shut_down`` action on the web server." ] }, { @@ -1183,7 +1183,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Using CAOS ACTION: NODE_SHUT_DOWN & capturing the OBS\n", + "# Using CAOS ACTION: node_shut_down & capturing the OBS\n", "post_blue_action_obs, _, _, _, _ = blue_env.step(2)" ] }, @@ -1264,7 +1264,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code cell below uses the custom blue agent defined at the start of this section to perform a ROUTER_ACL_ADDRULE on router 1." + "The code cell below uses the custom blue agent defined at the start of this section to perform a router_acl_add_rule on router 1." ] }, { @@ -1273,7 +1273,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Using CAOS ACTION: ROUTER_ACL_ADDRULE & capturing the OBS\n", + "# Using CAOS ACTION: router_acl_add_rule & capturing the OBS\n", "post_blue_action_obs, _, _, _, _ = blue_env.step(3)" ] }, @@ -1387,11 +1387,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As demonstrated earlier, red agents can use the ``CONFIGURE_C2_BEACON`` action to configure these settings mid episode through the configuration options:\n", + "As demonstrated earlier, red agents can use the ``configure_c2_beacon`` action to configure these settings mid episode through the configuration options:\n", "\n", "``` YAML\n", "...\n", - " action: CONFIGURE_C2_BEACON\n", + " action: configure_c2_beacon\n", " options:\n", " node_id: 0\n", " config:\n", diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 50bfa59f..d1154b54 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -67,9 +67,9 @@ " # parse the info dict form step output and write out what the red agent is doing\n", " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", - " if red_action == 'DONOTHING':\n", + " if red_action == 'do_nothing':\n", " red_str = 'DO NOTHING'\n", - " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", + " elif red_action == 'node_application_execute':\n", " client = \"client 1\" if red_info.parameters['node_id'] == 0 else \"client 2\"\n", " red_str = f\"ATTACK from {client}\"\n", " return red_str" diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 89620215..143bbe09 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -449,9 +449,9 @@ " # parse the info dict form step output and write out what the red agent is doing\n", " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", - " if red_action == 'DONOTHING':\n", + " if red_action == 'do_nothing':\n", " red_str = 'DO NOTHING'\n", - " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", + " elif red_action == 'node_application_execute':\n", " client = \"client 1\" if red_info.parameters['node_id'] == 0 else \"client 2\"\n", " red_str = f\"ATTACK from {client}\"\n", " return red_str" @@ -547,7 +547,7 @@ "\n", "The reward will increase slightly as soon as the file finishes restoring. Then, the reward will increase to 0.9 when both green agents make successful requests.\n", "\n", - "Run the following cell until the green action is `NODE_APPLICATION_EXECUTE` for application 0, then the reward should increase. If you run it enough times, another red attack will happen and the reward will drop again." + "Run the following cell until the green action is `node_application_execute` for application 0, then the reward should increase. If you run it enough times, another red attack will happen and the reward will drop again." ] }, { diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index cb06e0f9..d08ca67b 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -238,7 +238,7 @@ "### Episode 2\n", "When we reset the environment again, it moves onto episode 2, where it will bring in greens_1 and reds_1 for green and red agent definitions. Let's verify the agent names and that they take actions at the defined frequency.\n", "\n", - "Most green actions will be `NODE_APPLICATION_EXECUTE` while red will `DONOTHING` except at steps 10 and 20." + "Most green actions will be `node_application_execute` while red will `DONOTHING` except at steps 10 and 20." ] }, { @@ -269,7 +269,7 @@ "### Episode 3\n", "When we reset the environment again, it moves onto episode 3, where it will bring in greens_2 and reds_2 for green and red agent definitions. Let's verify the agent names and that they take actions at the defined frequency.\n", "\n", - "Now, green will perform `NODE_APPLICATION_EXECUTE` only 5% of the time, while red will perform `NODE_APPLICATION_EXECUTE` more frequently than before." + "Now, green will perform `node_application_execute` only 5% of the time, while red will perform `node_application_execute` more frequently than before." ] }, { diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index 57d01ec9..bad26a0a 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -130,8 +130,8 @@ class File(FileSystemItemABC): Return False if corruption is detected, otherwise True """ - warnings.warn("NODE_FILE_CHECKHASH is currently not implemented.") - self.sys_log.warning("NODE_FILE_CHECKHASH is currently not implemented.") + warnings.warn("node_file_checkhash is currently not implemented.") + self.sys_log.warning("node_file_checkhash is currently not implemented.") return False if self.deleted: diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index ee0f3d01..78dba4e6 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -387,8 +387,8 @@ class Folder(FileSystemItemABC): Return False if corruption is detected, otherwise True """ - warnings.warn("NODE_FOLDER_CHECKHASH is currently not implemented.") - self.sys_log.error("NODE_FOLDER_CHECKHASH is currently not implemented.") + warnings.warn("node_folder_checkhash is currently not implemented.") + self.sys_log.error("node_folder_checkhash is currently not implemented.") return False if self.deleted: diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 2ebe1963..2828b5aa 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -98,421 +98,421 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_nodename: router_1 + target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_nodename: router_1 + target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_nodename: router_1 + target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_nodename: router_1 + target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_nodename: router_1 + target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_nodename: router_1 + target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router_nodename: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: reward_components: diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 9f3e6da5..1cd0883c 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -13,31 +13,16 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_step: 25 - frequency: 20 - variance: 5 + action_probabilities: + 0: 1.0 - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent - - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1,] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -119,324 +104,324 @@ agents: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" + action: "node_file_checkhash" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" + action: "node_folder_checkhash" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 19: # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 20: - action: "NODE_STARTUP" + action: "node_startup" options: - node_id: 5 + node_name: client_1 21: - action: "NODE_RESET" + action: "node_reset" options: - node_id: 5 + node_name: client_1 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 24: # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 25: # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 26: - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 27: - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 28: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 0 29: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 1 30: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 2 31: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 3 32: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 4 33: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 5 34: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 6 35: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 7 36: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 8 37: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 9 38: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 39: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 40: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 41: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 42: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 43: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 44: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 45: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 46: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 47: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 48: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 49: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 50: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 51: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 52: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 53: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: reward_components: diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 09e070d5..154956d3 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -27,26 +27,23 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser reward_function: reward_components: - type: DUMMY agent_settings: - start_step: 5 - frequency: 4 - variance: 3 action_probabilities: 0: 0.4 1: 0.6 diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 453db4b0..e74a6a4e 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -30,26 +30,23 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser reward_function: reward_components: - type: DUMMY agent_settings: - start_step: 5 - frequency: 4 - variance: 3 action_probabilities: 0: 0.4 1: 0.6 @@ -115,7 +112,7 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} reward_function: diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 90d8f806..5e12f1c6 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -30,22 +30,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -66,22 +66,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: WebBrowser reward_function: reward_components: @@ -101,16 +101,9 @@ agents: - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent - - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1, client_2] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -200,417 +193,417 @@ agents: options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: reward_components: diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index b0876768..6cdae6a5 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -52,26 +52,19 @@ agents: - ref: client_1_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 - - reward_function: - reward_components: - - type: DUMMY + node_name: client_1 + application_id: WebBrowser agent_settings: - start_step: 5 - frequency: 4 - variance: 3 action_probabilities: 0: 0.4 1: 0.6 diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 73930e7f..e277a881 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -13,11 +13,11 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} reward_function: @@ -25,31 +25,32 @@ agents: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_step: 25 - frequency: 20 - variance: 5 + action_probabilities: + 0: 1.0 - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: DataManipulationBot reward_function: reward_components: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1,] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -131,324 +132,324 @@ agents: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" + action: "node_file_checkhash" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 1 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" + action: "node_folder_checkhash" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 1 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 19: # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 20: - action: "NODE_STARTUP" + action: "node_startup" options: - node_id: 5 + node_name: client_1 21: - action: "NODE_RESET" + action: "node_reset" options: - node_id: 5 + node_name: client_1 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 24: # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 25: # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 26: - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 27: - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 28: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 0 29: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 1 30: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 2 31: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 3 32: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 4 33: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 5 34: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 6 35: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 7 36: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 8 37: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 9 38: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 39: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 40: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 41: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 42: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 43: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 44: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 45: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 46: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 47: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 48: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 49: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 50: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 51: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 52: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 53: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: reward_components: diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index f8e86d31..97d9299a 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -30,22 +30,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -66,22 +66,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: DatabaseClient reward_function: reward_components: @@ -102,15 +102,9 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1, client_2] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -196,420 +190,420 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_checkhash" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_checkhash" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 + node_name: domain_controller nic_id: 0 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 + node_name: domain_controller nic_id: 0 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 + node_name: web_server nic_id: 0 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 + node_name: web_server nic_id: 0 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 + node_name: database_server nic_id: 0 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 + node_name: database_server nic_id: 0 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 + node_name: backup_server nic_id: 0 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 + node_name: backup_server nic_id: 0 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 + node_name: security_suite nic_id: 0 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 + node_name: security_suite nic_id: 0 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 + node_name: security_suite nic_id: 1 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 + node_name: security_suite nic_id: 1 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 + node_name: client_1 nic_id: 0 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 + node_name: client_1 nic_id: 0 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 + node_name: client_2 nic_id: 0 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 + node_name: client_2 nic_id: 0 diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index ceb9c924..41b856fc 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -244,10 +244,6 @@ agents: type: network_port_enable target_nodename: firewall port_id: 3 - reward_function: - reward_components: - - type: DUMMY - agent_settings: start_step: 5 frequency: 4 diff --git a/tests/assets/configs/fix_duration_one_item.yaml b/tests/assets/configs/fix_duration_one_item.yaml index 26ee574a..6444e9e8 100644 --- a/tests/assets/configs/fix_duration_one_item.yaml +++ b/tests/assets/configs/fix_duration_one_item.yaml @@ -27,26 +27,18 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 - - reward_function: - reward_components: - - type: DUMMY - + node_name: client_1 + application_name: WebBrowser agent_settings: - start_step: 5 - frequency: 4 - variance: 3 action_probabilities: 0: 0.4 1: 0.6 @@ -110,7 +102,7 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} reward_function: diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index efe4428a..e4b8805e 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -18,51 +18,51 @@ agents: team: BLUE type: ProxyAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_INSTALL + action: node_application_install options: - node_id: 0 + node_name: client_1 application_name: DatabaseClient 2: - action: NODE_APPLICATION_INSTALL + action: node_application_install options: - node_id: 1 + node_name: client_2 application_name: RansomwareScript 3: - action: NODE_APPLICATION_INSTALL + action: node_application_install options: - node_id: 2 + node_name: client_3 application_name: DoSBot 4: - action: CONFIGURE_DATABASE_CLIENT + action: configure_database_client options: - node_id: 0 + node_name: client_1 config: server_ip_address: 10.0.0.5 5: - action: CONFIGURE_DATABASE_CLIENT + action: configure_database_client options: - node_id: 0 + node_name: client_1 config: server_password: correct_password 6: - action: CONFIGURE_RANSOMWARE_SCRIPT + action: c2_server_ransomware_configure options: - node_id: 1 + node_name: client_2 config: server_ip_address: 10.0.0.5 server_password: correct_password payload: ENCRYPT 7: - action: CONFIGURE_DOSBOT + action: configure_dos_bot options: - node_id: 2 + node_name: client_3 config: target_ip_address: 10.0.0.5 target_port: POSTGRES_SERVER @@ -72,13 +72,10 @@ agents: dos_intensity: 1.0 max_sessions: 1000 8: - action: NODE_APPLICATION_INSTALL + action: node_application_install options: - node_id: 1 + node_name: client_2 application_name: DatabaseClient - reward_function: - reward_components: - - type: DUMMY agent_settings: flatten_obs: True action_masking: False diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 9f2cbd84..bc1f1b69 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -26,22 +26,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -62,22 +62,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: WebBrowser reward_function: reward_components: @@ -98,14 +98,9 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1, client_2] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -186,421 +181,421 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: reward_components: @@ -700,421 +695,421 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: - target_router: router_1 + target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: diff --git a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml index a4deff6f..c2f79144 100644 --- a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml +++ b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml @@ -22,7 +22,7 @@ agents: - ref: client_1_red_nmap team: RED type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: @@ -33,11 +33,6 @@ agents: target_port: 80 target_protocol: tcp show: false - - reward_function: - reward_components: - - type: DUMMY - agent_settings: action_probabilities: 0: 1.0 diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index ee6de2c5..cd485ced 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -22,7 +22,7 @@ agents: - ref: client_1_red_nmap team: RED type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: @@ -32,10 +32,6 @@ agents: target_ip_address: 192.168.1.0/24 show: False - reward_function: - reward_components: - - type: DUMMY - agent_settings: action_probabilities: 0: 1.0 diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 47d34e52..09e88a76 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -22,7 +22,7 @@ agents: - ref: client_1_red_nmap team: RED type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: @@ -39,10 +39,6 @@ agents: - 219 show: false - reward_function: - reward_components: - - type: DUMMY - agent_settings: action_probabilities: 0: 1.0 diff --git a/tests/assets/configs/scenario_with_placeholders/greens_1.yaml b/tests/assets/configs/scenario_with_placeholders/greens_1.yaml index ce670f5f..677cd5a5 100644 --- a/tests/assets/configs/scenario_with_placeholders/greens_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/greens_1.yaml @@ -6,17 +6,17 @@ agents: &greens action_probabilities: 0: 0.2 1: 0.8 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client + application_name: DatabaseClient reward_function: reward_components: diff --git a/tests/assets/configs/scenario_with_placeholders/greens_2.yaml b/tests/assets/configs/scenario_with_placeholders/greens_2.yaml index 9ff099dd..eb7823f8 100644 --- a/tests/assets/configs/scenario_with_placeholders/greens_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/greens_2.yaml @@ -6,17 +6,17 @@ agents: &greens action_probabilities: 0: 0.95 1: 0.05 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client + application_name: DatabaseClient reward_function: reward_components: diff --git a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml index b7e7560d..0170143f 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml @@ -3,15 +3,9 @@ reds: &reds team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: + possible_start_nodes: [client,] + target_application: DataManipulationBot start_step: 10 frequency: 10 variance: 0 diff --git a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml index 1d9012d7..e14eaa43 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml @@ -2,16 +2,9 @@ reds: &reds - ref: red_B team: RED type: RedDatabaseCorruptingAgent - - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: + possible_start_nodes: [client_1,] + target_application: DataManipulationBot start_step: 3 frequency: 2 variance: 1 diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index a61af830..7ea0145a 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -56,44 +56,44 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_SHUTDOWN + action: node_shutdown options: - node_id: 0 + node_name: client 2: - action: NODE_SHUTDOWN + action: node_shutdown options: - node_id: 1 + node_name: server 3: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: client 4: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: server 5: - action: HOST_NIC_DISABLE + action: host_nic_disable options: - node_id: 0 - nic_id: 0 + node_name: client + nic_num: 1 6: - action: HOST_NIC_DISABLE + action: host_nic_disable options: - node_id: 1 - nic_id: 0 + node_name: server + nic_num: 1 7: - action: HOST_NIC_ENABLE + action: host_nic_enable options: - node_id: 0 - nic_id: 0 + node_name: client + nic_num: 1 8: - action: HOST_NIC_ENABLE + action: host_nic_enable options: - node_id: 1 - nic_id: 0 + node_name: server + nic_num: 1 reward_function: reward_components: diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 60e22366..96dada07 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -29,22 +29,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -65,22 +65,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: DatabaseClient reward_function: reward_components: @@ -97,15 +97,9 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1, client_2] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -186,420 +180,420 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" + action: "node_file_checkhash" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" + action: "node_folder_checkhash" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 + node_name: domain_controller nic_id: 0 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 + node_name: domain_controller nic_id: 0 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 + node_name: web_server nic_id: 0 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 + node_name: web_server nic_id: 0 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 + node_name: database_server nic_id: 0 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 + node_name: database_server nic_id: 0 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 + node_name: backup_server nic_id: 0 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 + node_name: backup_server nic_id: 0 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 + node_name: security_suite nic_id: 0 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 + node_name: security_suite nic_id: 0 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 + node_name: security_suite nic_id: 1 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 + node_name: security_suite nic_id: 1 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 + node_name: client_1 nic_id: 0 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 + node_name: client_1 nic_id: 0 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 + node_name: client_2 nic_id: 0 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 + node_name: client_2 nic_id: 0 reward_function: diff --git a/tests/assets/configs/software_fix_duration.yaml b/tests/assets/configs/software_fix_duration.yaml index 006328ba..0059d18a 100644 --- a/tests/assets/configs/software_fix_duration.yaml +++ b/tests/assets/configs/software_fix_duration.yaml @@ -27,26 +27,18 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 - - reward_function: - reward_components: - - type: DUMMY - + node_name: client_2 + application_name: WebBrowser agent_settings: - start_step: 5 - frequency: 4 - variance: 3 action_probabilities: 0: 0.4 1: 0.6 @@ -110,7 +102,7 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} reward_function: diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index c085fd63..55c4afd3 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -29,32 +29,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: - options: - nodes: - - node_name: client_2 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_2 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_2 + application_name: DatabaseClient reward_function: reward_components: @@ -75,32 +65,22 @@ agents: 0: 0.3 1: 0.6 2: 0.1 - observation_space: null + action_space: - options: - nodes: - - node_name: client_1 - applications: - - application_name: WebBrowser - - application_name: DatabaseClient - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_applications_per_node: 2 action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 + node_name: client_1 + application_name: WebBrowser 2: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 1 + node_name: client_1 + application_name: WebBrowser reward_function: reward_components: @@ -121,16 +101,9 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null - - action_space: - action_map: - - reward_function: - reward_components: - - type: DUMMY - agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1, client_2] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -211,445 +184,445 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 0 + node_name: domain_controller 19: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 0 + node_name: domain_controller 20: - action: NODE_STARTUP + action: node_startup options: - node_id: 0 + node_name: domain_controller 21: - action: NODE_RESET + action: node_reset options: - node_id: 0 + node_name: domain_controller 22: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 1 + node_name: web_server 23: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 1 + node_name: web_server 24: - action: NODE_STARTUP + action: node_startup options: - node_id: 1 + node_name: web_server 25: - action: NODE_RESET + action: node_reset options: - node_id: 1 + node_name: web_server 26: # old action num: 18 - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 27: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 2 + node_name: database_server 28: - action: NODE_STARTUP + action: node_startup options: - node_id: 2 + node_name: database_server 29: - action: NODE_RESET + action: node_reset options: - node_id: 2 + node_name: database_server 30: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 3 + node_name: backup_server 31: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 3 + node_name: backup_server 32: - action: NODE_STARTUP + action: node_startup options: - node_id: 3 + node_name: backup_server 33: - action: NODE_RESET + action: node_reset options: - node_id: 3 + node_name: backup_server 34: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 4 + node_name: security_suite 35: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 4 + node_name: security_suite 36: - action: NODE_STARTUP + action: node_startup options: - node_id: 4 + node_name: security_suite 37: - action: NODE_RESET + action: node_reset options: - node_id: 4 + node_name: security_suite 38: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 5 + node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 40: # old action num: 20 - action: NODE_STARTUP + action: node_startup options: - node_id: 5 + node_name: client_1 41: # old action num: 21 - action: NODE_RESET + action: node_reset options: - node_id: 5 + node_name: client_1 42: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 6 + node_name: client_2 43: - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 6 + node_name: client_2 44: - action: NODE_STARTUP + action: node_startup options: - node_id: 6 + node_name: client_2 45: - action: NODE_RESET + action: node_reset options: - node_id: 6 + node_name: client_2 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 50: # old action num: 26 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 51: # old action num: 27 - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 52: # old action num: 28 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 0 53: # old action num: 29 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 1 54: # old action num: 30 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 2 55: # old action num: 31 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 3 56: # old action num: 32 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 4 57: # old action num: 33 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 5 58: # old action num: 34 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 6 59: # old action num: 35 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 7 60: # old action num: 36 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 8 61: # old action num: 37 - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router_hostname: router_1 position: 9 62: # old action num: 38 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 63: # old action num: 39 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 64: # old action num: 40 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 65: # old action num: 41 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 66: # old action num: 42 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 67: # old action num: 43 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 68: # old action num: 44 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 69: # old action num: 45 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 70: # old action num: 46 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 71: # old action num: 47 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 72: # old action num: 48 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 73: # old action num: 49 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 74: # old action num: 50 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 75: # old action num: 51 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 76: # old action num: 52 - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 77: # old action num: 53 - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 78: - action: NODE_APPLICATION_INSTALL + action: node_application_install options: - node_id: 0 + node_name: domain_controller application_name: DoSBot 79: - action: NODE_APPLICATION_REMOVE + action: node_application_remove options: - node_id: 0 + node_name: domain_controller application_name: DoSBot 80: - action: NODE_APPLICATION_REMOVE + action: node_application_remove options: - node_id: 0 + node_name: domain_controller application_name: WebBrowser 81: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 + node_name: domain_controller application_id: 0 82: - action: CONFIGURE_DOSBOT + action: configure_dos_bot options: - node_id: 0 + node_name: domain_controller config: target_ip_address: 192.168.1.14 target_port: POSTGRES_SERVER diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 8c22fbed..cd5d08d3 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -21,20 +21,14 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} - reward_function: - reward_components: - - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_step: 25 - frequency: 20 - variance: 5 action_probabilities: 0: 1.0 @@ -42,24 +36,22 @@ agents: team: RED type: RedDatabaseCorruptingAgent - observation_space: null + action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} 1: - action: NODE_APPLICATION_EXECUTE + action: node_application_execute options: - node_id: 0 - application_id: 0 - - reward_function: - reward_components: - - type: DUMMY + node_name: client_1 + application_name: DataManipulationBot agent_settings: # options specific to this particular agent type, basically args of __init__(self) + possible_start_nodes: [client_1,] + target_application: DataManipulationBot start_step: 25 frequency: 20 variance: 5 @@ -140,324 +132,324 @@ agents: action_space: action_map: 0: - action: DONOTHING + action: do_nothing options: {} # scan webapp service 1: - action: NODE_SERVICE_SCAN + action: node_service_scan options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # stop webapp service 2: - action: NODE_SERVICE_STOP + action: node_service_stop options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer # start webapp service 3: - action: "NODE_SERVICE_START" + action: "node_service_start" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 4: - action: "NODE_SERVICE_PAUSE" + action: "node_service_pause" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 5: - action: "NODE_SERVICE_RESUME" + action: "node_service_resume" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 6: - action: "NODE_SERVICE_RESTART" + action: "node_service_restart" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 7: - action: "NODE_SERVICE_DISABLE" + action: "node_service_disable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 8: - action: "NODE_SERVICE_ENABLE" + action: "node_service_enable" options: - node_id: 1 - service_id: 0 + node_name: web_server + service_name: WebServer 9: # check database.db file - action: "NODE_FILE_SCAN" + action: "node_file_scan" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 10: - action: "NODE_FILE_CHECKHASH" + action: "node_file_checkhash" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 11: - action: "NODE_FILE_DELETE" + action: "node_file_delete" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 12: - action: "NODE_FILE_REPAIR" + action: "node_file_repair" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_name: database_server + folder_name: database + file_name: database.db 13: - action: "NODE_SERVICE_FIX" + action: "node_service_fix" options: - node_id: 2 - service_id: 0 + node_name: database_server + service_name: DatabaseService 14: - action: "NODE_FOLDER_SCAN" + action: "node_folder_scan" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 15: - action: "NODE_FOLDER_CHECKHASH" + action: "node_folder_checkhash" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 16: - action: "NODE_FOLDER_REPAIR" + action: "node_folder_repair" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 17: - action: "NODE_FOLDER_RESTORE" + action: "node_folder_restore" options: - node_id: 2 - folder_id: 0 + node_name: database_server + folder_name: database 18: - action: "NODE_OS_SCAN" + action: "node_os_scan" options: - node_id: 2 + node_name: database_server 19: # shutdown client 1 - action: "NODE_SHUTDOWN" + action: "node_shutdown" options: - node_id: 5 + node_name: client_1 20: - action: "NODE_STARTUP" + action: "node_startup" options: - node_id: 5 + node_name: client_1 21: - action: "NODE_RESET" + action: "node_reset" options: - node_id: 5 + node_name: client_1 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 1 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 2 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 1 # ALL - source_port_id: 1 - dest_port_id: 1 - protocol_id: 1 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: ALL # ALL + src_port: ALL + dst_port: ALL + protocol_name: ALL + src_wildcard: NONE + dst_wildcard: NONE 24: # block tcp traffic from client 1 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 3 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 25: # block tcp traffic from client 2 to web app - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 4 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 3 # web server - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.12 # web server + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 26: - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 5 - permission: 2 - source_ip_id: 7 # client 1 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.21 # client 1 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 27: - action: "ROUTER_ACL_ADDRULE" + action: "router_acl_add_rule" options: target_router: router_1 position: 6 - permission: 2 - source_ip_id: 8 # client 2 - dest_ip_id: 4 # database - source_port_id: 1 - dest_port_id: 1 - protocol_id: 3 - source_wildcard_id: 0 - dest_wildcard_id: 0 + permission: DENY + src_ip: 192.168.10.22 # client 2 + dst_ip: 192.168.1.14 # database + src_port: ALL + dst_port: ALL + protocol_name: TCP + src_wildcard: NONE + dst_wildcard: NONE 28: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 0 29: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 1 30: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 2 31: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 3 32: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 4 33: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 5 34: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 6 35: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 7 36: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 8 37: - action: "ROUTER_ACL_REMOVERULE" + action: "router_acl_remove_rule" options: target_router: router_1 position: 9 38: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 39: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 0 - nic_id: 0 + node_name: domain_controller + nic_num: 1 40: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 41: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 1 - nic_id: 0 + node_name: web_server + nic_num: 1 42: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 43: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 2 - nic_id: 0 + node_name: database_server + nic_num: 1 44: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 45: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 3 - nic_id: 0 + node_name: backup_server + nic_num: 1 46: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 47: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 0 + node_name: security_suite + nic_num: 1 48: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 49: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 4 - nic_id: 1 + node_name: security_suite + nic_num: 2 50: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 51: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 5 - nic_id: 0 + node_name: client_1 + nic_num: 1 52: - action: "HOST_NIC_DISABLE" + action: "host_nic_disable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 53: - action: "HOST_NIC_ENABLE" + action: "host_nic_enable" options: - node_id: 6 - nic_id: 0 + node_name: client_2 + nic_num: 1 reward_function: reward_components: diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 6c8393e2..4ca97a0e 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -49,7 +49,7 @@ def test_application_install_uninstall_on_uc2(): cfg = yaml.safe_load(f) env = PrimaiteGymEnv(env_config=cfg) - env.agent.config.flatten_obs = False + env.agent.config.agent_settings.flatten_obs = False env.reset() _, _, _, _, _ = env.step(0) diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index e90fa591..c0c039f6 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -33,22 +33,22 @@ def test_application_cannot_perform_actions_unless_running(game_and_agent_fixtur browser.close() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("NODE_APPLICATION_SCAN", {"node_id": 0, "application_id": 0}) + action = ("node_application_scan", {"node_id": 0, "application_id": 0}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("NODE_APPLICATION_CLOSE", {"node_id": 0, "application_id": 0}) + action = ("node_application_close", {"node_id": 0, "application_id": 0}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("NODE_APPLICATION_FIX", {"node_id": 0, "application_id": 0}) + action = ("node_application_fix", {"node_id": 0, "application_id": 0}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}) + action = ("node_application_execute", {"node_id": 0, "application_id": 0}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 36fee9a0..2984429a 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -46,7 +46,7 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen server_1: Server = game.simulation.network.get_node_by_hostname("server_1") action = ( - "NODE_APPLICATION_INSTALL", + "node_application_install", {"node_id": 1, "application_name": "C2Beacon"}, ) agent.store_action(action) @@ -54,7 +54,7 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen assert agent.history[-1].response.status == "success" action = ( - "CONFIGURE_C2_BEACON", + "configure_c2_beacon", { "node_id": 1, "config": { @@ -70,7 +70,7 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen assert agent.history[-1].response.status == "success" action = ( - "NODE_APPLICATION_EXECUTE", + "node_application_execute", {"node_id": 1, "application_id": 0}, ) agent.store_action(action) @@ -122,7 +122,7 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA assert agent.history[-1].response.status == "success" action = ( - "C2_SERVER_RANSOMWARE_CONFIGURE", + "c2_server_ransomware_configure", { "node_id": 0, "config": {"server_ip_address": "10.0.2.3", "payload": "ENCRYPT"}, @@ -141,7 +141,7 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA game.step() action = ( - "C2_SERVER_RANSOMWARE_LAUNCH", + "c2_server_ransomware_launch", { "node_id": 0, }, @@ -181,7 +181,7 @@ def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, # C2 Action: Data exfiltrate. action = ( - "C2_SERVER_DATA_EXFILTRATE", + "c2_server_data_exfiltrate", { "node_id": 0, "target_file_name": "database.db", diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 338bd049..0e1a4873 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -27,7 +27,7 @@ class TestConfigureDatabaseAction: def test_configure_ip_password(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["CONFIGURE_DATABASE_CLIENT"] = ConfigureDatabaseClientAction(agent.action_manager) + agent.action_manager.actions["configure_database_client"] = ConfigureDatabaseClientAction(agent.action_manager) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -61,7 +61,7 @@ class TestConfigureDatabaseAction: db_client: DatabaseClient = client_1.software_manager.software["DatabaseClient"] action = ( - "CONFIGURE_DATABASE_CLIENT", + "configure_database_client", { "node_id": 0, "config": { @@ -78,7 +78,7 @@ class TestConfigureDatabaseAction: def test_configure_password(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["CONFIGURE_DATABASE_CLIENT"] = ConfigureDatabaseClientAction(agent.action_manager) + agent.action_manager.actions["configure_database_client"] = ConfigureDatabaseClientAction(agent.action_manager) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -87,7 +87,7 @@ class TestConfigureDatabaseAction: old_ip = db_client.server_ip_address action = ( - "CONFIGURE_DATABASE_CLIENT", + "configure_database_client", { "node_id": 0, "config": { @@ -120,7 +120,7 @@ class TestConfigureRansomwareScriptAction: def test_configure_ip_password(self, game_and_agent, config): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["CONFIGURE_RANSOMWARE_SCRIPT"] = ConfigureRansomwareScriptAction( + agent.action_manager.actions["c2_server_ransomware_configure"] = ConfigureRansomwareScriptAction( agent.action_manager ) @@ -134,7 +134,7 @@ class TestConfigureRansomwareScriptAction: old_payload = ransomware_script.payload action = ( - "CONFIGURE_RANSOMWARE_SCRIPT", + "c2_server_ransomware_configure", {"node_id": 0, "config": config}, ) agent.store_action(action) @@ -151,7 +151,7 @@ class TestConfigureRansomwareScriptAction: def test_invalid_config(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["CONFIGURE_RANSOMWARE_SCRIPT"] = ConfigureRansomwareScriptAction( + agent.action_manager.actions["c2_server_ransomware_configure"] = ConfigureRansomwareScriptAction( agent.action_manager ) @@ -160,7 +160,7 @@ class TestConfigureRansomwareScriptAction: client_1.software_manager.install(RansomwareScript) ransomware_script: RansomwareScript = client_1.software_manager.software["RansomwareScript"] action = ( - "CONFIGURE_RANSOMWARE_SCRIPT", + "c2_server_ransomware_configure", { "node_id": 0, "config": {"server_password": "admin123", "bad_option": 70}, @@ -172,17 +172,17 @@ class TestConfigureRansomwareScriptAction: class TestConfigureDoSBot: - def test_configure_DoSBot(self, game_and_agent): + def test_configure_dos_bot(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["CONFIGURE_DOSBOT"] = ConfigureDoSBotAction(agent.action_manager) + agent.action_manager.actions["configure_dos_bot"] = ConfigureDoSBotAction(agent.action_manager) client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DoSBot) dos_bot: DoSBot = client_1.software_manager.software["DoSBot"] action = ( - "CONFIGURE_DOSBOT", + "configure_dos_bot", { "node_id": 0, "config": { @@ -239,7 +239,7 @@ class TestConfigureYAML: assert db_client.server_password == "correct_password" assert db_client.connect() - def test_configure_ransomware_script(self): + def test_c2_server_ransomware_configure(self): env = PrimaiteGymEnv(env_config=APP_CONFIG_YAML) client_2 = env.game.simulation.network.get_node_by_hostname("client_2") assert client_2.software_manager.software.get("RansomwareScript") is None diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 91aa9fcd..2ed76063 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -33,7 +33,7 @@ def test_create_file(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert client_1.file_system.get_file(folder_name=random_folder, file_name=random_file) is None action = ( - "NODE_FILE_CREATE", + "node_file_create", {"node_id": 0, "folder_name": random_folder, "file_name": random_file}, ) agent.store_action(action) @@ -51,7 +51,7 @@ def test_file_delete_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert file.deleted is False action = ( - "NODE_FILE_DELETE", + "node_file_delete", {"node_id": 0, "folder_id": 0, "file_id": 0}, ) agent.store_action(action) @@ -72,7 +72,7 @@ def test_file_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent assert file.visible_health_status == FileSystemItemHealthStatus.GOOD action = ( - "NODE_FILE_SCAN", + "node_file_scan", {"node_id": 0, "folder_id": 0, "file_id": 0}, ) agent.store_action(action) @@ -93,7 +93,7 @@ def test_file_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert file.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "NODE_FILE_REPAIR", + "node_file_repair", {"node_id": 0, "folder_id": 0, "file_id": 0}, ) agent.store_action(action) @@ -113,7 +113,7 @@ def test_file_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAg assert file.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "NODE_FILE_RESTORE", + "node_file_restore", {"node_id": 0, "folder_id": 0, "file_id": 0}, ) agent.store_action(action) @@ -132,7 +132,7 @@ def test_file_corrupt_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAg assert file.health_status == FileSystemItemHealthStatus.GOOD action = ( - "NODE_FILE_CORRUPT", + "node_file_corrupt", {"node_id": 0, "folder_id": 0, "file_id": 0}, ) agent.store_action(action) @@ -150,7 +150,7 @@ def test_file_access_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert file.num_access == 0 action = ( - "NODE_FILE_ACCESS", + "node_file_access", {"node_id": 0, "folder_name": file.folder_name, "file_name": file.name}, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index 56bbbd4e..1c3cca7b 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -32,7 +32,7 @@ def test_create_folder(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert client_1.file_system.get_folder(folder_name=random_folder) is None action = ( - "NODE_FOLDER_CREATE", + "node_folder_create", { "node_id": 0, "folder_name": random_folder, @@ -60,7 +60,7 @@ def test_folder_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD action = ( - "NODE_FOLDER_SCAN", + "node_folder_scan", { "node_id": 0, # client_1, "folder_id": 0, # downloads @@ -87,7 +87,7 @@ def test_folder_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA assert folder.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "NODE_FOLDER_REPAIR", + "node_folder_repair", { "node_id": 0, # client_1, "folder_id": 0, # downloads @@ -111,7 +111,7 @@ def test_folder_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert folder.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "NODE_FOLDER_RESTORE", + "node_folder_restore", { "node_id": 0, # client_1, "folder_id": 0, # downloads diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index 8846809d..ac92205b 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -29,7 +29,7 @@ def test_nic_cannot_be_turned_off_if_not_on(game_and_agent_fixture: Tuple[Primai assert nic.enabled is False action = ( - "HOST_NIC_DISABLE", + "host_nic_disable", { "node_id": 0, # client_1 "nic_id": 0, # the only nic (eth-1) @@ -50,7 +50,7 @@ def test_nic_cannot_be_turned_on_if_already_on(game_and_agent_fixture: Tuple[Pri assert nic.enabled action = ( - "HOST_NIC_ENABLE", + "host_nic_enable", { "node_id": 0, # client_1 "nic_id": 0, # the only nic (eth-1) @@ -71,7 +71,7 @@ def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[Pr assert nic.enabled action = ( - "HOST_NIC_DISABLE", + "host_nic_disable", { "node_id": 0, # client_1 "nic_id": 0, # the only nic (eth-1) @@ -83,7 +83,7 @@ def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[Pr assert nic.enabled is False action = ( - "HOST_NIC_ENABLE", + "host_nic_enable", { "node_id": 0, # client_1 "nic_id": 0, # the only nic (eth-1) diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index 8fbbbd70..997a9282 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -29,7 +29,7 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.ON # turn it off - action = ("NODE_SHUTDOWN", {"node_id": 0}) + action = ("node_shutdown", {"node_id": 0}) agent.store_action(action) game.step() @@ -43,7 +43,7 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.OFF # turn it on - action = ("NODE_STARTUP", {"node_id": 0}) + action = ("node_startup", {"node_id": 0}) agent.store_action(action) game.step() @@ -65,7 +65,7 @@ def test_node_cannot_be_started_up_if_node_is_already_on(game_and_agent_fixture: assert client_1.operating_state == NodeOperatingState.ON # turn it on - action = ("NODE_STARTUP", {"node_id": 0}) + action = ("node_startup", {"node_id": 0}) agent.store_action(action) game.step() @@ -87,7 +87,7 @@ def test_node_cannot_be_shut_down_if_node_is_already_off(game_and_agent_fixture: assert client_1.operating_state == NodeOperatingState.OFF # turn it ff - action = ("NODE_SHUTDOWN", {"node_id": 0}) + action = ("node_shutdown", {"node_id": 0}) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index ebc9fd3b..dad67d10 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -31,7 +31,7 @@ def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): dns_server.pause() assert dns_server.operating_state == ServiceOperatingState.PAUSED - action = ("NODE_SERVICE_START", {"node_id": 1, "service_id": 0}) + action = ("node_service_start", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.PAUSED @@ -40,7 +40,7 @@ def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("NODE_SERVICE_START", {"node_id": 1, "service_id": 0}) + action = ("node_service_start", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() @@ -54,7 +54,7 @@ def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]) server_1: Server = game.simulation.network.get_node_by_hostname("server_1") dns_server = server_1.software_manager.software.get("DNSServer") - action = ("NODE_SERVICE_RESUME", {"node_id": 1, "service_id": 0}) + action = ("node_service_resume", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.RUNNING @@ -63,7 +63,7 @@ def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]) assert dns_server.operating_state == ServiceOperatingState.PAUSED - action = ("NODE_SERVICE_RESUME", {"node_id": 1, "service_id": 0}) + action = ("node_service_resume", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() @@ -80,27 +80,27 @@ def test_service_cannot_perform_actions_unless_running(game_and_agent_fixture: T dns_server.stop() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("NODE_SERVICE_SCAN", {"node_id": 1, "service_id": 0}) + action = ("node_service_scan", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("NODE_SERVICE_PAUSE", {"node_id": 1, "service_id": 0}) + action = ("node_service_pause", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("NODE_SERVICE_RESUME", {"node_id": 1, "service_id": 0}) + action = ("node_service_resume", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("NODE_SERVICE_RESTART", {"node_id": 1, "service_id": 0}) + action = ("node_service_restart", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("NODE_SERVICE_FIX", {"node_id": 1, "service_id": 0}) + action = ("node_service_fix", {"node_id": 1, "service_id": 0}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index 96110656..beaec5da 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -100,7 +100,7 @@ def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame server_1_um.add_user("user123", "password", is_admin=True) action = ( - "NODE_ACCOUNTS_CHANGE_PASSWORD", + "node_accounts_change_password", { "node_id": 1, # server_1 "username": "user123", @@ -139,7 +139,7 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam # Change password action = ( - "NODE_ACCOUNTS_CHANGE_PASSWORD", + "node_accounts_change_password", { "node_id": 1, # server_1 "username": "user123", @@ -152,7 +152,7 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam # Assert that the user cannot execute an action action = ( - "NODE_SEND_REMOTE_COMMAND", + "node_send_remote_command", { "node_id": 0, "remote_ip": str(server_1.network_interface[1].ip_address), diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 22c00aa4..485ad138 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -15,7 +15,6 @@ def test_mask_contents_correct(): net = sim.network mask = game.action_mask("defender") agent = env.agent - node_list = agent.action_manager.node_names action_map = agent.action_manager.action_map # CHECK NIC ENABLE/DISABLE ACTIONS @@ -23,8 +22,8 @@ def test_mask_contents_correct(): mask = game.action_mask("defender") act_type, act_params = action - if act_type == "NODE_NIC_ENABLE": - node_name = node_list[act_params["node_id"]] + if act_type == "node_nic_enable": + node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) nic_obj = node_obj.network_interface[act_params["nic_id"] + 1] assert nic_obj.enabled @@ -34,8 +33,8 @@ def test_mask_contents_correct(): assert mask[action_num] nic_obj.enable() - if act_type == "NODE_NIC_DISABLE": - node_name = node_list[act_params["node_id"]] + if act_type == "node_nic_disable": + node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) nic_obj = node_obj.network_interface[act_params["nic_id"] + 1] assert nic_obj.enabled @@ -45,14 +44,14 @@ def test_mask_contents_correct(): assert not mask[action_num] nic_obj.enable() - if act_type == "ROUTER_ACL_ADDRULE": + if act_type == "router_acl_add_rule": assert mask[action_num] - if act_type == "ROUTER_ACL_REMOVERULE": + if act_type == "router_acl_remove_rule": assert mask[action_num] - if act_type == "NODE_RESET": - node_name = node_list[act_params["node_id"]] + if act_type == "node_reset": + node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON assert mask[action_num] @@ -61,8 +60,8 @@ def test_mask_contents_correct(): assert not mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "NODE_SHUTDOWN": - node_name = node_list[act_params["node_id"]] + if act_type == "node_shutdown": + node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON assert mask[action_num] @@ -71,8 +70,8 @@ def test_mask_contents_correct(): assert not mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "NODE_OS_SCAN": - node_name = node_list[act_params["node_id"]] + if act_type == "node_os_scan": + node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON assert mask[action_num] @@ -81,8 +80,8 @@ def test_mask_contents_correct(): assert not mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "NODE_STARTUP": - node_name = node_list[act_params["node_id"]] + if act_type == "node_startup": + node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON assert not mask[action_num] @@ -94,12 +93,12 @@ def test_mask_contents_correct(): if act_type == "do_nothing": assert mask[action_num] - if act_type == "NODE_SERVICE_DISABLE": + if act_type == "node_service_disable": assert mask[action_num] - if act_type in ["NODE_SERVICE_SCAN", "NODE_SERVICE_STOP", "NODE_SERVICE_PAUSE"]: - node_name = node_list[act_params["node_id"]] - service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + if act_type in ["node_service_scan", "node_service_stop", "node_service_pause"]: + node_name = act_params["node_name"] + service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) service_obj = node_obj.software_manager.software.get(service_name) assert service_obj.operating_state is ServiceOperatingState.RUNNING @@ -109,9 +108,9 @@ def test_mask_contents_correct(): assert not mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type == "NODE_SERVICE_RESUME": - node_name = node_list[act_params["node_id"]] - service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + if act_type == "node_service_resume": + node_name = act_params["node_name"] + service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) service_obj = node_obj.software_manager.software.get(service_name) assert service_obj.operating_state is ServiceOperatingState.RUNNING @@ -121,9 +120,9 @@ def test_mask_contents_correct(): assert mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type == "NODE_SERVICE_START": - node_name = node_list[act_params["node_id"]] - service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + if act_type == "node_service_start": + node_name = act_params["node_name"] + service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) service_obj = node_obj.software_manager.software.get(service_name) assert service_obj.operating_state is ServiceOperatingState.RUNNING @@ -133,9 +132,9 @@ def test_mask_contents_correct(): assert mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type == "NODE_SERVICE_ENABLE": - node_name = node_list[act_params["node_id"]] - service_name = agent.action_manager.service_names[act_params["node_id"]][act_params["service_id"]] + if act_type == "node_service_enable": + node_name = act_params["node_name"] + service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) service_obj = node_obj.software_manager.software.get(service_name) assert service_obj.operating_state is ServiceOperatingState.RUNNING @@ -145,12 +144,10 @@ def test_mask_contents_correct(): assert mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type in ["NODE_FILE_SCAN", "NODE_FILE_CHECKHASH", "NODE_FILE_DELETE"]: - node_name = node_list[act_params["node_id"]] - folder_name = agent.action_manager.get_folder_name_by_idx(act_params["node_id"], act_params["folder_id"]) - file_name = agent.action_manager.get_file_name_by_idx( - act_params["node_id"], act_params["folder_id"], act_params["file_id"] - ) + if act_type in ["node_file_scan", "node_file_checkhash", "node_file_delete"]: + node_name = act_params["node_name"] + folder_name = act_params["folder_name"] + file_name = act_params["file_name"] node_obj = net.get_node_by_hostname(node_name) file_obj = node_obj.file_system.get_file(folder_name, file_name, include_deleted=True) assert not file_obj.deleted diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 1648d685..3d360313 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -38,7 +38,7 @@ def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, Controlle assert agent.reward_function.current_reward == 0.0 # Check that successfully fetching the webpage yields a reward of 0.7 - agent.store_action(("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0})) + agent.store_action(("node_application_execute", {"node_id": 0, "application_id": 0})) game.step() assert agent.reward_function.current_reward == 0.7 @@ -50,7 +50,7 @@ def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, Controlle src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], ) - agent.store_action(("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0})) + agent.store_action(("node_application_execute", {"node_id": 0, "application_id": 0})) game.step() assert agent.reward_function.current_reward == -0.7 @@ -83,7 +83,7 @@ def test_uc2_rewards(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): response = game.simulation.apply_request(request) state = game.get_sim_state() ahi = AgentHistoryItem( - timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=request, response=response + timestep=0, action="node_application_execute", parameters={}, request=request, response=response ) reward_value = comp.calculate(state, last_action_response=ahi) assert reward_value == 1.0 @@ -94,7 +94,7 @@ def test_uc2_rewards(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): response = game.simulation.apply_request(request) state = game.get_sim_state() ahi = AgentHistoryItem( - timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=request, response=response + timestep=0, action="node_application_execute", parameters={}, request=request, response=response ) reward_value = comp.calculate( state, @@ -159,7 +159,7 @@ def test_action_penalty(): state={}, last_action_response=AgentHistoryItem( timestep=0, - action="NODE_APPLICATION_EXECUTE", + action="node_application_execute", parameters={"node_id": 0, "application_id": 1}, request=["execute"], response=RequestResponse.from_bool(True), @@ -197,7 +197,7 @@ def test_action_penalty_e2e(game_and_agent: tuple[PrimaiteGame, ControlledAgent] game.step() assert agent.reward_function.current_reward == 0.125 - action = ("NODE_FILE_SCAN", {"node_id": 0, "folder_id": 0, "file_id": 0}) + action = ("node_file_scan", {"node_id": 0, "folder_id": 0, "file_id": 0}) agent.store_action(action) game.step() assert agent.reward_function.current_reward == -0.75 diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index cb2bb7a2..dd8d5678 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -19,12 +19,7 @@ from primaite.game.agent.actions.service import ( def test_do_nothing_action_form_request(): """Test that the do_nothingAction can form a request and that it is correct.""" - manager = Mock() - - action = DoNothingAction(manager=manager) - - request = action.form_request() - + request = DoNothingAction.form_request(DoNothingAction.ConfigSchema()) assert request == ["do_nothing"] @@ -52,13 +47,9 @@ def test_do_nothing_action_form_request(): ) # flake8: noqa def test_service_action_form_request(node_name, service_name, expect_to_do_nothing, action_class, action_verb): """Test that the ServiceScanAction can form a request and that it is correct.""" - manager: ActionManager = Mock() - manager.get_node_name_by_idx.return_value = node_name - manager.get_service_name_by_idx.return_value = service_name - - action = action_class(manager=manager, num_nodes=1, num_services=1) - - request = action.form_request(node_id=0, service_id=0) + request = action_class.form_request( + config=action_class.ConfigSchema(node_name=node_name, service_name=service_name) + ) if expect_to_do_nothing: assert request == ["do_nothing"] @@ -77,13 +68,9 @@ def test_service_action_form_request(node_name, service_name, expect_to_do_nothi ) # flake8: noqa def test_service_scan_form_request(node_name, service_name, expect_to_do_nothing): """Test that the ServiceScanAction can form a request and that it is correct.""" - manager: ActionManager = Mock() - manager.get_node_name_by_idx.return_value = node_name - manager.get_service_name_by_idx.return_value = service_name - - action = NodeServiceScanAction(manager=manager, num_nodes=1, num_services=1) - - request = action.form_request(node_id=0, service_id=0) + request = NodeServiceScanAction.form_request( + NodeServiceScanAction.ConfigSchema(node_id=node_name, service_id=service_name) + ) if expect_to_do_nothing: assert request == ["do_nothing"] diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index bb3ad33c..1888e9c1 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -98,7 +98,7 @@ class TestFileSystemRequiresScan: """ cfg = yaml.safe_load(obs_cfg_yaml) - manager = ObservationManager.from_config(cfg) + manager = ObservationManager(cfg) hosts: List[HostObservation] = manager.obs.components["NODES"].hosts for i, host in enumerate(hosts): diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index 94a77a10..f55033fd 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -17,39 +17,39 @@ def test_probabilistic_agent(): """ N_TRIALS = 10_000 P_DO_NOTHING = 0.1 - P_NODE_APPLICATION_EXECUTE = 0.3 - P_NODE_FILE_DELETE = 0.6 + P_node_application_execute = 0.3 + P_node_file_delete = 0.6 MIN_DO_NOTHING = 850 MAX_DO_NOTHING = 1150 - MIN_NODE_APPLICATION_EXECUTE = 2800 - MAX_NODE_APPLICATION_EXECUTE = 3200 - MIN_NODE_FILE_DELETE = 5750 - MAX_NODE_FILE_DELETE = 6250 + MIN_node_application_execute = 2800 + MAX_node_application_execute = 3200 + MIN_node_file_delete = 5750 + MAX_node_file_delete = 6250 action_space_cfg = { - "act_map": { + "action_map": { 0: {"action": "do_nothing", "options": {}}, - 1: {"action": "node_application_execute", "options": {"node_id": 0, "application_id": 0}}, - 2: {"action": "node_file_delete", "options": {"node_id": 0, "folder_id": 0, "file_id": 0}}, + 1: { + "action": "node_application_execute", + "options": {"node_name": "client_1", "application_name": "WebBrowser"}, + }, + 2: { + "action": "node_file_delete", + "options": {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, + }, }, - "options": {}, } game = PrimaiteGame() game.options = PrimaiteGameOptions(ports=[], protocols=[]) - observation_space_cfg = None - - reward_function_cfg = {} - pa_config = { "type": "ProbabilisticAgent", - "game": game, + "ref": "ProbabilisticAgent", + "team": "BLUE", "action_space": action_space_cfg, - "observation_space": observation_space_cfg, - "reward_function": reward_function_cfg, "agent_settings": { - "action_probabilities": {0: P_DO_NOTHING, 1: P_NODE_APPLICATION_EXECUTE, 2: P_NODE_FILE_DELETE}, + "action_probabilities": {0: P_DO_NOTHING, 1: P_node_application_execute, 2: P_node_file_delete}, }, } @@ -70,5 +70,5 @@ def test_probabilistic_agent(): raise AssertionError("Probabilistic agent produced an unexpected action.") assert MIN_DO_NOTHING < do_nothing_count < MAX_DO_NOTHING - assert MIN_NODE_APPLICATION_EXECUTE < node_application_execute_count < MAX_NODE_APPLICATION_EXECUTE - assert MIN_NODE_FILE_DELETE < node_file_delete_count < MAX_NODE_FILE_DELETE + assert MIN_node_application_execute < node_application_execute_count < MAX_node_application_execute + assert MIN_node_file_delete < node_file_delete_count < MAX_node_file_delete diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 91d5c607..289d3941 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -91,7 +91,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="success", data={}) @@ -104,7 +104,7 @@ class TestWebpageUnavailabilitySticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} @@ -114,7 +114,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == 0.0 # agent fails to fetch, get a -1.0 reward - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) @@ -126,7 +126,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) @@ -142,7 +142,7 @@ class TestWebpageUnavailabilitySticky: reward = WebpageUnavailablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) browser_history = [] state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} @@ -152,7 +152,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="success", data={}) @@ -165,7 +165,7 @@ class TestWebpageUnavailabilitySticky: # THE IMPORTANT BIT # agent did nothing, because reward is sticky, it stays at 1.0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( @@ -174,7 +174,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == 1.0 # agent fails to fetch, get a -1.0 reward - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) @@ -186,7 +186,7 @@ class TestWebpageUnavailabilitySticky: assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) @@ -207,7 +207,7 @@ class TestGreenAdminDatabaseUnreachableSticky: reward = GreenAdminDatabaseUnreachablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( @@ -216,7 +216,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="success", data={}) @@ -228,7 +228,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( @@ -237,7 +237,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == 0.0 # agent fails to fetch, get a -1.0 reward - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) @@ -248,7 +248,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) @@ -266,7 +266,7 @@ class TestGreenAdminDatabaseUnreachableSticky: reward = GreenAdminDatabaseUnreachablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( @@ -275,7 +275,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="success", data={}) @@ -287,7 +287,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "DO_NOTHING", {}, ["do_nothing"] + action, params, request = "do_nothing", {}, ["do_nothing"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} last_action_response = AgentHistoryItem( @@ -296,7 +296,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == 1.0 # agent fails to fetch, get a -1.0 reward - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) @@ -307,7 +307,7 @@ class TestGreenAdminDatabaseUnreachableSticky: assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "NODE_APPLICATION_EXECUTE" + action = "node_application_execute" params = {"node_id": 0, "application_id": 0} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py index 6cbf93c8..9cacdccf 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py @@ -46,7 +46,7 @@ def test_file_reveal_to_red_scan(file_system): assert file.revealed_to_red is True -@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented") +@pytest.mark.skip(reason="node_file_checkhash not implemented") def test_simulated_file_check_hash(file_system): file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder") diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 4ec1ec57..2729e5e4 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -32,7 +32,7 @@ def test_file_scan_request(populated_file_system): assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT -@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented") +@pytest.mark.skip(reason="node_file_checkhash not implemented") def test_file_checkhash_request(populated_file_system): """Test that an agent can request a file hash check.""" fs, folder, file = populated_file_system diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py index 473e0db2..10393c6c 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py @@ -120,7 +120,7 @@ def test_folder_corrupt_repair(file_system): assert file.health_status == FileSystemItemHealthStatus.GOOD -@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented") +@pytest.mark.skip(reason="node_file_checkhash not implemented") def test_simulated_folder_check_hash(file_system): folder: Folder = file_system.create_folder(folder_name="test_folder") file_system.create_file(file_name="test_file.txt", folder_name="test_folder") diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 609e29c4..07c1ec46 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -51,7 +51,7 @@ def test_folder_scan_request(populated_file_system): assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT -@pytest.mark.skip(reason="NODE_FOLDER_CHECKHASH not implemented") +@pytest.mark.skip(reason="node_folder_checkhash not implemented") def test_folder_checkhash_request(populated_file_system): """Test that an agent can request a folder hash check.""" fs, folder, file = populated_file_system From dff976b3366d7ee2b0171e5475d75269ee66fa81 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 16 Jan 2025 16:32:55 +0000 Subject: [PATCH 136/224] #2888: Fix merge test failures. --- src/primaite/game/game.py | 2 +- .../system/services/database/database_service.py | 2 ++ .../simulator/system/services/dns/dns_server.py | 1 + tests/assets/configs/bad_primaite_session.yaml | 6 +++--- tests/assets/configs/multi_agent_session.yaml | 6 +++--- .../red_applications/test_c2_suite_integration.py | 11 +++++------ 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 5523c33c..a02f2b26 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -376,7 +376,7 @@ class PrimaiteGame: if service_class is not None: _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") - new_node.software_manager.install(service_class, **service_cfg.get("options", {})) + new_node.software_manager.install(service_class) new_service = new_node.software_manager.software[service_class.__name__] # fixing duration for the service diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index f16b4125..4ba4c4d4 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -30,6 +30,7 @@ class DatabaseService(Service, identifier="DatabaseService"): """ConfigSchema for DatabaseService.""" type: str = "DatabaseService" + backup_server_ip: Optional[IPv4Address] = None config: "DatabaseService.ConfigSchema" = Field(default_factory=lambda: DatabaseService.ConfigSchema()) @@ -51,6 +52,7 @@ class DatabaseService(Service, identifier="DatabaseService"): kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self._create_db_file() + self.backup_server_ip = self.config.backup_server_ip def install(self): """ diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 46008ddf..3a1c0e18 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -21,6 +21,7 @@ class DNSServer(Service, identifier="DNSServer"): """ConfigSchema for DNSServer.""" type: str = "DNSServer" + domain_mapping: dict = {} config: "DNSServer.ConfigSchema" = Field(default_factory=lambda: DNSServer.ConfigSchema()) diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index c83cadc8..6f6a5cfd 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -588,9 +588,9 @@ simulation: default_gateway: 192.168.1.1 services: - type: DNSServer - options: - domain_mapping: - arcd.com: 192.168.1.12 # web server + # options: + # domain_mapping: + # arcd.com: 192.168.1.12 # web server - type: server hostname: web_server diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index a2d64605..29836971 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1380,9 +1380,9 @@ simulation: default_gateway: 192.168.1.1 services: - type: DNSServer - options: - domain_mapping: - arcd.com: 192.168.1.12 # web server + # options: + # domain_mapping: + # arcd.com: 192.168.1.12 # web server - hostname: web_server type: server diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 6eab7361..40226be6 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -496,12 +496,11 @@ def test_c2_suite_yaml(): computer_b: Computer = yaml_network.get_node_by_hostname("node_b") c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") c2_beacon.configure( - c2_server_ip_address=c2_beacon.config.c2_server_ip_address, - keep_alive_frequency=c2_beacon.config.keep_alive_frequency, - masquerade_port=c2_beacon.config.masquerade_port, - masquerade_protocol=c2_beacon.config.masquerade_protocol, - ) - + c2_server_ip_address=c2_beacon.config.c2_server_ip_address, + keep_alive_frequency=c2_beacon.config.keep_alive_frequency, + masquerade_port=c2_beacon.config.masquerade_port, + masquerade_protocol=c2_beacon.config.masquerade_protocol, + ) assert c2_server.operating_state == ApplicationOperatingState.RUNNING From c82865d630fb7c54b8f8109b4ad0efca9b938c7a Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 16 Jan 2025 16:52:49 +0000 Subject: [PATCH 137/224] #2888: Uncomment domain_mapping in test configs. --- tests/assets/configs/bad_primaite_session.yaml | 6 +++--- tests/assets/configs/multi_agent_session.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 6f6a5cfd..c83cadc8 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -588,9 +588,9 @@ simulation: default_gateway: 192.168.1.1 services: - type: DNSServer - # options: - # domain_mapping: - # arcd.com: 192.168.1.12 # web server + options: + domain_mapping: + arcd.com: 192.168.1.12 # web server - type: server hostname: web_server diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 29836971..a2d64605 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1380,9 +1380,9 @@ simulation: default_gateway: 192.168.1.1 services: - type: DNSServer - # options: - # domain_mapping: - # arcd.com: 192.168.1.12 # web server + options: + domain_mapping: + arcd.com: 192.168.1.12 # web server - hostname: web_server type: server From 858406c4a355392c23cbec69c45fd22061bccc57 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 17 Jan 2025 14:38:59 +0000 Subject: [PATCH 138/224] update more tests to new actions schema --- docs/index.rst | 2 +- docs/source/about.rst | 4 +- docs/source/action_masking.rst | 8 +-- .../_package_data/data_manipulation.yaml | 20 +++--- .../_package_data/data_manipulation_marl.yaml | 40 ++++++------ .../scenario_with_placeholders/scenario.yaml | 8 +-- src/primaite/game/agent/actions/abstract.py | 2 +- src/primaite/game/agent/actions/acl.py | 10 +-- .../game/agent/actions/application.py | 2 +- src/primaite/game/agent/actions/host_nic.py | 4 +- src/primaite/game/agent/actions/manager.py | 2 +- src/primaite/game/agent/actions/node.py | 23 +++---- src/primaite/game/agent/actions/session.py | 2 +- src/primaite/game/agent/actions/software.py | 64 ++++++++++--------- src/primaite/game/agent/interface.py | 4 +- src/primaite/game/agent/rewards.py | 4 +- .../scripted_agents/probabilistic_agent.py | 3 +- ...ommand-and-Control-E2E-Demonstration.ipynb | 2 +- ...ege-Escalation-and Data-Loss-Example.ipynb | 6 +- .../system/services/terminal/terminal.py | 2 +- tests/assets/configs/action_penalty.yaml | 20 +++--- tests/assets/configs/basic_firewall.yaml | 4 -- .../configs/basic_switched_network.yaml | 3 - tests/assets/configs/data_manipulation.yaml | 20 +++--- tests/assets/configs/dmz_network.yaml | 2 +- tests/assets/configs/extended_config.yaml | 32 +++++----- .../configs/firewall_actions_network.yaml | 35 +++++----- .../configs/install_and_configure_apps.yaml | 28 ++++---- tests/assets/configs/multi_agent_session.yaml | 40 ++++++------ .../nmap_ping_scan_red_agent_config.yaml | 2 +- tests/assets/configs/shared_rewards.yaml | 32 +++++----- .../configs/test_application_install.yaml | 27 ++++---- .../test_application_request_permission.py | 8 +-- .../actions/test_c2_suite_actions.py | 38 +++++------ .../actions/test_configure_actions.py | 50 +++++---------- .../actions/test_file_request_permission.py | 14 ++-- .../actions/test_folder_request_permission.py | 14 ++-- .../actions/test_nic_request_permission.py | 16 ++--- .../actions/test_node_request_permission.py | 14 ++-- .../test_service_request_permission.py | 18 +++--- .../actions/test_terminal_actions.py | 24 +++---- .../game_layer/test_action_mask.py | 4 +- .../game_layer/test_actions.py | 31 +++------ .../game_layer/test_rewards.py | 8 +-- .../_primaite/_game/_agent/test_actions.py | 40 ++++-------- .../_primaite/_game/_agent/test_agent.py | 2 + .../_game/_agent/test_observations.py | 2 +- .../_game/_agent/test_sticky_rewards.py | 24 +++---- 48 files changed, 351 insertions(+), 413 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 42cc1d6d..aa7d16e0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -70,7 +70,7 @@ PrimAITE incorporates the following features: - Architected with a separate Simulation layer and Game layer. This separation of concerns defines a clear path towards transfer learning with environments of differing fidelity; - Ability to reconfigure an RL reward function based on (a) the ability to counter the modelled adversarial cyber-attack, and (b) the ability to ensure success for green agents; -- Access Control List (ACL) functions for network devices (routers and firewalls), following standard ACL rule format (e.g., DENY / ALLOW, source / destination IP addresses, protocol and port); +- Access Control List (ACL) functions for network devices (routers and firewalls), following standard ACL rule format (e.g., DENY / PERMIT, source / destination IP addresses, protocol and port); - Application of traffic to the links of the system laydown adheres to the ACL rulesets and routing tables contained within each network device; - Provides RL environments adherent to the Farama Foundation Gymnasium (Previously OpenAI Gym) API, allowing integration with any compliant RL Agent frameworks; - Provides RL environments adherent to Ray RLlib environment specifications for single-agent and multi-agent scenarios; diff --git a/docs/source/about.rst b/docs/source/about.rst index da87102a..839bbb0b 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -184,7 +184,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! - 192.168.1.5 - ANY - ANY - All ACL rules are considered when applying an IER. Logic follows the order of rules, so a DENY or ALLOW for the same parameters will override an earlier entry. + All ACL rules are considered when applying an IER. Logic follows the order of rules, so a DENY or PERMIT for the same parameters will override an earlier entry. Observation Spaces ****************** The observation space provides the blue agent with information about the current status of nodes and links. @@ -331,7 +331,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! * Dictionary item {... ,1: [x1, x2, x3, x4, x5, x6] ...} The placeholders inside the list under the key '1' mean the following: * [0, 2] - Action (0 = do nothing, 1 = create rule, 2 = delete rule) - * [0, 1] - Permission (0 = DENY, 1 = ALLOW) + * [0, 1] - Permission (0 = DENY, 1 = PERMIT) * [0, num nodes] - Source IP (0 = any, then 1 -> x resolving to IP addresses) * [0, num nodes] - Dest IP (0 = any, then 1 -> x resolving to IP addresses) * [0, num services] - Protocol (0 = any, then 1 -> x resolving to protocol) diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index c6e4ca59..359ad452 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -134,15 +134,15 @@ The following logic is applied: +------------------------------------------+---------------------------------------------------------------------+ | **C2_SERVER_RANSOMWARE_CONFIGURE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **C2_SERVER_TERMINAL_COMMAND** | Node is on. | +| **c2_server_terminal_command** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ | **C2_SERVER_DATA_EXFILTRATE** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_ACCOUNTS_CHANGE_PASSWORD** | Node is on. | +| **node_account_change_password** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **SSH_TO_REMOTE** | Node is on. | +| **node_session_remote_login** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **SESSIONS_REMOTE_LOGOFF** | Node is on. | +| **node_session_remote_logoff** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ | **NODE_SEND_REMOTE_COMMAND** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 2f6e24b3..fa10a463 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -478,52 +478,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index 53ff0634..b0131c8c 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -470,52 +470,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" @@ -985,52 +985,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index 0223beb6..c692c725 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -78,22 +78,22 @@ agents: action: host_nic_disable options: node_name: client_1 - nic_id: 0 + nic_num: 1 6: action: host_nic_disable options: node_name: server - nic_id: 0 + nic_num: 1 7: action: host_nic_enable options: node_name: client_1 - nic_id: 0 + nic_num: 1 8: action: host_nic_enable options: node_name: server - nic_id: 0 + nic_num: 1 reward_function: reward_components: diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index c570119b..1cda4360 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -18,7 +18,7 @@ class AbstractAction(BaseModel, ABC): """Base configuration schema for Actions.""" model_config = ConfigDict(extra="forbid") - type: str + type: str = "" _registry: ClassVar[Dict[str, Type[AbstractAction]]] = {} diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index ee5ed292..a097b906 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -28,7 +28,7 @@ class ACLAddRuleAbstractAction(AbstractAction, ABC): src_ip: IPV4Address protocol_name: Union[IPProtocol, Literal["ALL"]] - permission: Literal["ALLOW", "DENY"] + permission: Literal["PERMIT", "DENY"] position: int dst_ip: Union[IPV4Address, Literal["ALL"]] src_port: Union[Port, Literal["ALL"]] @@ -70,10 +70,10 @@ class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_ad config.permission, config.protocol_name, str(config.src_ip), - config.src_wildcard, + str(config.src_wildcard), config.src_port, str(config.dst_ip), - config.dst_wildcard, + str(config.dst_wildcard), config.dst_port, config.position, ] @@ -121,10 +121,10 @@ class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_ac config.permission, config.protocol_name, str(config.src_ip), - config.src_wildcard, + str(config.src_wildcard), config.src_port, str(config.dst_ip), - config.dst_wildcard, + str(config.dst_wildcard), config.dst_port, config.position, ] diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 223effc4..f6ce0624 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -19,7 +19,7 @@ class NodeApplicationAbstractAction(AbstractAction, ABC): """ Base class for application actions. - Any action which applies to an application and uses node_id and application_id as its only two parameters can + Any action which applies to an application and uses node_name and application_name as its only two parameters can inherit from this base class. """ diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index b9206b9c..0ca816f3 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -12,8 +12,8 @@ class HostNICAbstractAction(AbstractAction, ABC): """ Abstract base class for NIC actions. - Any action which applies to a NIC and uses node_id and nic_id as its only two parameters can inherit from this base - class. + Any action which applies to a NIC and uses node_name and nic_num as its only two parameters can inherit from this + base class. """ config: "HostNICAbstractAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index fefa22b8..a6e235c5 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -84,7 +84,7 @@ class ActionManager(BaseModel): def form_request(self, action_identifier: str, action_options: Dict) -> RequestFormat: """Take action in CAOS format and use the execution definition to change it into PrimAITE request format.""" act_class = AbstractAction._registry[action_identifier] - config = act_class.ConfigSchema(type=action_identifier, **action_options) + config = act_class.ConfigSchema(**action_options) return act_class.form_request(config=config) @property diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 4a7f725e..95bf5c34 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -4,6 +4,8 @@ from typing import ClassVar, List, Optional, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.port import Port __all__ = ( "NodeOSScanAction", @@ -92,7 +94,7 @@ class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_acti target_ip_address: Union[str, List[str]] show: bool = False - node_name: str + source_node: str @classmethod @abstractmethod @@ -107,18 +109,13 @@ class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_ config: "NodeNMAPPingScanAction.ConfigSchema" - class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): - """Configuration schema for NodeNMAPPingScanAction.""" - - pass - @classmethod - def form_request(cls, config: ConfigSchema) -> List[str]: # noqa + def form_request(cls, config: "NodeNMAPPingScanAction.ConfigSchema") -> List[str]: # noqa """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", "node", - config.node_name, + config.source_node, "application", "NMAP", "ping_scan", @@ -135,8 +132,8 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_ """Configuration Schema for NodeNMAPPortScanAction.""" source_node: str - target_protocol: Optional[Union[str, List[str]]] = (None,) - target_port: Optional[Union[str, List[str]]] = (None,) + target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] = None + target_port: Optional[Union[Port, List[Port]]] = None show: Optional[bool] = (False,) @classmethod @@ -166,11 +163,11 @@ class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, identifier="node_net config: "NodeNetworkServiceReconAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): """Configuration schema for NodeNetworkServiceReconAction.""" - target_protocol: Optional[Union[str, List[str]]] = (None,) - target_port: Optional[Union[str, List[str]]] = (None,) + target_protocol: Optional[Union[IPProtocol, List[IPProtocol]]] = None + target_port: Optional[Union[Port, List[Port]]] = None show: Optional[bool] = (False,) @classmethod diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 1191987b..9720d371 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -55,7 +55,7 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_ config.node_name, "service", "Terminal", - "ssh_to_remote", + "node_session_remote_login", config.username, config.password, config.remote_ip, diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index 760e8dfa..23fbd70d 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -4,7 +4,7 @@ from typing import List, Optional, Union from pydantic import ConfigDict, Field, field_validator, ValidationInfo -from primaite.game.agent.actions.manager import AbstractAction, ActionManager +from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat __all__ = ( @@ -28,36 +28,31 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_rans """Configuration schema for ConfigureRansomwareScriptAction.""" node_name: str - server_ip_address: Optional[str] - server_password: Optional[str] - payload: Optional[str] + server_ip_address: Optional[str] = None + server_password: Optional[str] = None + payload: Optional[str] = None @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] - return [ - "network", - "node", - config.node_name, - "application", - "RansomwareScript", - "configure", - config.model_config, - ] + data = dict( + server_ip_address=config.server_ip_address, + server_password=config.server_password, + payload=config.payload, + ) + return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", data] class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): """Action which sets config parameters for a DoS bot on a node.""" - config: "ConfigureDoSBotAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): """Schema for options that can be passed to this action.""" - node_name: str model_config = ConfigDict(extra="forbid") + node_name: str target_ip_address: Optional[str] = None target_port: Optional[str] = None payload: Optional[str] = None @@ -66,22 +61,24 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): dos_intensity: Optional[float] = None max_sessions: Optional[int] = None - def __init__(self, manager: "ActionManager", **kwargs) -> None: - super().__init__(manager=manager) - - def form_request(self, config: ConfigSchema) -> RequestFormat: + @classmethod + def form_request(config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - if config.node_name is None: - return ["do_nothing"] - self.ConfigSchema.model_validate(config) # check that options adhere to schema - return ["network", "node", config.node_name, "application", "DoSBot", "configure", config] + data = dict( + target_ip_address=config.target_ip_address, + target_port=config.target_port, + payload=config.payload, + repeat=config.repeat, + port_scan_p_of_success=config.port_scan_p_of_success, + dos_intensity=config.dos_intensity, + max_sessions=config.max_sessions, + ) + return ["network", "node", config.node_name, "application", "DoSBot", "configure", data] class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): """Action which configures a C2 Beacon based on the parameters given.""" - config: "ConfigureC2BeaconAction.ConfigSchema" - class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for ConfigureC2BeaconAction.""" @@ -91,6 +88,7 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): masquerade_protocol: str = Field(default="TCP") masquerade_port: str = Field(default="HTTP") + # TODO: this validator should not be needed anymore, test what happens if removed. @field_validator( "c2_server_ip_address", "keep_alive_frequency", @@ -108,7 +106,13 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): @classmethod def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" - return ["network", "node", config.node_name, "application", "C2Beacon", "configure", config] + data = dict( + c2_server_ip_address=config.c2_server_ip_address, + keep_alive_frequency=config.keep_alive_frequency, + masquerade_protocol=config.masquerade_protocol, + masquerade_port=config.masquerade_port, + ) + return ["network", "node", config.node_name, "application", "C2Beacon", "configure", data] class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_command"): @@ -228,11 +232,13 @@ class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_databa """Schema for options that can be passed to this action.""" node_name: str - model_config = ConfigDict(extra="forbid") + server_ip_address: Optional[str] = None + server_password: Optional[str] = None @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: return ["do_nothing"] - return ["network", "node", config.node_name, "application", "DatabaseClient", "configure", config.model_config] + data = {"server_ip_address": config.server_ip_address, "server_password": config.server_password} + return ["network", "node", config.node_name, "application", "DatabaseClient", "configure", data] diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index f5714644..b58cdf29 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -58,9 +58,9 @@ class AbstractAgent(BaseModel, ABC): model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) type: str - ref: str + ref: str = "" """name of the agent.""" - team: Optional[Literal["BLUE", "GREEN", "RED"]] + team: Optional[Literal["BLUE", "GREEN", "RED"]] = None agent_settings: AbstractAgent.AgentSettingsSchema = Field(default=lambda: AbstractAgent.AgentSettingsSchema()) action_space: ActionManager.ConfigSchema = Field(default_factory=lambda: ActionManager.ConfigSchema()) observation_space: ObservationManager.ConfigSchema = Field( diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 2881f967..80be14ef 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -51,7 +51,7 @@ class AbstractReward(BaseModel): class ConfigSchema(BaseModel, ABC): """Config schema for AbstractReward.""" - type: str + type: str = "" _registry: ClassVar[Dict[str, Type["AbstractReward"]]] = {} @@ -404,7 +404,7 @@ class ActionPenalty(AbstractReward, identifier="ACTION_PENALTY"): :rtype: float """ if last_action_response.action == "do_nothing": - return self.do_nothing_penalty + return self.config.do_nothing_penalty else: return self.config.action_penalty diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 20924a95..959eaadc 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -16,7 +16,6 @@ __all__ = "ProbabilisticAgent" class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" - config: "ProbabilisticAgent.ConfigSchema" = Field(default_factory=lambda: ProbabilisticAgent.ConfigSchema()) rng: Generator = np.random.default_rng(np.random.randint(0, 65535)) class AgentSettingsSchema(AbstractScriptedAgent.AgentSettingsSchema): @@ -52,6 +51,8 @@ class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent") default_factory=lambda: ProbabilisticAgent.AgentSettingsSchema() ) + config: "ProbabilisticAgent.ConfigSchema" = Field(default_factory=lambda: ProbabilisticAgent.ConfigSchema()) + @property def probabilities(self) -> Dict[str, int]: """Convenience method to view the probabilities of the Agent.""" diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 278fb3dc..66a684de 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -95,7 +95,7 @@ " node_id: 0\n", " application_id: 0\n", " 4:\n", - " action: C2_SERVER_TERMINAL_COMMAND\n", + " action: c2_server_terminal_command\n", " options:\n", " node_id: 1\n", " ip_address:\n", diff --git a/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb b/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb index c751edfd..fcda4dbd 100644 --- a/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb +++ b/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb @@ -201,7 +201,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"ssh_to_remote\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", + " \"service\", \"Terminal\", \"node_session_remote_login\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] @@ -259,7 +259,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"ssh_to_remote\", \"admin\", \"admin\", str(some_tech_rt.network_interface[4].ip_address)\n", + " \"service\", \"Terminal\", \"node_session_remote_login\", \"admin\", \"admin\", str(some_tech_rt.network_interface[4].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] @@ -396,7 +396,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"ssh_to_remote\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", + " \"service\", \"Terminal\", \"node_session_remote_login\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index e26e77f6..1c249ebb 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -179,7 +179,7 @@ class Terminal(Service): return RequestResponse(status="failure", data={}) rm.add_request( - "ssh_to_remote", + "node_session_remote_login", request_type=RequestType(func=_remote_login), ) diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 2828b5aa..9ab13036 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -386,52 +386,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 154956d3..3a62c75c 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -39,10 +39,6 @@ agents: node_name: client_2 application_name: WebBrowser - reward_function: - reward_components: - - type: DUMMY - agent_settings: action_probabilities: 0: 0.4 diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index e74a6a4e..10a92d7a 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -51,9 +51,6 @@ agents: 0: 0.4 1: 0.6 - - - - ref: defender team: BLUE type: ProxyAgent diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 5e12f1c6..328fe413 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -477,52 +477,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 6cdae6a5..b4d018c8 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -62,7 +62,7 @@ agents: action: node_application_execute options: node_name: client_1 - application_id: WebBrowser + application_name: WebBrowser agent_settings: action_probabilities: diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index 97d9299a..0ec0c91f 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -529,82 +529,82 @@ agents: action: "host_nic_disable" options: node_name: domain_controller - nic_id: 0 + nic_num: 1 63: # old action num: 39 action: "host_nic_enable" options: node_name: domain_controller - nic_id: 0 + nic_num: 1 64: # old action num: 40 action: "host_nic_disable" options: node_name: web_server - nic_id: 0 + nic_num: 1 65: # old action num: 41 action: "host_nic_enable" options: node_name: web_server - nic_id: 0 + nic_num: 1 66: # old action num: 42 action: "host_nic_disable" options: node_name: database_server - nic_id: 0 + nic_num: 1 67: # old action num: 43 action: "host_nic_enable" options: node_name: database_server - nic_id: 0 + nic_num: 1 68: # old action num: 44 action: "host_nic_disable" options: node_name: backup_server - nic_id: 0 + nic_num: 1 69: # old action num: 45 action: "host_nic_enable" options: node_name: backup_server - nic_id: 0 + nic_num: 1 70: # old action num: 46 action: "host_nic_disable" options: node_name: security_suite - nic_id: 0 + nic_num: 1 71: # old action num: 47 action: "host_nic_enable" options: node_name: security_suite - nic_id: 0 + nic_num: 1 72: # old action num: 48 action: "host_nic_disable" options: node_name: security_suite - nic_id: 1 + nic_num: 2 73: # old action num: 49 action: "host_nic_enable" options: node_name: security_suite - nic_id: 1 + nic_num: 2 74: # old action num: 50 action: "host_nic_disable" options: node_name: client_1 - nic_id: 0 + nic_num: 1 75: # old action num: 51 action: "host_nic_enable" options: node_name: client_1 - nic_id: 0 + nic_num: 1 76: # old action num: 52 action: "host_nic_disable" options: node_name: client_2 - nic_id: 0 + nic_num: 1 77: # old action num: 53 action: "host_nic_enable" options: node_name: client_2 - nic_id: 0 + nic_num: 1 diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 41b856fc..ff8e784d 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -109,12 +109,12 @@ agents: position: 1 permission: PERMIT src_ip: 192.168.0.10 - dst_ip: 0.0.0.0 + dst_ip: ALL src_port: 80 dst_port: HTTP protocol_name: TCP - src_wildcard: 0 - dst_wildcard: 0 + src_wildcard: NONE + dst_wildcard: NONE 2: action: firewall_acl_remove_rule options: @@ -135,8 +135,8 @@ agents: src_port: ARP dst_port: DNS protocol_name: ICMP - source_wildcard_id: 0 - dest_wildcard_id: 0 + src_wildcard: NONE + dst_wildcard: NONE 4: action: firewall_acl_remove_rule options: @@ -157,8 +157,8 @@ agents: src_port: HTTP dst_port: HTTP protocol_name: UDP - source_wildcard_id: 0 - dest_wildcard_id: 0 + src_wildcard: NONE + dst_wildcard: NONE 6: action: firewall_acl_remove_rule options: @@ -179,8 +179,8 @@ agents: src_port: HTTP dst_port: HTTP protocol_name: TCP - source_wildcard_id: 0 - dest_wildcard_id: 0 + src_wildcard: NONE + dst_wildcard: NONE 8: action: firewall_acl_remove_rule options: @@ -201,8 +201,8 @@ agents: src_port: POSTGRES_SERVER dst_port: POSTGRES_SERVER protocol_name: ICMP - source_wildcard_id: 0 - dest_wildcard_id: 0 + src_wildcard: NONE + dst_wildcard: NONE 10: action: firewall_acl_remove_rule options: @@ -223,8 +223,8 @@ agents: src_port: NONE dst_port: NONE protocol_name: none - source_wildcard_id: 0 - dest_wildcard_id: 0 + src_wildcard: NONE + dst_wildcard: NONE 12: action: firewall_acl_remove_rule options: @@ -237,17 +237,14 @@ agents: options: type: network_port_disable target_nodename: firewall - port_id: 3 + port_num: 3 14: action: network_port_enable options: type: network_port_enable target_nodename: firewall - port_id: 3 - agent_settings: - start_step: 5 - frequency: 4 - variance: 3 + port_num: 3 + diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index e4b8805e..ecc81668 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -43,34 +43,30 @@ agents: action: configure_database_client options: node_name: client_1 - config: - server_ip_address: 10.0.0.5 + server_ip_address: 10.0.0.5 5: action: configure_database_client options: node_name: client_1 - config: - server_password: correct_password + server_password: correct_password 6: action: c2_server_ransomware_configure options: node_name: client_2 - config: - server_ip_address: 10.0.0.5 - server_password: correct_password - payload: ENCRYPT + server_ip_address: 10.0.0.5 + server_password: correct_password + payload: ENCRYPT 7: action: configure_dos_bot options: node_name: client_3 - config: - target_ip_address: 10.0.0.5 - target_port: POSTGRES_SERVER - payload: DELETE - repeat: true - port_scan_p_of_success: 1.0 - dos_intensity: 1.0 - max_sessions: 1000 + target_ip_address: 10.0.0.5 + target_port: POSTGRES_SERVER + payload: DELETE + repeat: true + port_scan_p_of_success: 1.0 + dos_intensity: 1.0 + max_sessions: 1000 8: action: node_application_install options: diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index bc1f1b69..3b746273 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -469,52 +469,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" @@ -983,52 +983,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index cd485ced..f6d549e8 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -28,7 +28,7 @@ agents: 0: action: node_nmap_ping_scan options: - node_name: client_1 + source_node: client_1 target_ip_address: 192.168.1.0/24 show: False diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 96dada07..7ad5371d 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -519,82 +519,82 @@ agents: action: "host_nic_disable" options: node_name: domain_controller - nic_id: 0 + nic_num: 1 63: # old action num: 39 action: "host_nic_enable" options: node_name: domain_controller - nic_id: 0 + nic_num: 1 64: # old action num: 40 action: "host_nic_disable" options: node_name: web_server - nic_id: 0 + nic_num: 1 65: # old action num: 41 action: "host_nic_enable" options: node_name: web_server - nic_id: 0 + nic_num: 1 66: # old action num: 42 action: "host_nic_disable" options: node_name: database_server - nic_id: 0 + nic_num: 1 67: # old action num: 43 action: "host_nic_enable" options: node_name: database_server - nic_id: 0 + nic_num: 1 68: # old action num: 44 action: "host_nic_disable" options: node_name: backup_server - nic_id: 0 + nic_num: 1 69: # old action num: 45 action: "host_nic_enable" options: node_name: backup_server - nic_id: 0 + nic_num: 1 70: # old action num: 46 action: "host_nic_disable" options: node_name: security_suite - nic_id: 0 + nic_num: 1 71: # old action num: 47 action: "host_nic_enable" options: node_name: security_suite - nic_id: 0 + nic_num: 1 72: # old action num: 48 action: "host_nic_disable" options: node_name: security_suite - nic_id: 1 + nic_num: 2 73: # old action num: 49 action: "host_nic_enable" options: node_name: security_suite - nic_id: 1 + nic_num: 2 74: # old action num: 50 action: "host_nic_disable" options: node_name: client_1 - nic_id: 0 + nic_num: 1 75: # old action num: 51 action: "host_nic_enable" options: node_name: client_1 - nic_id: 0 + nic_num: 1 76: # old action num: 52 action: "host_nic_disable" options: node_name: client_2 - nic_id: 0 + nic_num: 1 77: # old action num: 53 action: "host_nic_enable" options: node_name: client_2 - nic_id: 0 + nic_num: 1 reward_function: reward_components: diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index 55c4afd3..cafcc72b 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -472,52 +472,52 @@ agents: 52: # old action num: 28 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 0 53: # old action num: 29 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 1 54: # old action num: 30 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 2 55: # old action num: 31 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 3 56: # old action num: 32 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 4 57: # old action num: 33 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 5 58: # old action num: 34 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 6 59: # old action num: 35 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 7 60: # old action num: 36 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 8 61: # old action num: 37 action: "router_acl_remove_rule" options: - target_router_hostname: router_1 + target_router: router_1 position: 9 62: # old action num: 38 action: "host_nic_disable" @@ -618,14 +618,13 @@ agents: action: node_application_execute options: node_name: domain_controller - application_id: 0 + application_name: DoSBot 82: action: configure_dos_bot options: node_name: domain_controller - config: - target_ip_address: 192.168.1.14 - target_port: POSTGRES_SERVER + target_ip_address: 192.168.1.14 + target_port: POSTGRES_SERVER reward_function: reward_components: diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index c0c039f6..c47b617b 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -33,22 +33,22 @@ def test_application_cannot_perform_actions_unless_running(game_and_agent_fixtur browser.close() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_scan", {"node_id": 0, "application_id": 0}) + action = ("node_application_scan", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_close", {"node_id": 0, "application_id": 0}) + action = ("node_application_close", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_fix", {"node_id": 0, "application_id": 0}) + action = ("node_application_fix", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_execute", {"node_id": 0, "application_id": 0}) + action = ("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 2984429a..c52c5761 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -47,7 +47,7 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen action = ( "node_application_install", - {"node_id": 1, "application_name": "C2Beacon"}, + {"node_name": "server_1", "application_name": "C2Beacon"}, ) agent.store_action(action) game.step() @@ -56,13 +56,11 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen action = ( "configure_c2_beacon", { - "node_id": 1, - "config": { - "c2_server_ip_address": "10.0.1.2", - "keep_alive_frequency": 5, - "masquerade_protocol": "TCP", - "masquerade_port": "HTTP", - }, + "node_name": "server_1", + "c2_server_ip_address": "10.0.1.2", + "keep_alive_frequency": 5, + "masquerade_protocol": "TCP", + "masquerade_port": "HTTP", }, ) agent.store_action(action) @@ -71,7 +69,7 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen action = ( "node_application_execute", - {"node_id": 1, "application_id": 0}, + {"node_name": "server_1", "application_name": "C2Beacon"}, ) agent.store_action(action) game.step() @@ -103,14 +101,12 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA # C2 Action 1: Installing the RansomwareScript & Database client via Terminal action = ( - "C2_SERVER_TERMINAL_COMMAND", + "c2_server_terminal_command", { - "node_id": 0, + "node_name": "client_1", "ip_address": None, - "account": { - "username": "admin", - "password": "admin", - }, + "username": "admin", + "password": "admin", "commands": [ ["software_manager", "application", "install", "RansomwareScript"], ["software_manager", "application", "install", "DatabaseClient"], @@ -124,7 +120,7 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA action = ( "c2_server_ransomware_configure", { - "node_id": 0, + "node_name": "client_1", "config": {"server_ip_address": "10.0.2.3", "payload": "ENCRYPT"}, }, ) @@ -143,7 +139,7 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA action = ( "c2_server_ransomware_launch", { - "node_id": 0, + "node_name": "client_1", }, ) agent.store_action(action) @@ -183,15 +179,13 @@ def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, action = ( "c2_server_data_exfiltrate", { - "node_id": 0, + "node_name": "client_1", "target_file_name": "database.db", "target_folder_name": "database", "exfiltration_folder_name": "spoils", "target_ip_address": "10.0.2.3", - "account": { - "username": "admin", - "password": "admin", - }, + "username": "admin", + "password": "admin", }, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 0e1a4873..5c9f09e4 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -27,7 +27,6 @@ class TestConfigureDatabaseAction: def test_configure_ip_password(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["configure_database_client"] = ConfigureDatabaseClientAction(agent.action_manager) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -38,10 +37,8 @@ class TestConfigureDatabaseAction: "configure_database_client", { "node_name": "client_1", - "model_config": { - "server_ip_address": "192.168.1.99", - "server_password": "admin123", - }, + "server_ip_address": "192.168.1.99", + "server_password": "admin123", }, ) agent.store_action(action) @@ -53,7 +50,6 @@ class TestConfigureDatabaseAction: def test_configure_ip(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["configure_database_client"] = ConfigureDatabaseClientAction(agent.action_manager) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -63,10 +59,8 @@ class TestConfigureDatabaseAction: action = ( "configure_database_client", { - "node_id": 0, - "config": { - "server_ip_address": "192.168.1.99", - }, + "node_name": "client_1", + "server_ip_address": "192.168.1.99", }, ) agent.store_action(action) @@ -78,7 +72,6 @@ class TestConfigureDatabaseAction: def test_configure_password(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["configure_database_client"] = ConfigureDatabaseClientAction(agent.action_manager) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -89,10 +82,8 @@ class TestConfigureDatabaseAction: action = ( "configure_database_client", { - "node_id": 0, - "config": { - "server_password": "admin123", - }, + "node_name": "client_1", + "server_password": "admin123", }, ) agent.store_action(action) @@ -120,9 +111,6 @@ class TestConfigureRansomwareScriptAction: def test_configure_ip_password(self, game_and_agent, config): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["c2_server_ransomware_configure"] = ConfigureRansomwareScriptAction( - agent.action_manager - ) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -135,7 +123,7 @@ class TestConfigureRansomwareScriptAction: action = ( "c2_server_ransomware_configure", - {"node_id": 0, "config": config}, + {"node_name": "client_1", **config}, ) agent.store_action(action) game.step() @@ -151,9 +139,6 @@ class TestConfigureRansomwareScriptAction: def test_invalid_config(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["c2_server_ransomware_configure"] = ConfigureRansomwareScriptAction( - agent.action_manager - ) # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") @@ -162,7 +147,7 @@ class TestConfigureRansomwareScriptAction: action = ( "c2_server_ransomware_configure", { - "node_id": 0, + "node_name": "client_1", "config": {"server_password": "admin123", "bad_option": 70}, }, ) @@ -175,7 +160,6 @@ class TestConfigureDoSBot: def test_configure_dos_bot(self, game_and_agent): game, agent = game_and_agent agent: ControlledAgent - agent.action_manager.actions["configure_dos_bot"] = ConfigureDoSBotAction(agent.action_manager) client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DoSBot) @@ -184,16 +168,14 @@ class TestConfigureDoSBot: action = ( "configure_dos_bot", { - "node_id": 0, - "config": { - "target_ip_address": "192.168.1.99", - "target_port": "POSTGRES_SERVER", - "payload": "HACC", - "repeat": False, - "port_scan_p_of_success": 0.875, - "dos_intensity": 0.75, - "max_sessions": 50, - }, + "node_name": "client_1", + "target_ip_address": "192.168.1.99", + "target_port": "POSTGRES_SERVER", + "payload": "HACC", + "repeat": False, + "port_scan_p_of_success": 0.875, + "dos_intensity": 0.75, + "max_sessions": 50, }, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 2ed76063..0976abdc 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -34,7 +34,7 @@ def test_create_file(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_create", - {"node_id": 0, "folder_name": random_folder, "file_name": random_file}, + {"node_name": "client_1", "folder_name": random_folder, "file_name": random_file}, ) agent.store_action(action) game.step() @@ -52,7 +52,7 @@ def test_file_delete_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge action = ( "node_file_delete", - {"node_id": 0, "folder_id": 0, "file_id": 0}, + {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) game.step() @@ -73,7 +73,7 @@ def test_file_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent action = ( "node_file_scan", - {"node_id": 0, "folder_id": 0, "file_id": 0}, + {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) game.step() @@ -94,7 +94,7 @@ def test_file_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge action = ( "node_file_repair", - {"node_id": 0, "folder_id": 0, "file_id": 0}, + {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) game.step() @@ -114,7 +114,7 @@ def test_file_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAg action = ( "node_file_restore", - {"node_id": 0, "folder_id": 0, "file_id": 0}, + {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) game.step() @@ -133,7 +133,7 @@ def test_file_corrupt_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAg action = ( "node_file_corrupt", - {"node_id": 0, "folder_id": 0, "file_id": 0}, + {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) game.step() @@ -151,7 +151,7 @@ def test_file_access_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge action = ( "node_file_access", - {"node_id": 0, "folder_name": file.folder_name, "file_name": file.name}, + {"node_name": "client_1", "folder_name": file.folder_name, "file_name": file.name}, ) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index 1c3cca7b..9cd4bfcf 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -34,7 +34,7 @@ def test_create_folder(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_folder_create", { - "node_id": 0, + "node_name": "client_1", "folder_name": random_folder, }, ) @@ -62,8 +62,8 @@ def test_folder_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge action = ( "node_folder_scan", { - "node_id": 0, # client_1, - "folder_id": 0, # downloads + "node_name": "client_1", # client_1, + "folder_name": "downloads", # downloads }, ) agent.store_action(action) @@ -89,8 +89,8 @@ def test_folder_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA action = ( "node_folder_repair", { - "node_id": 0, # client_1, - "folder_id": 0, # downloads + "node_name": "client_1", # client_1, + "folder_name": "downloads", # downloads }, ) agent.store_action(action) @@ -113,8 +113,8 @@ def test_folder_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy action = ( "node_folder_restore", { - "node_id": 0, # client_1, - "folder_id": 0, # downloads + "node_name": "client_1", # client_1, + "folder_name": "downloads", # downloads }, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index ac92205b..11e39c7e 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -31,8 +31,8 @@ def test_nic_cannot_be_turned_off_if_not_on(game_and_agent_fixture: Tuple[Primai action = ( "host_nic_disable", { - "node_id": 0, # client_1 - "nic_id": 0, # the only nic (eth-1) + "node_name": "client_1", # client_1 + "nic_num": 1, # the only nic (eth-1) }, ) agent.store_action(action) @@ -52,8 +52,8 @@ def test_nic_cannot_be_turned_on_if_already_on(game_and_agent_fixture: Tuple[Pri action = ( "host_nic_enable", { - "node_id": 0, # client_1 - "nic_id": 0, # the only nic (eth-1) + "node_name": "client_1", # client_1 + "nic_num": 1, # the only nic (eth-1) }, ) agent.store_action(action) @@ -73,8 +73,8 @@ def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[Pr action = ( "host_nic_disable", { - "node_id": 0, # client_1 - "nic_id": 0, # the only nic (eth-1) + "node_name": "client_1", # client_1 + "nic_num": 1, # the only nic (eth-1) }, ) agent.store_action(action) @@ -85,8 +85,8 @@ def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[Pr action = ( "host_nic_enable", { - "node_id": 0, # client_1 - "nic_id": 0, # the only nic (eth-1) + "node_name": "client_1", # client_1 + "nic_num": 1, # the only nic (eth-1) }, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index 997a9282..8a438673 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -29,28 +29,28 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.ON # turn it off - action = ("node_shutdown", {"node_id": 0}) + action = ("node_shutdown", {"node_name": "client_1"}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.SHUTTING_DOWN for i in range(client_1.shut_down_duration + 1): - action = ("do_nothing", {"node_id": 0}) + action = ("do_nothing", {}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.OFF # turn it on - action = ("node_startup", {"node_id": 0}) + action = ("node_startup", {"node_name": "client_1"}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.BOOTING for i in range(client_1.start_up_duration + 1): - action = ("do_nothing", {"node_id": 0}) + action = ("do_nothing", {}) agent.store_action(action) game.step() @@ -65,7 +65,7 @@ def test_node_cannot_be_started_up_if_node_is_already_on(game_and_agent_fixture: assert client_1.operating_state == NodeOperatingState.ON # turn it on - action = ("node_startup", {"node_id": 0}) + action = ("node_startup", {"node_name": "client_1"}) agent.store_action(action) game.step() @@ -80,14 +80,14 @@ def test_node_cannot_be_shut_down_if_node_is_already_off(game_and_agent_fixture: client_1.power_off() for i in range(client_1.shut_down_duration + 1): - action = ("do_nothing", {"node_id": 0}) + action = ("do_nothing", {}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.OFF # turn it ff - action = ("node_shutdown", {"node_id": 0}) + action = ("node_shutdown", {"node_name": "client_1"}) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index dad67d10..80e68131 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -31,7 +31,7 @@ def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): dns_server.pause() assert dns_server.operating_state == ServiceOperatingState.PAUSED - action = ("node_service_start", {"node_id": 1, "service_id": 0}) + action = ("node_service_start", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.PAUSED @@ -40,7 +40,7 @@ def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_start", {"node_id": 1, "service_id": 0}) + action = ("node_service_start", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() @@ -54,7 +54,7 @@ def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]) server_1: Server = game.simulation.network.get_node_by_hostname("server_1") dns_server = server_1.software_manager.software.get("DNSServer") - action = ("node_service_resume", {"node_id": 1, "service_id": 0}) + action = ("node_service_resume", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.RUNNING @@ -63,7 +63,7 @@ def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]) assert dns_server.operating_state == ServiceOperatingState.PAUSED - action = ("node_service_resume", {"node_id": 1, "service_id": 0}) + action = ("node_service_resume", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() @@ -80,27 +80,27 @@ def test_service_cannot_perform_actions_unless_running(game_and_agent_fixture: T dns_server.stop() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_scan", {"node_id": 1, "service_id": 0}) + action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_pause", {"node_id": 1, "service_id": 0}) + action = ("node_service_pause", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_resume", {"node_id": 1, "service_id": 0}) + action = ("node_service_resume", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_restart", {"node_id": 1, "service_id": 0}) + action = ("node_service_restart", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_fix", {"node_id": 1, "service_id": 0}) + action = ("node_service_fix", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index beaec5da..f15f7156 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -36,9 +36,9 @@ def test_remote_login(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): server_1_usm.add_user("user123", "password", is_admin=True) action = ( - "SSH_TO_REMOTE", + "node_session_remote_login", { - "node_id": 0, + "node_name": "client_1", "username": "user123", "password": "password", "remote_ip": str(server_1.network_interface[1].ip_address), @@ -68,9 +68,9 @@ def test_remote_login_wrong_password(game_and_agent_fixture: Tuple[PrimaiteGame, server_1_usm.add_user("user123", "password", is_admin=True) action = ( - "SSH_TO_REMOTE", + "node_session_remote_login", { - "node_id": 0, + "node_name": "client_1", "username": "user123", "password": "wrong_password", "remote_ip": str(server_1.network_interface[1].ip_address), @@ -100,12 +100,13 @@ def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame server_1_um.add_user("user123", "password", is_admin=True) action = ( - "node_accounts_change_password", + "node_account_change_password", { - "node_id": 1, # server_1 + "node_name": "server_1", # server_1 "username": "user123", "current_password": "password", "new_password": "different_password", + "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) @@ -126,9 +127,9 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam # Log in remotely action = ( - "SSH_TO_REMOTE", + "node_session_remote_login", { - "node_id": 0, + "node_name": "client_1", "username": "user123", "password": "password", "remote_ip": str(server_1.network_interface[1].ip_address), @@ -139,12 +140,13 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam # Change password action = ( - "node_accounts_change_password", + "node_account_change_password", { - "node_id": 1, # server_1 + "node_name": "server_1", # server_1 "username": "user123", "current_password": "password", "new_password": "different_password", + "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) @@ -154,7 +156,7 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam action = ( "node_send_remote_command", { - "node_id": 0, + "node_name": "client_1", "remote_ip": str(server_1.network_interface[1].ip_address), "command": ["file_system", "create", "file", "folder123", "doggo.pdf", False], }, diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 485ad138..75965f16 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -25,7 +25,7 @@ def test_mask_contents_correct(): if act_type == "node_nic_enable": node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) - nic_obj = node_obj.network_interface[act_params["nic_id"] + 1] + nic_obj = node_obj.network_interface[act_params["nic_num"]] assert nic_obj.enabled assert not mask[action_num] nic_obj.disable() @@ -36,7 +36,7 @@ def test_mask_contents_correct(): if act_type == "node_nic_disable": node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) - nic_obj = node_obj.network_interface[act_params["nic_id"] + 1] + nic_obj = node_obj.network_interface[act_params["nic_num"]] assert nic_obj.enabled assert mask[action_num] nic_obj.disable() diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index ff86dbf0..cf8a33ce 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -56,7 +56,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("node_service_scan", {"type": "node_service_scan", "node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -67,7 +67,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("node_service_scan", {"type": "node_service_scan", "node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.COMPROMISED @@ -88,7 +88,7 @@ def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA svc.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a patch action - action = ("node_service_fix", {"type": "node_service_fix", "node_name": "server_1", "service_name": "DNSServer"}) + action = ("node_service_fix", {"node_name": "server_1", "service_name": "DNSServer"}) agent.store_action(action) game.step() @@ -123,7 +123,6 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox action = ( "router_acl_add_rule", { - "type": "router_acl_add_rule", "target_router": "router", "position": 4, "permission": "DENY", @@ -151,7 +150,6 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox action = ( "router_acl_add_rule", { - "type": "router_acl_add_rule", "target_router": "router", "position": 5, # 5th rule "permission": "DENY", # DENY @@ -192,7 +190,6 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P action = ( "router_acl_remove_rule", { - "type": "router_acl_remove_rule", "target_router": "router", "position": 3, # 4th rule }, @@ -226,7 +223,6 @@ def test_host_nic_disable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA action = ( "host_nic_disable", { - "type": "host_nic_disable", "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) }, @@ -258,7 +254,6 @@ def test_host_nic_enable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAg action = ( "host_nic_enable", { - "type": "host_nic_enable", "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) }, @@ -286,7 +281,6 @@ def test_node_file_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAge action = ( "node_file_scan", { - "type": "node_file_scan", "node_name": "client_1", # client_1, "folder_name": "downloads", # downloads, "file_name": "cat.png", # cat.png @@ -324,7 +318,6 @@ def test_node_file_delete_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA action = ( "node_file_delete", { - "type": "node_file_delete", "node_name": "client_1", # client_1 "folder_name": "downloads", # downloads "file_name": "cat.png", # cat.png @@ -348,7 +341,6 @@ def test_node_file_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_create", { - "type": "node_file_create", "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", @@ -370,7 +362,6 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_create", { - "type": "node_file_create", "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", @@ -384,7 +375,6 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_file_access", { - "type": "node_file_access", "node_name": "client_1", "folder_name": "test", "file_name": "file.txt", @@ -405,7 +395,6 @@ def test_node_folder_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): action = ( "node_folder_create", { - "type": "node_folder_create", "node_name": "client_1", "folder_name": "test", }, @@ -434,7 +423,6 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG action = ( "network_port_disable", { - "type": "network_port_disable", "target_nodename": "router", # router "port_id": 1, # port 1 }, @@ -467,7 +455,6 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa action = ( "network_port_enable", { - "type": "network_port_enable", "target_nodename": "router", # router "port_id": 1, # port 1 }, @@ -498,7 +485,7 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P # 2: Scan and check that the visible state is now correct action = ( "node_application_scan", - {"type": "node_application_scan", "node_name": "client_1", "application_name": "WebBrowser"}, + {"node_name": "client_1", "application_name": "WebBrowser"}, ) agent.store_action(action) game.step() @@ -512,7 +499,7 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P # 4: Scan and check that the visible state is now correct action = ( "node_application_scan", - {"type": "node_application_scan", "node_name": "client_1", "application_name": "WebBrowser"}, + {"node_name": "client_1", "application_name": "WebBrowser"}, ) agent.store_action(action) game.step() @@ -536,7 +523,7 @@ def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, Pr # 2: Apply a fix action action = ( "node_application_fix", - {"type": "node_application_fix", "node_name": "client_1", "application_name": "WebBrowser"}, + {"node_name": "client_1", "application_name": "WebBrowser"}, ) agent.store_action(action) game.step() @@ -565,7 +552,7 @@ def test_node_application_close_integration(game_and_agent: Tuple[PrimaiteGame, # 2: Apply a close action action = ( "node_application_close", - {"type": "node_application_close", "node_name": "client_1", "application_name": "WebBrowser"}, + {"node_name": "client_1", "application_name": "WebBrowser"}, ) agent.store_action(action) game.step() @@ -587,7 +574,7 @@ def test_node_application_install_and_uninstall_integration(game_and_agent: Tupl action = ( "node_application_install", - {"type": "node_application_install", "node_name": "client_1", "application_name": "DoSBot"}, + {"node_name": "client_1", "application_name": "DoSBot"}, ) agent.store_action(action) game.step() @@ -596,7 +583,7 @@ def test_node_application_install_and_uninstall_integration(game_and_agent: Tupl action = ( "node_application_remove", - {"type": "node_application_remove", "node_name": "client_1", "application_name": "DoSBot"}, + {"node_name": "client_1", "application_name": "DoSBot"}, ) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 3d360313..91a022d5 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -38,7 +38,7 @@ def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, Controlle assert agent.reward_function.current_reward == 0.0 # Check that successfully fetching the webpage yields a reward of 0.7 - agent.store_action(("node_application_execute", {"node_id": 0, "application_id": 0})) + agent.store_action(("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"})) game.step() assert agent.reward_function.current_reward == 0.7 @@ -50,7 +50,7 @@ def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, Controlle src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], ) - agent.store_action(("node_application_execute", {"node_id": 0, "application_id": 0})) + agent.store_action(("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"})) game.step() assert agent.reward_function.current_reward == -0.7 @@ -160,7 +160,7 @@ def test_action_penalty(): last_action_response=AgentHistoryItem( timestep=0, action="node_application_execute", - parameters={"node_id": 0, "application_id": 1}, + parameters={"node_name": "client", "application_name": "WebBrowser"}, request=["execute"], response=RequestResponse.from_bool(True), ), @@ -197,7 +197,7 @@ def test_action_penalty_e2e(game_and_agent: tuple[PrimaiteGame, ControlledAgent] game.step() assert agent.reward_function.current_reward == 0.125 - action = ("node_file_scan", {"node_id": 0, "folder_id": 0, "file_id": 0}) + action = ("node_file_scan", {"node_name": "client", "folder_name": "downloads", "file_name": "document.pdf"}) agent.store_action(action) game.step() assert agent.reward_function.current_reward == -0.75 diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index dd8d5678..5750befd 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -2,6 +2,7 @@ from unittest.mock import Mock import pytest +from pydantic import ValidationError from primaite.game.agent.actions import ActionManager from primaite.game.agent.actions.manager import DoNothingAction @@ -37,7 +38,7 @@ def test_do_nothing_action_form_request(): ], ) # flake8: noqa @pytest.mark.parametrize( - "node_name, service_name, expect_to_do_nothing", + "node_name, service_name, expect_failure", [ ("pc_1", "chrome", False), (None, "chrome", True), @@ -45,34 +46,15 @@ def test_do_nothing_action_form_request(): (None, None, True), ], ) # flake8: noqa -def test_service_action_form_request(node_name, service_name, expect_to_do_nothing, action_class, action_verb): +def test_service_action_form_request(node_name, service_name, expect_failure, action_class, action_verb): """Test that the ServiceScanAction can form a request and that it is correct.""" - request = action_class.form_request( - config=action_class.ConfigSchema(node_name=node_name, service_name=service_name) - ) - - if expect_to_do_nothing: - assert request == ["do_nothing"] + if expect_failure: + with pytest.raises(ValidationError): + request = action_class.form_request( + config=action_class.ConfigSchema(node_name=node_name, service_name=service_name) + ) else: + request = action_class.form_request( + config=action_class.ConfigSchema(node_name=node_name, service_name=service_name) + ) assert request == ["network", "node", node_name, "service", service_name, action_verb] - - -@pytest.mark.parametrize( - "node_name, service_name, expect_to_do_nothing", - [ - ("pc_1", "chrome", False), - (None, "chrome", True), - ("pc_1", None, True), - (None, None, True), - ], -) # flake8: noqa -def test_service_scan_form_request(node_name, service_name, expect_to_do_nothing): - """Test that the ServiceScanAction can form a request and that it is correct.""" - request = NodeServiceScanAction.form_request( - NodeServiceScanAction.ConfigSchema(node_id=node_name, service_id=service_name) - ) - - if expect_to_do_nothing: - assert request == ["do_nothing"] - else: - assert request == ["network", "node", node_name, "service", service_name, "scan"] diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_agent.py index 5f3b4fc0..7956a44f 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent.py @@ -39,6 +39,8 @@ def test_creating_agent_from_dict(): } agent = RandomAgent( config={ + "ref": "random_agent", + "team": "BLUE", "action_space": action_config, "observation_space": observation_config, "reward_function": reward_config, diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index 1888e9c1..5170bcf3 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -98,7 +98,7 @@ class TestFileSystemRequiresScan: """ cfg = yaml.safe_load(obs_cfg_yaml) - manager = ObservationManager(cfg) + manager = ObservationManager(config=cfg) hosts: List[HostObservation] = manager.obs.components["NODES"].hosts for i, host in enumerate(hosts): diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 289d3941..5af71319 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -92,7 +92,7 @@ class TestWebpageUnavailabilitySticky: # agent did a successful fetch action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "WebBrowser"} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="success", data={}) browser_history.append({"outcome": 200}) @@ -115,7 +115,7 @@ class TestWebpageUnavailabilitySticky: # agent fails to fetch, get a -1.0 reward action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "WebBrowser"} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) @@ -127,7 +127,7 @@ class TestWebpageUnavailabilitySticky: # agent fails again to fetch, get a -1.0 reward again action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "WebBrowser"} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) @@ -153,7 +153,7 @@ class TestWebpageUnavailabilitySticky: # agent did a successful fetch action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "WebBrowser"} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="success", data={}) browser_history.append({"outcome": 200}) @@ -175,7 +175,7 @@ class TestWebpageUnavailabilitySticky: # agent fails to fetch, get a -1.0 reward action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "WebBrowser"} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) @@ -187,7 +187,7 @@ class TestWebpageUnavailabilitySticky: # agent fails again to fetch, get a -1.0 reward again action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "WebBrowser"} request = ["network", "node", "computer", "application", "WebBrowser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) @@ -217,7 +217,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent did a successful fetch action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "DatabaseClient"} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} @@ -238,7 +238,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent fails to fetch, get a -1.0 reward action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "DatabaseClient"} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} @@ -249,7 +249,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent fails again to fetch, get a -1.0 reward again action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "DatabaseClient"} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} @@ -276,7 +276,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent did a successful fetch action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "DatabaseClient"} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="success", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} @@ -297,7 +297,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent fails to fetch, get a -1.0 reward action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "DatabaseClient"} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} @@ -308,7 +308,7 @@ class TestGreenAdminDatabaseUnreachableSticky: # agent fails again to fetch, get a -1.0 reward again action = "node_application_execute" - params = {"node_id": 0, "application_id": 0} + params = {"node_name": "computer", "application_name": "DatabaseClient"} request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] response = RequestResponse(status="failure", data={}) state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} From 4481d073e4ea34eb47bcded32e9d3a24c81454e7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 20 Jan 2025 08:35:11 +0000 Subject: [PATCH 139/224] Fix action config schemas and formrequest method for dos bot action --- src/primaite/game/agent/actions/network.py | 6 +++--- src/primaite/game/agent/actions/software.py | 2 +- .../game_layer/actions/test_c2_suite_actions.py | 3 ++- tests/integration_tests/game_layer/test_actions.py | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index 7f1e069a..d244fb74 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -17,20 +17,20 @@ class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstrac """Base configuration schema for NetworkPort actions.""" target_nodename: str - port_id: int + port_num: int verb: ClassVar[str] @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - if config.target_nodename is None or config.port_id is None: + if config.target_nodename is None or config.port_num is None: return ["do_nothing"] return [ "network", "node", config.target_nodename, "network_interface", - config.port_id, + config.port_num, config.verb, ] diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index 23fbd70d..da751a15 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -62,7 +62,7 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): max_sessions: Optional[int] = None @classmethod - def form_request(config: ConfigSchema) -> RequestFormat: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" data = dict( target_ip_address=config.target_ip_address, diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index c52c5761..59eb8a60 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -121,7 +121,8 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA "c2_server_ransomware_configure", { "node_name": "client_1", - "config": {"server_ip_address": "10.0.2.3", "payload": "ENCRYPT"}, + "server_ip_address": "10.0.2.3", + "payload": "ENCRYPT", }, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index cf8a33ce..80f359da 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -424,7 +424,7 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG "network_port_disable", { "target_nodename": "router", # router - "port_id": 1, # port 1 + "port_num": 2, # port 1 }, ) agent.store_action(action) @@ -456,7 +456,7 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa "network_port_enable", { "target_nodename": "router", # router - "port_id": 1, # port 1 + "port_num": 2, # port 1 }, ) agent.store_action(action) From 4c0f87e8aad39e6b01c907e10a22d481283716c7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 20 Jan 2025 10:23:13 +0000 Subject: [PATCH 140/224] Fix configure actions that were accidentally combined --- src/primaite/game/agent/actions/software.py | 13 ++++++++++++- .../assets/configs/install_and_configure_apps.yaml | 2 +- .../configs/nmap_port_scan_red_agent_config.yaml | 1 - .../game_layer/actions/test_configure_actions.py | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index da751a15..aeb7c582 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -19,7 +19,7 @@ __all__ = ( ) -class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_ransomware_configure"): +class ConfigureRansomwareScriptAction(AbstractAction, identifier="configure_ransomware_script"): """Action which sets config parameters for a ransomware script on a node.""" config: "ConfigureRansomwareScriptAction.ConfigSchema" @@ -45,6 +45,17 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="c2_server_rans return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", data] +class RansomwareConfigureC2ServerAction(ConfigureRansomwareScriptAction, identifier="c2_server_ransomware_configure"): + """Action which causes a C2 server to send a command to set options on a ransomware script remotely.""" + + @classmethod + def form_request(cls, config: ConfigureRansomwareScriptAction.ConfigSchema) -> RequestFormat: + data = dict( + server_ip_address=config.server_ip_address, server_password=config.server_password, payload=config.payload + ) + return ["network", "node", config.node_name, "application", "C2Server", "ransomware_configure", data] + + class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): """Action which sets config parameters for a DoS bot on a node.""" diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index ecc81668..2baca409 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -50,7 +50,7 @@ agents: node_name: client_1 server_password: correct_password 6: - action: c2_server_ransomware_configure + action: configure_ransomware_script options: node_name: client_2 server_ip_address: 10.0.0.5 diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 09e88a76..873401b9 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -30,7 +30,6 @@ agents: options: source_node: client_1 target_ip_address: 192.168.10.0/24 - target_protocol: tcp target_port: - 21 - 53 diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 5c9f09e4..17559405 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -122,7 +122,7 @@ class TestConfigureRansomwareScriptAction: old_payload = ransomware_script.payload action = ( - "c2_server_ransomware_configure", + "configure_ransomware_script", {"node_name": "client_1", **config}, ) agent.store_action(action) @@ -145,7 +145,7 @@ class TestConfigureRansomwareScriptAction: client_1.software_manager.install(RansomwareScript) ransomware_script: RansomwareScript = client_1.software_manager.software["RansomwareScript"] action = ( - "c2_server_ransomware_configure", + "configure_ransomware_script", { "node_name": "client_1", "config": {"server_password": "admin123", "bad_option": 70}, From 18a665e56246b376bade49ae786ca41ed8d5836c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 20 Jan 2025 14:07:51 +0000 Subject: [PATCH 141/224] Update actions and agents to get all tests passing post-refactor --- src/primaite/game/agent/actions/software.py | 1 + .../scripted_agents/probabilistic_agent.py | 2 +- src/primaite/game/game.py | 27 ------------------- .../configs/firewall_actions_network.yaml | 16 +++++------ .../game_layer/test_actions.py | 16 +++++------ 5 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index aeb7c582..12462ede 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -84,6 +84,7 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): dos_intensity=config.dos_intensity, max_sessions=config.max_sessions, ) + data = {k: v for k, v in data.items() if v is not None} return ["network", "node", config.node_name, "application", "DoSBot", "configure", data] diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 959eaadc..de643ed8 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -16,7 +16,7 @@ __all__ = "ProbabilisticAgent" class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" - rng: Generator = np.random.default_rng(np.random.randint(0, 65535)) + rng: Generator = Field(default_factory=lambda: np.random.default_rng(np.random.randint(0, 65535))) class AgentSettingsSchema(AbstractScriptedAgent.AgentSettingsSchema): """Schema for the `agent_settings` part of the agent config.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 198e8750..8bc37597 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -476,33 +476,6 @@ class PrimaiteGame: if isinstance(new_agent, ProxyAgent): game.rl_agents[agent_cfg["ref"]] = new_agent - # agent_name = agent_cfg["ref"] # noqa: F841 - # agent_type = agent_cfg["type"] - # action_space_cfg = agent_cfg["action_space"] - # observation_space_cfg = agent_cfg["observation_space"] - # reward_function_cfg = agent_cfg["reward_function"] - # agent_settings = agent_cfg["agent_settings"] - - # agent_config = { - # "type": agent_type, - # "action_space": action_space_cfg, - # "observation_space": observation_space_cfg, - # "reward_function": reward_function_cfg, - # "agent_settings": agent_settings, - # "game": game, - # } - - # # CREATE AGENT - # if agent_type in AbstractAgent._registry: - # new_agent = AbstractAgent._registry[agent_cfg["type"]].from_config(config=agent_config) - # # If blue agent is created, add to game.rl_agents - # if agent_type == "ProxyAgent": - # game.rl_agents[agent_cfg["ref"]] = new_agent - # else: - # msg = f"Configuration error: {agent_type} is not a valid agent type." - # _LOGGER.error(msg) - # raise ValueError(msg) - # Validate that if any agents are sharing rewards, they aren't forming an infinite loop. game.setup_reward_sharing() diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index ff8e784d..6b454a12 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -110,9 +110,9 @@ agents: permission: PERMIT src_ip: 192.168.0.10 dst_ip: ALL - src_port: 80 - dst_port: HTTP - protocol_name: TCP + src_port: ALL + dst_port: ALL + protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE 2: @@ -131,7 +131,7 @@ agents: position: 1 permission: DENY src_ip: 192.168.0.10 # client 1 - dest_ip: ALL + dst_ip: ALL src_port: ARP dst_port: DNS protocol_name: ICMP @@ -153,7 +153,7 @@ agents: position: 1 permission: DENY src_ip: 192.168.10.10 # dmz_server - dest_ip: 192.168.0.10 # client_1 + dst_ip: 192.168.0.10 # client_1 src_port: HTTP dst_port: HTTP protocol_name: UDP @@ -175,7 +175,7 @@ agents: position: 2 permission: DENY src_ip: 192.168.10.10 # dmz_server - dest_ip: 192.168.0.10 # client_1 + dst_ip: 192.168.0.10 # client_1 src_port: HTTP dst_port: HTTP protocol_name: TCP @@ -197,7 +197,7 @@ agents: position: 10 permission: DENY src_ip: 192.168.20.10 # external_computer - dest_ip: 192.168.10.10 # dmz + dst_ip: 192.168.10.10 # dmz src_port: POSTGRES_SERVER dst_port: POSTGRES_SERVER protocol_name: ICMP @@ -219,7 +219,7 @@ agents: position: 1 permission: DENY src_ip: 192.168.20.10 # external_computer - dest_ip: 192.168.0.10 # client_1 + dst_ip: 192.168.0.10 # client_1 src_port: NONE dst_port: NONE protocol_name: none diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index cef41e1b..800549bc 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -127,12 +127,12 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox "position": 4, "permission": "DENY", "src_ip": "10.0.1.2", - "src_wildcard": 0, - "src_port": "HTTP", + "src_wildcard": "NONE", + "src_port": "ALL", "dst_ip": "10.0.2.3", - "dst_wildcard": 0, - "dst_port": "HTTP", - "protocol_name": "udp", + "dst_wildcard": "NONE", + "dst_port": "ALL", + "protocol_name": "icmp", }, ) agent.store_action(action) @@ -155,7 +155,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox "permission": "DENY", # DENY "src_ip": "10.0.2.2", # 10.0.2.2 (server_1) "src_wildcard": 0, - "source_port": "ALL", # ALL + "src_port": "ALL", # ALL "dst_ip": "10.0.2.3", # 10.0.2.3 (server_2) "dst_wildcard": 0, "dst_port": "ALL", # ALL @@ -424,7 +424,7 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG "network_port_disable", { "target_nodename": "router", # router - "port_num": 2, # port 1 + "port_num": 1, # port 1 }, ) agent.store_action(action) @@ -456,7 +456,7 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa "network_port_enable", { "target_nodename": "router", # router - "port_num": 2, # port 1 + "port_num": 1, # port 1 }, ) agent.store_action(action) From 4b79c88ae50b9d2b6aecda345ec03639ad1bbff9 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 21 Jan 2025 10:42:09 +0000 Subject: [PATCH 142/224] Fix typos and TODOs --- docs/source/action_masking.rst | 121 ++++++++---------- .../how_to_guides/extensible_actions.rst | 6 +- .../how_to_guides/extensible_agents.rst | 2 +- src/primaite/game/agent/actions/acl.py | 6 +- src/primaite/game/agent/actions/manager.py | 1 - src/primaite/game/agent/actions/node.py | 12 +- src/primaite/game/agent/actions/software.py | 17 +-- src/primaite/game/agent/interface.py | 2 +- 8 files changed, 67 insertions(+), 100 deletions(-) diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index 359ad452..4331b090 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -20,131 +20,120 @@ Masking Logic ============= The following logic is applied: - -..only:: comment - - TODO: update table - +------------------------------------------+---------------------------------------------------------------------+ | Action | Action Mask Logic | +==========================================+=====================================================================+ -| **DONOTHING** | Always Possible. | +| **do_nothing** | Always Possible. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_SCAN** | Node is on. Service is running. | +| **node_service_scan** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_STOP** | Node is on. Service is running. | +| **node_service_stop** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_START** | Node is on. Service is stopped. | +| **node_service_start** | Node is on. Service is stopped. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_PAUSE** | Node is on. Service is running. | +| **node_service_pause** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_RESUME** | Node is on. Service is paused. | +| **node_service_resume** | Node is on. Service is paused. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_RESTART** | Node is on. Service is running. | +| **node_service_restart** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_DISABLE** | Node is on. | +| **node_service_disable** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_ENABLE** | Node is on. Service is disabled. | +| **node_service_enable** | Node is on. Service is disabled. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SERVICE_FIX** | Node is on. Service is running. | +| **node_service_fix** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_APPLICATION_EXECUTE** | Node is on. | +| **node_application_execute** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_APPLICATION_SCAN** | Node is on. Application is running. | +| **node_application_scan** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_APPLICATION_CLOSE** | Node is on. Application is running. | +| **node_application_close** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_APPLICATION_FIX** | Node is on. Application is running. | +| **node_application_fix** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_APPLICATION_INSTALL** | Node is on. | +| **node_application_install** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_APPLICATION_REMOVE** | Node is on. | +| **node_application_remove** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_SCAN** | Node is on. File exists. File not deleted. | +| **node_file_scan** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_CREATE** | Node is on. | +| **node_file_create** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_CHECKHASH** | Node is on. File exists. File not deleted. | +| **node_file_checkhash** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_DELETE** | Node is on. File exists. | +| **node_file_delete** | Node is on. File exists. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_REPAIR** | Node is on. File exists. File not deleted. | +| **node_file_repair** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_RESTORE** | Node is on. File exists. File is deleted. | +| **node_file_restore** | Node is on. File exists. File is deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_CORRUPT** | Node is on. File exists. File not deleted. | +| **node_file_corrupt** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FILE_ACCESS** | Node is on. File exists. File not deleted. | +| **node_file_access** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FOLDER_CREATE** | Node is on. | +| **node_folder_create** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FOLDER_SCAN** | Node is on. Folder exists. Folder not deleted. | +| **node_folder_scan** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FOLDER_CHECKHASH** | Node is on. Folder exists. Folder not deleted. | +| **node_folder_checkhash** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FOLDER_REPAIR** | Node is on. Folder exists. Folder not deleted. | +| **node_folder_repair** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_FOLDER_RESTORE** | Node is on. Folder exists. Folder is deleted. | +| **node_folder_restore** | Node is on. Folder exists. Folder is deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_OS_SCAN** | Node is on. | +| **node_os_scan** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **HOST_NIC_ENABLE** | NIC is disabled. Node is on. | +| **host_nic_enable** | NIC is disabled. Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **HOST_NIC_DISABLE** | NIC is enabled. Node is on. | +| **host_nic_disable** | NIC is enabled. Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SHUTDOWN** | Node is on. | +| **node_shutdown** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_STARTUP** | Node is off. | +| **node_startup** | Node is off. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_RESET** | Node is on. | +| **node_reset** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_NMAP_PING_SCAN** | Node is on. | +| **node_nmap_ping_scan** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_NMAP_PORT_SCAN** | Node is on. | +| **node_nmap_port_scan** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_NMAP_NETWORK_SERVICE_RECON** | Node is on. | +| **node_network_service_recon** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NETWORK_PORT_ENABLE** | Node is on. Router is on. | +| **network_port_enable** | Node is on. Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NETWORK_PORT_DISABLE** | Router is on. | +| **network_port_disable** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **ROUTER_ACL_ADDRULE** | Router is on. | +| **router_acl_addrule** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **ROUTER_ACL_REMOVERULE** | Router is on. | +| **router_acl_removerule** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **FIREWALL_ACL_ADDRULE** | Firewall is on. | +| **firewall_acl_addrule** | Firewall is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **FIREWALL_ACL_REMOVERULE** | Firewall is on. | +| **firewall_acl_removerule** | Firewall is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_NMAP_PING_SCAN** | Node is on. | +| **configure_database_client** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_NMAP_PORT_SCAN** | Node is on. | +| **configure_ransomware_script** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_NMAP_NETWORK_SERVICE_RECON** | Node is on. | +| **c2_server_ransomware_configure** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **CONFIGURE_DATABASE_CLIENT** | Node is on. | +| **configure_dos_bot** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **c2_server_ransomware_configure** | Node is on. | +| **configure_c2_beacon** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **configure_dos_bot** | Node is on. | -+------------------------------------------+---------------------------------------------------------------------+ -| **CONFIGURE_C2_BEACON** | Node is on. | -+------------------------------------------+---------------------------------------------------------------------+ -| **C2_SERVER_RANSOMWARE_LAUNCH** | Node is on. | -+------------------------------------------+---------------------------------------------------------------------+ -| **C2_SERVER_RANSOMWARE_CONFIGURE** | Node is on. | +| **c2_server_ransomware_launch** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ | **c2_server_terminal_command** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **C2_SERVER_DATA_EXFILTRATE** | Node is on. | +| **c2_server_data_exfiltrate** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_account_change_password** | Node is on. | +| **node_account_change_password** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_session_remote_login** | Node is on. | +| **node_session_remote_login** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_session_remote_logoff** | Node is on. | +| **node_session_remote_logoff** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **NODE_SEND_REMOTE_COMMAND** | Node is on. | +| **node_send_remote_command** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 0064a3a7..93a6cf21 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -22,7 +22,7 @@ Custom actions within PrimAITE must be a sub-class of `AbstractAction`, and cont #. Unique Identifier -#. `from_request` method. +#. `form_request` method. ConfigSchema @@ -61,7 +61,7 @@ When declaring a custom class, it must have a unique identifier string, that all The above action would fail pydantic validation as the identifier "node_folder_create" is already used by the `NodeFolderCreateAction`, and would create a duplicate listing within `AbstractAction._registry`. -from_request method +form_request method ################### -PrimAITE actions need to be have a `from_request` method, which can be passed to the `RequestManager` for processing. This allows the custom action to be actioned within the simulation environment. +PrimAITE actions need to have a `form_request` method, which can be passed to the `RequestManager` for processing. This allows the custom action to be actioned within the simulation environment. diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 4b6f8598..256e96ca 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -66,7 +66,7 @@ The core features that should be implemented in any new agent are detailed below #. **Identifiers**: - All agent classes should have a ``identifier`` attribute, a unique kebab-case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. + All agent classes should have an ``identifier`` attribute, a unique kebab-case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. Changes to YAML file ==================== diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index a097b906..3341868f 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import ABC -from typing import List, Literal, Union +from typing import Literal, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -59,7 +59,7 @@ class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_ad target_router: str @classmethod - def form_request(cls, config: ConfigSchema) -> List[str]: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", @@ -143,7 +143,7 @@ class FirewallACLRemoveRuleAction(ACLRemoveRuleAbstractAction, identifier="firew firewall_port_direction: str @classmethod - def form_request(cls, config: ConfigSchema) -> List[str]: + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index a6e235c5..3e5b21b1 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -18,7 +18,6 @@ from typing import Dict, Tuple from gymnasium import spaces from pydantic import BaseModel, ConfigDict, Field, field_validator -# from primaite.game.game import PrimaiteGame # TODO: Breaks things from primaite.game.agent.actions.abstract import AbstractAction from primaite.interface.request import RequestFormat diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 95bf5c34..fbab18f0 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -110,7 +110,7 @@ class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_ config: "NodeNMAPPingScanAction.ConfigSchema" @classmethod - def form_request(cls, config: "NodeNMAPPingScanAction.ConfigSchema") -> List[str]: # noqa + def form_request(cls, config: "NodeNMAPPingScanAction.ConfigSchema") -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", @@ -137,10 +137,7 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_ show: Optional[bool] = (False,) @classmethod - def form_request( - cls, - config: ConfigSchema, - ) -> List[str]: # noqa + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", @@ -171,10 +168,7 @@ class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, identifier="node_net show: Optional[bool] = (False,) @classmethod - def form_request( - cls, - config: ConfigSchema, - ) -> List[str]: # noqa + def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" return [ "network", diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index 12462ede..e0d602ed 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -2,7 +2,7 @@ from typing import List, Optional, Union -from pydantic import ConfigDict, Field, field_validator, ValidationInfo +from pydantic import ConfigDict, Field from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -100,21 +100,6 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): masquerade_protocol: str = Field(default="TCP") masquerade_port: str = Field(default="HTTP") - # TODO: this validator should not be needed anymore, test what happens if removed. - @field_validator( - "c2_server_ip_address", - "keep_alive_frequency", - "masquerade_protocol", - "masquerade_port", - mode="before", - ) - @classmethod - def not_none(cls, v: str, info: ValidationInfo) -> int: - """If None is passed, use the default value instead.""" - if v is None: - return cls.model_fields[info.field_name].default - return v - @classmethod def form_request(self, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index b58cdf29..05e3643a 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -132,7 +132,7 @@ class AbstractAgent(BaseModel, ABC): # then use a bespoke conversion to take 1-40 int back into CAOS action return ("do_nothing", {}) - def format_request(self, action: Tuple[str, Dict], options: Dict[str, int]) -> List[str]: + def format_request(self, action: Tuple[str, Dict], options: Dict[str, int]) -> RequestFormat: # this will take something like APPLICATION.EXECUTE and add things like target_ip_address in simulator. # therefore the execution definition needs to be a mapping from CAOS into SIMULATOR """Format action into format expected by the simulator, and apply execution definition if applicable.""" From 66daab3baf2a6765bb548c95513a7d6dd55bda32 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 21 Jan 2025 13:08:36 +0000 Subject: [PATCH 143/224] Backport 3.3.1 fixes into Core --- .azure/azure-ci-build-pipeline.yaml | 42 ++++--- .pre-commit-config.yaml | 2 +- docs/source/primaite-dependencies.rst | 82 ++++++------ pyproject.toml | 16 +-- .../_package_data/data_manipulation.yaml | 4 +- .../_package_data/data_manipulation_marl.yaml | 8 +- src/primaite/game/agent/agent_log.py | 2 +- .../agent/observations/acl_observation.py | 12 +- .../observations/file_system_observations.py | 7 +- .../observations/firewall_observation.py | 118 ++++++++++-------- .../agent/observations/host_observations.py | 44 ++++--- .../agent/observations/node_observations.py | 2 +- .../agent/observations/router_observation.py | 35 ++++-- src/primaite/game/game.py | 22 ++++ src/primaite/notebooks/Action-masking.ipynb | 9 ++ ...a-Manipulation-Customising-Red-Agent.ipynb | 9 ++ .../Data-Manipulation-E2E-Demonstration.ipynb | 9 ++ .../Getting-Information-Out-Of-PrimAITE.ipynb | 9 ++ ...ge-Escalation-and-Data-Loss-Example.ipynb} | 0 .../notebooks/Requests-and-Responses.ipynb | 9 ++ .../notebooks/Terminal-Processing.ipynb | 9 ++ .../Training-an-RLLIB-MARL-System.ipynb | 11 +- .../notebooks/Training-an-RLLib-Agent.ipynb | 9 ++ .../notebooks/Training-an-SB3-Agent.ipynb | 9 ++ .../notebooks/_package_data/uc2_attack.png | Bin 112286 -> 112279 bytes .../notebooks/_package_data/uc2_network.png | Bin 70887 -> 70945 bytes src/primaite/notebooks/multi-processing.ipynb | 9 ++ .../simulator/file_system/file_system.py | 10 ++ .../file_system/file_system_item_abc.py | 5 +- src/primaite/simulator/file_system/folder.py | 17 ++- .../system/services/ftp/ftp_client.py | 5 + .../system/services/ftp/ftp_service.py | 24 +++- tests/assets/configs/action_penalty.yaml | 4 +- .../assets/configs/bad_primaite_session.yaml | 4 +- .../configs/basic_switched_network.yaml | 4 +- tests/assets/configs/data_manipulation.yaml | 4 +- .../configs/eval_only_primaite_session.yaml | 4 +- tests/assets/configs/extended_config.yaml | 4 +- .../configs/firewall_actions_network.yaml | 4 +- .../configs/fixing_duration_one_item.yaml | 4 +- tests/assets/configs/multi_agent_session.yaml | 8 +- tests/assets/configs/shared_rewards.yaml | 4 +- .../configs/software_fixing_duration.yaml | 4 +- .../configs/test_application_install.yaml | 4 +- .../assets/configs/test_primaite_session.yaml | 4 +- .../actions/test_file_request_permission.py | 2 +- .../actions/test_folder_request_permission.py | 4 +- .../test_file_system_observations.py | 8 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_observations.py | 7 +- .../system/test_database_on_node.py | 4 +- .../_game/_agent/test_observations.py | 14 ++- .../_simulator/_file_system/test_file.py | 4 +- .../_file_system/test_file_actions.py | 4 +- .../_simulator/_file_system/test_folder.py | 18 +-- .../_file_system/test_folder_actions.py | 12 +- .../_network/_hardware/test_node_actions.py | 4 +- 57 files changed, 441 insertions(+), 247 deletions(-) rename src/primaite/notebooks/{Privilege-Escalation-and Data-Loss-Example.ipynb => Privilege-Escalation-and-Data-Loss-Example.ipynb} (100%) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 2375a391..624c9ca4 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -14,31 +14,36 @@ parameters: - name: matrix type: object default: - # - job_name: 'UbuntuPython38' - # py: '3.8' - # img: 'ubuntu-latest' - # every_time: false - # publish_coverage: false - - job_name: 'UbuntuPython311' - py: '3.11' + - job_name: 'UbuntuPython39' + py: '3.9' + img: 'ubuntu-latest' + every_time: false + publish_coverage: false + - job_name: 'UbuntuPython310' + py: '3.10' img: 'ubuntu-latest' every_time: true publish_coverage: true - # - job_name: 'WindowsPython38' - # py: '3.8' - # img: 'windows-latest' - # every_time: false - # publish_coverage: false + - job_name: 'UbuntuPython311' + py: '3.11' + img: 'ubuntu-latest' + every_time: false + publish_coverage: false + - job_name: 'WindowsPython39' + py: '3.9' + img: 'windows-latest' + every_time: false + publish_coverage: false - job_name: 'WindowsPython311' py: '3.11' img: 'windows-latest' every_time: false publish_coverage: false - # - job_name: 'MacOSPython38' - # py: '3.8' - # img: 'macOS-latest' - # every_time: false - # publish_coverage: false + - job_name: 'MacOSPython39' + py: '3.9' + img: 'macOS-latest' + every_time: false + publish_coverage: false - job_name: 'MacOSPython311' py: '3.11' img: 'macOS-latest' @@ -63,7 +68,7 @@ stages: displayName: 'Use Python ${{ item.py }}' - script: | - python -m pip install pre-commit + python -m pip install pre-commit>=6.1 pre-commit install pre-commit run --all-files displayName: 'Run pre-commits' @@ -71,7 +76,6 @@ stages: - script: | python -m pip install --upgrade pip==23.0.1 pip install wheel==0.38.4 --upgrade - pip install setuptools==66 --upgrade pip install build==0.10.0 pip install pytest-azurepipelines displayName: 'Install build dependencies' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df3bb504..d004dd6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - id: isort args: [ "--profile", "black" ] - repo: http://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: diff --git a/docs/source/primaite-dependencies.rst b/docs/source/primaite-dependencies.rst index 14a96349..ce2087ca 100644 --- a/docs/source/primaite-dependencies.rst +++ b/docs/source/primaite-dependencies.rst @@ -2,44 +2,44 @@ © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| Name | Version | License | Description | URL | -+===================+=========+====================================+=======================================================================================================+====================================================================+ -| gymnasium | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| ipywidgets | 8.1.5 | BSD License | Jupyter interactive widgets | http://jupyter.org | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| jupyterlab | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| kaleido | 0.2.1 | MIT | Static image export for web-based visualization libraries with zero dependencies | https://github.com/plotly/Kaleido | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| matplotlib | 3.7.1 | Python Software Foundation License | Python plotting package | https://matplotlib.org | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| networkx | 3.1 | BSD License | Python package for creating and manipulating graphs and networks | https://networkx.org/ | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| numpy | 1.23.5 | BSD License | NumPy is the fundamental package for array computing with Python. | https://www.numpy.org | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| platformdirs | 3.5.1 | MIT License | A small Python package for determining appropriate platform-specific dirs, e.g. a "user data dir". | https://github.com/platformdirs/platformdirs | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| plotly | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| polars | 0.20.30 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| prettytable | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| pydantic | 2.7.0 | MIT License | Data validation using Python type hints | https://github.com/pydantic/pydantic | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| PyYAML | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| ray | 2.32.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| stable-baselines3 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| tensorflow | 2.12.0 | Apache Software License | TensorFlow is an open source machine learning framework for everyone. | https://www.tensorflow.org/ | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| typer | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| Deepdiff | 8.0.1 | MIT License | Deep difference of dictionaries, iterables, strings, and any other object objects. | https://github.com/seperman/deepdiff | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ -| sb3_contrib | 2.1.0 | MIT License | Contrib package for Stable-Baselines3 - Experimental reinforcement learning (RL) code (Action Masking)| https://github.com/Stable-Baselines-Team/stable-baselines3-contrib | -+-------------------+---------+------------------------------------+-------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------+ ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| Name | Supported Version | Built Version | License | Description | URL | ++===================+=====================+===============+======================================+========================================================================================================+=====================================================================+ +| gymnasium | 0.28.1 | 0.28.1 | MIT License | A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym). | https://farama.org | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| ipywidgets | ~=8.0 | 8.1.5 | BSD License | Jupyter interactive widgets | http://jupyter.org | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| jupyterlab | 3.6.1 | 3.6.1 | BSD License | JupyterLab computational environment | https://jupyter.org | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| kaleido | ==0.2.1 | 0.2.1 | MIT | Static image export for web-based visualization libraries with zero dependencies | https://github.com/plotly/Kaleido | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| matplotlib | >=3.7.1 | 3.7.1 | Python Software Foundation License | Python plotting package | https://matplotlib.org | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| networkx | 3.1 | 3.1 | BSD License | Python package for creating and manipulating graphs and networks | https://networkx.org/ | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| numpy | ~1.23 | 1.23.5 | BSD License | NumPy is the fundamental package for array computing with Python. | https://www.numpy.org | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| platformdirs | 3.5.1 | 3.5.1 | MIT License | A small Python package for determining appropriate platform-specific dirs, e.g. a "user data dir". | https://github.com/platformdirs/platformdirs | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| plotly | 5.15 | 5.15.0 | MIT License | An open-source, interactive data visualization library for Python | https://plotly.com/python/ | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| polars | 0.20.30 | 0.20.30 | MIT License | Blazingly fast DataFrame library | https://www.pola.rs/ | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| prettytable | 3.8.0 | 3.8.0 | BSD License (BSD (3 clause)) | A simple Python library for easily displaying tabular data in a visually appealing ASCII table format | https://github.com/jazzband/prettytable | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| pydantic | 2.7.0 | 2.7.0 | MIT License | Data validation using Python type hints | https://github.com/pydantic/pydantic | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| PyYAML | >=6.0 | 6.0 | MIT License | YAML parser and emitter for Python | https://pyyaml.org/ | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| ray | >=2.20, <2.33 | 2.32.0 | Apache 2.0 | Ray provides a simple, universal API for building distributed applications. | https://github.com/ray-project/ray | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| stable-baselines3 | 2.1.0 | 2.1.0 | MIT | Pytorch version of Stable Baselines, implementations of reinforcement learning algorithms. | https://github.com/DLR-RM/stable-baselines3 | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| tensorflow | ~=2.12 | 2.12.0 | Apache Software License | TensorFlow is an open source machine learning framework for everyone. | https://www.tensorflow.org/ | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| typer | >=0.9 | 0.9.0 | MIT License | Typer, build great CLIs. Easy to code. Based on Python type hints. | https://github.com/tiangolo/typer | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| Deepdiff | 8.0.1 | 8.0.1 | MIT License | Deep difference of dictionaries, iterables, strings, and any other object objects. | https://github.com/seperman/deepdiff | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ +| sb3_contrib | 2.1.0 | 2.1.0 | MIT License | Contrib package for Stable-Baselines3 - Experimental reinforcement learning (RL) code (Action Masking) | https://github.com/Stable-Baselines-Team/stable-baselines3-contrib | ++-------------------+---------------------+---------------+--------------------------------------+--------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------+ diff --git a/pyproject.toml b/pyproject.toml index 354df8b2..e840797c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "primaite" description = "PrimAITE (Primary-level AI Training Environment) is a simulation environment for training AI under the ARCD programme." authors = [{name="Defence Science and Technology Laboratory UK", email="oss@dstl.gov.uk"}] license = {file = "LICENSE"} -requires-python = ">=3.9, <3.12" +requires-python = ">=3.9, <3.13" dynamic = ["version", "readme"] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -26,15 +26,15 @@ dependencies = [ "gymnasium==0.28.1", "jupyterlab==3.6.1", "kaleido==0.2.1", - "matplotlib==3.7.1", + "matplotlib>=3.7.1", "networkx==3.1", - "numpy==1.23.5", + "numpy~=1.23", "platformdirs==3.5.1", "plotly==5.15.0", "polars==0.20.30", "prettytable==3.8.0", - "PyYAML==6.0", - "typer[all]==0.9.0", + "PyYAML>=6.0", + "typer[all]>=0.9", "pydantic==2.7.0", "ipywidgets", "deepdiff" @@ -53,8 +53,8 @@ license-files = ["LICENSE"] [project.optional-dependencies] rl = [ "ray[rllib] >= 2.20.0, <2.33", - "tensorflow==2.12.0", - "stable-baselines3[extra]==2.1.0", + "tensorflow~=2.12", + "stable-baselines3==2.1.0", "sb3-contrib==2.1.0", ] dev = [ @@ -69,7 +69,7 @@ dev = [ "pytest-xdist==3.3.1", "pytest-cov==4.0.0", "pytest-flake8==1.1.1", - "setuptools==66", + "setuptools==75.6.0", "Sphinx==7.1.2", "sphinx-copybutton==0.5.2", "wheel==0.38.4", diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index fa10a463..b0d5d087 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -161,8 +161,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index b0131c8c..e45f193e 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -153,8 +153,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP @@ -668,8 +668,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 31d74176..36f8c707 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -93,7 +93,7 @@ class AgentLog: def _write_to_terminal(self, msg: str, level: str, to_terminal: bool = False): if to_terminal or SIM_OUTPUT.write_agent_log_to_terminal: - print(f"{self.agent_name}: ({ self.timestep}) ({level}) {msg}") + print(f"{self.agent_name}: ({self.timestep}) ({level}) {msg}") def debug(self, msg: str, to_terminal: bool = False): """ diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index 86a6463a..cb2cb38e 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -24,8 +24,8 @@ class ACLObservation(AbstractObservation, identifier="ACL"): """List of IP addresses.""" wildcard_list: Optional[List[str]] = None """List of wildcard strings.""" - port_list: Optional[List[int]] = None - """List of port numbers.""" + port_list: Optional[List[str]] = None + """List of port names.""" protocol_list: Optional[List[str]] = None """List of protocol names.""" num_rules: Optional[int] = None @@ -37,7 +37,7 @@ class ACLObservation(AbstractObservation, identifier="ACL"): num_rules: int, ip_list: List[IPv4Address], wildcard_list: List[str], - port_list: List[int], + port_list: List[str], protocol_list: List[str], ) -> None: """ @@ -51,8 +51,8 @@ class ACLObservation(AbstractObservation, identifier="ACL"): :type ip_list: List[IPv4Address] :param wildcard_list: List of wildcard strings. :type wildcard_list: List[str] - :param port_list: List of port numbers. - :type port_list: List[int] + :param port_list: List of port names. + :type port_list: List[str] :param protocol_list: List of protocol names. :type protocol_list: List[str] """ @@ -60,7 +60,7 @@ class ACLObservation(AbstractObservation, identifier="ACL"): self.num_rules: int = num_rules self.ip_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(ip_list)} self.wildcard_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(wildcard_list)} - self.port_to_id: Dict[int, int] = {p: i + 2 for i, p in enumerate(port_list)} + self.port_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(port_list)} self.protocol_to_id: Dict[str, int] = {p: i + 2 for i, p in enumerate(protocol_list)} self.default_observation: Dict = { i diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 50ca93fd..784eaa7f 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -190,6 +190,8 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"): if self.files: self.default_observation["FILES"] = {i + 1: f.default_observation for i, f in enumerate(self.files)} + self.cached_obs: Optional[ObsType] = self.default_observation + def observe(self, state: Dict) -> ObsType: """ Generate observation based on the current state of the simulation. @@ -204,7 +206,10 @@ class FolderObservation(AbstractObservation, identifier="FOLDER"): return self.default_observation if self.file_system_requires_scan: - health_status = folder_state["visible_status"] + if not folder_state["scanned_this_step"]: + health_status = self.cached_obs["health_status"] + else: + health_status = folder_state["visible_status"] else: health_status = folder_state["health_status"] diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index a89ddfc5..c63c6927 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -27,13 +27,13 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): """List of IP addresses for encoding ACLs.""" wildcard_list: Optional[List[str]] = None """List of IP wildcards for encoding ACLs.""" - port_list: Optional[List[int]] = None + port_list: Optional[List[str]] = None """List of ports for encoding ACLs.""" protocol_list: Optional[List[str]] = None """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" - include_users: Optional[bool] = True + include_users: Optional[bool] = None """If True, report user session information.""" def __init__( @@ -41,7 +41,7 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): where: WhereType, ip_list: List[str], wildcard_list: List[str], - port_list: List[int], + port_list: List[str], protocol_list: List[str], num_rules: int, include_users: bool, @@ -56,8 +56,8 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): :type ip_list: List[str] :param wildcard_list: List of wildcard rules. :type wildcard_list: List[str] - :param port_list: List of port numbers. - :type port_list: List[int] + :param port_list: List of port names. + :type port_list: List[str] :param protocol_list: List of protocol types. :type protocol_list: List[str] :param num_rules: Number of rules configured in the firewall. @@ -140,6 +140,8 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): }, }, } + if self.include_users: + self.default_observation["users"] = {"local_login": 0, "remote_sessions": 0} def observe(self, state: Dict) -> ObsType: """ @@ -153,29 +155,35 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): firewall_state = access_from_nested_dict(state, self.where) if firewall_state is NOT_PRESENT_IN_STATE: return self.default_observation - obs = { - "PORTS": {i + 1: p.observe(state) for i, p in enumerate(self.ports)}, - "ACL": { - "INTERNAL": { - "INBOUND": self.internal_inbound_acl.observe(state), - "OUTBOUND": self.internal_outbound_acl.observe(state), + + is_on = firewall_state["operating_state"] == 1 + if not is_on: + obs = {**self.default_observation} + + else: + obs = { + "PORTS": {i + 1: p.observe(state) for i, p in enumerate(self.ports)}, + "ACL": { + "INTERNAL": { + "INBOUND": self.internal_inbound_acl.observe(state), + "OUTBOUND": self.internal_outbound_acl.observe(state), + }, + "DMZ": { + "INBOUND": self.dmz_inbound_acl.observe(state), + "OUTBOUND": self.dmz_outbound_acl.observe(state), + }, + "EXTERNAL": { + "INBOUND": self.external_inbound_acl.observe(state), + "OUTBOUND": self.external_outbound_acl.observe(state), + }, }, - "DMZ": { - "INBOUND": self.dmz_inbound_acl.observe(state), - "OUTBOUND": self.dmz_outbound_acl.observe(state), - }, - "EXTERNAL": { - "INBOUND": self.external_inbound_acl.observe(state), - "OUTBOUND": self.external_outbound_acl.observe(state), - }, - }, - } - if self.include_users: - sess = firewall_state["services"]["UserSessionManager"] - obs["users"] = { - "local_login": 1 if sess["current_local_user"] else 0, - "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), } + if self.include_users: + sess = firewall_state["services"]["UserSessionManager"] + obs["users"] = { + "local_login": 1 if sess["current_local_user"] else 0, + "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), + } return obs @property @@ -186,34 +194,36 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): :return: Gymnasium space representing the observation space for firewall status. :rtype: spaces.Space """ - space = spaces.Dict( - { - "PORTS": spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)}), - "ACL": spaces.Dict( - { - "INTERNAL": spaces.Dict( - { - "INBOUND": self.internal_inbound_acl.space, - "OUTBOUND": self.internal_outbound_acl.space, - } - ), - "DMZ": spaces.Dict( - { - "INBOUND": self.dmz_inbound_acl.space, - "OUTBOUND": self.dmz_outbound_acl.space, - } - ), - "EXTERNAL": spaces.Dict( - { - "INBOUND": self.external_inbound_acl.space, - "OUTBOUND": self.external_outbound_acl.space, - } - ), - } - ), - } - ) - return space + shape = { + "PORTS": spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)}), + "ACL": spaces.Dict( + { + "INTERNAL": spaces.Dict( + { + "INBOUND": self.internal_inbound_acl.space, + "OUTBOUND": self.internal_outbound_acl.space, + } + ), + "DMZ": spaces.Dict( + { + "INBOUND": self.dmz_inbound_acl.space, + "OUTBOUND": self.dmz_outbound_acl.space, + } + ), + "EXTERNAL": spaces.Dict( + { + "INBOUND": self.external_inbound_acl.space, + "OUTBOUND": self.external_outbound_acl.space, + } + ), + } + ), + } + if self.include_users: + shape["users"] = spaces.Dict( + {"local_login": spaces.Discrete(2), "remote_sessions": spaces.Discrete(self.max_users + 1)} + ) + return spaces.Dict(shape) @classmethod def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> FirewallObservation: diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 03e9aca1..e46cc805 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -54,7 +54,7 @@ class HostObservation(AbstractObservation, identifier="HOST"): """ If True, files and folders must be scanned to update the health state. If False, true state is always shown. """ - include_users: Optional[bool] = True + include_users: Optional[bool] = None """If True, report user session information.""" def __init__( @@ -191,25 +191,31 @@ class HostObservation(AbstractObservation, identifier="HOST"): if node_state is NOT_PRESENT_IN_STATE: return self.default_observation - obs = {} + is_on = node_state["operating_state"] == 1 + if not is_on: + obs = {**self.default_observation} + + else: + obs = {} + if self.services: + obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)} + if self.applications: + obs["APPLICATIONS"] = {i + 1: app.observe(state) for i, app in enumerate(self.applications)} + if self.folders: + obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)} + if self.nics: + obs["NICS"] = {i + 1: nic.observe(state) for i, nic in enumerate(self.nics)} + if self.include_num_access: + obs["num_file_creations"] = node_state["file_system"]["num_file_creations"] + obs["num_file_deletions"] = node_state["file_system"]["num_file_deletions"] + if self.include_users: + sess = node_state["services"]["UserSessionManager"] + obs["users"] = { + "local_login": 1 if sess["current_local_user"] else 0, + "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), + } + obs["operating_status"] = node_state["operating_state"] - if self.services: - obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)} - if self.applications: - obs["APPLICATIONS"] = {i + 1: app.observe(state) for i, app in enumerate(self.applications)} - if self.folders: - obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)} - if self.nics: - obs["NICS"] = {i + 1: nic.observe(state) for i, nic in enumerate(self.nics)} - if self.include_num_access: - obs["num_file_creations"] = node_state["file_system"]["num_file_creations"] - obs["num_file_deletions"] = node_state["file_system"]["num_file_deletions"] - if self.include_users: - sess = node_state["services"]["UserSessionManager"] - obs["users"] = { - "local_login": 1 if sess["current_local_user"] else 0, - "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), - } return obs @property diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 03869367..0c5d11da 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -56,7 +56,7 @@ class NodesObservation(AbstractObservation, identifier="NODES"): """List of IP addresses for encoding ACLs.""" wildcard_list: Optional[List[str]] = None """List of IP wildcards for encoding ACLs.""" - port_list: Optional[List[int]] = None + port_list: Optional[List[str]] = None """List of ports for encoding ACLs.""" protocol_list: Optional[List[str]] = None """List of protocols for encoding ACLs.""" diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index ca455f4c..9687d083 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -33,13 +33,13 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): """List of IP addresses for encoding ACLs.""" wildcard_list: Optional[List[str]] = None """List of IP wildcards for encoding ACLs.""" - port_list: Optional[List[int]] = None + port_list: Optional[List[str]] = None """List of ports for encoding ACLs.""" protocol_list: Optional[List[str]] = None """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" - include_users: Optional[bool] = True + include_users: Optional[bool] = None """If True, report user session information.""" def __init__( @@ -84,6 +84,8 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): } if self.ports: self.default_observation["PORTS"] = {i + 1: p.default_observation for i, p in enumerate(self.ports)} + if self.include_users: + self.default_observation["users"] = {"local_login": 0, "remote_sessions": 0} def observe(self, state: Dict) -> ObsType: """ @@ -98,16 +100,21 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): if router_state is NOT_PRESENT_IN_STATE: return self.default_observation - obs = {} - obs["ACL"] = self.acl.observe(state) - if self.ports: - obs["PORTS"] = {i + 1: p.observe(state) for i, p in enumerate(self.ports)} - if self.include_users: - sess = router_state["services"]["UserSessionManager"] - obs["users"] = { - "local_login": 1 if sess["current_local_user"] else 0, - "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), - } + is_on = router_state["operating_state"] == 1 + if not is_on: + obs = {**self.default_observation} + + else: + obs = {} + obs["ACL"] = self.acl.observe(state) + if self.ports: + obs["PORTS"] = {i + 1: p.observe(state) for i, p in enumerate(self.ports)} + if self.include_users: + sess = router_state["services"]["UserSessionManager"] + obs["users"] = { + "local_login": 1 if sess["current_local_user"] else 0, + "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), + } return obs @property @@ -121,6 +128,10 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): shape = {"ACL": self.acl.space} if self.ports: shape["PORTS"] = spaces.Dict({i + 1: p.space for i, p in enumerate(self.ports)}) + if self.include_users: + shape["users"] = spaces.Dict( + {"local_login": spaces.Discrete(2), "remote_sessions": spaces.Discrete(self.max_users + 1)} + ) return spaces.Dict(shape) @classmethod diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8bc37597..f59117f4 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -258,6 +258,7 @@ class PrimaiteGame: net = sim.network simulation_config = cfg.get("simulation", {}) + defaults_config = cfg.get("defaults", {}) network_config = simulation_config.get("network", {}) airspace_cfg = network_config.get("airspace", {}) frequency_max_capacity_mbps_cfg = airspace_cfg.get("frequency_max_capacity_mbps", {}) @@ -338,6 +339,18 @@ class PrimaiteGame: _LOGGER.error(msg) raise ValueError(msg) + # TODO: handle simulation defaults more cleanly + if "node_start_up_duration" in defaults_config: + new_node.start_up_duration = defaults_config["node_startup_duration"] + if "node_shut_down_duration" in defaults_config: + new_node.shut_down_duration = defaults_config["node_shut_down_duration"] + if "node_scan_duration" in defaults_config: + new_node.node_scan_duration = defaults_config["node_scan_duration"] + if "folder_scan_duration" in defaults_config: + new_node.file_system._default_folder_scan_duration = defaults_config["folder_scan_duration"] + if "folder_restore_duration" in defaults_config: + new_node.file_system._default_folder_restore_duration = defaults_config["folder_restore_duration"] + if "users" in node_cfg and new_node.software_manager.software.get("UserManager"): user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa for user_cfg in node_cfg["users"]: @@ -384,6 +397,15 @@ class PrimaiteGame: msg = f"Configuration contains an invalid service type: {service_type}" _LOGGER.error(msg) raise ValueError(msg) + + # TODO: handle simulation defaults more cleanly + if "service_fix_duration" in defaults_config: + new_service.fixing_duration = defaults_config["service_fix_duration"] + if "service_restart_duration" in defaults_config: + new_service.restart_duration = defaults_config["service_restart_duration"] + if "service_install_duration" in defaults_config: + new_service.install_duration = defaults_config["service_install_duration"] + # service-dependent options if service_type == "DNSClient": if "options" in service_cfg: diff --git a/src/primaite/notebooks/Action-masking.ipynb b/src/primaite/notebooks/Action-masking.ipynb index 858b4bb6..7fde0a49 100644 --- a/src/primaite/notebooks/Action-masking.ipynb +++ b/src/primaite/notebooks/Action-masking.ipynb @@ -11,6 +11,15 @@ "PrimAITE environments support action masking. The action mask shows which of the agent's actions are applicable with the current environment state. For example, a node can only be turned on if it is currently turned off." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index d1154b54..756fc44f 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -15,6 +15,15 @@ "*(For a full explanation of the Data Manipulation scenario, check out the data manipulation scenario notebook)*" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 143bbe09..dbc6f0c1 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -371,6 +371,15 @@ "First, load the required modules" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb index a832f3cc..f8691d7d 100644 --- a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb +++ b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb @@ -9,6 +9,15 @@ "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb similarity index 100% rename from src/primaite/notebooks/Privilege-Escalation-and Data-Loss-Example.ipynb rename to src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb diff --git a/src/primaite/notebooks/Requests-and-Responses.ipynb b/src/primaite/notebooks/Requests-and-Responses.ipynb index da614c93..83aed07c 100644 --- a/src/primaite/notebooks/Requests-and-Responses.ipynb +++ b/src/primaite/notebooks/Requests-and-Responses.ipynb @@ -25,6 +25,15 @@ "Let's set up a minimal network simulation and send some requests to see how it works." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb index fdf405a7..9aa4e96a 100644 --- a/src/primaite/notebooks/Terminal-Processing.ipynb +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -18,6 +18,15 @@ "The Terminal service comes pre-installed on most Nodes (The exception being Switches, as these are currently dumb). " ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index 19e95a95..76cab86a 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -18,6 +18,15 @@ "#### First, Import packages and read our config file." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, @@ -32,8 +41,6 @@ "from ray.rllib.algorithms.ppo import PPOConfig\n", "from primaite.session.ray_envs import PrimaiteRayMARLEnv\n", "\n", - "# If you get an error saying this config file doesn't exist, you may need to run `primaite setup` in your command line\n", - "# to copy the files to your user data path.\n", "with open(PRIMAITE_PATHS.user_config_path / 'example_config/data_manipulation_marl.yaml', 'r') as f:\n", " cfg = yaml.safe_load(f)\n", "\n", diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index 0fd212f2..7252b046 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -11,6 +11,15 @@ "This notebook will demonstrate how to use PrimaiteRayEnv to train a basic PPO agent." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index 5255b0ad..2b554475 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -18,6 +18,15 @@ "#### First, we import the inital packages and read in our configuration file." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/src/primaite/notebooks/_package_data/uc2_attack.png b/src/primaite/notebooks/_package_data/uc2_attack.png index 8b8df5ce8ddf74ad717c734cd52945164580f0f7..03797d0046e86536f9e7940262303c9db9c7cea6 100644 GIT binary patch delta 55804 zcmY&=bwE_z^Zwe30yc_-9Uve|w=F5sjTm%DEDaYEl~NRtT$IkGJ5-ca5SCh&MkJP6 zklF8}G^FS-%f%}tg3q@W|MUOx zo0bQ@-QqqYl`PAkO-yWfD&?40(M}lmRywuRP|*U^rO@ z&i&t0S`PS1IB6LcBkze$MVsbJ7}b)ND=W=qL7|EKZBU$2mn@4ppMA6D( zY1J}Cyl%-cgH&Vn`ZJM5lb(}ZCi5X;clW$z)4_{6?AsKMg}GdnKVjTV{Cfw5gs=tV z&rHMZ0;tT9cpfjO?34X&;@V#@n62kNo2mXt{#9+$=PP$E-(ZY*A06GvwUK#>-qUXYsK8JD~chqTdg&lN}d1OnWYxnNcQw zTp(t)&Zx!7q)oFScs!l=_rU`U%ie2>M-OktDEPr_tWUqGJuwhFwoEha*O%Svo}2hn zl{-x$9Y!~h-3ucZoHL~|ukj*z%`gc;CJ06O&V2WKT|?O&=xr?vq^`PYMF_@{itO$w zgmjl-ZLn0Q$THVbl~Xt4wCtw@xzEZBIU?&}$v3hMixt&fY8i%D)k#(0WZGb5};@ zTJebQQXZ*BffYfgKcdQ$^k1VhSr|WAT#*sGgP)U-H(3joOcFDKXL>rf0^v|9{pUBV9xXeX|80V$GiH^PH)>?I}V9am_h3$n-Bt~6-{!NWR4@5)5AeqC|x1q!S?DR0)9`Rio z<_IA_eexvL0k<*3-%?`k#|7+yw(ht?hE7IaUd!;Z8`&GlG@0pzeNWwk!C1e1pm&TG zGANzJvwr+%#-9G2Y#aQTnp!R@ofG1m-Fs^LPkdh%+RH^ao_5Q7=@mLA|hYL2i95MR@9x>Y}vO8Ylgw3K3o5jyWleplGi7F%{q@?FTHfS0vb5*Czg)1q%5Ib`?U-jI zJx3dz?Q7SS+JK2qJHpleoO-6)VqvHH7E_!={KeP0;3hE&7c_3&N*i`yss==ud-gi? z^xl0dHsZQz%O-umeO-^t_IUb@Bd(oVK>Hv+B`gZFZij)lm+->~Va4m&uRdfUaxX+9xlMEAsQ zPkNrGyNR*mRitG)Ur zX!0UYulE%F^UBj&A}hBo3%gB)X*w5jMrvH_K>Mlp5!Z~QwfvFAiy3_k1%k3t;DpJ$ zYISr4%Tu>!@yMZbCm*O(`ia7b2j}BZ;9>hxfalO>LMM*zE{zLzImRz(Z578HO~4-N zH6dqM#577}I3icQ+x0(=+He3?jz;c5=R|(M#;U;4W z7vHBH)w5M8cy_{cnAV^UGb3uY(pyUl?unJsaA_MqgU-^{S77OxJeLv*eFz-MxAm3- z$2`ZDQb=dY5=fW9bT7ur=uhQZ>s9+kh%Y1QX0Ehxvng+Id+6-x$zF-SSg|;3!x<*k zzDVxb$HBCW!rxWjp+FT4Vln_K6!Y=%X&dMyuk?-xZC$2~2^f|zXh(z1-oJXaGlZd- zZDG0C)xWejU}na`$|_uIWg*{MGqZ?uAc|%S2?$)^CXgeK?AsT%ysYr><;!41F%+U{ zQns-agDa!h!=XDa-YD6n&w5WZ!5#{ zFH9Y@80(kDVA}SgXeKKDVO%fZv zbi(2%*HanN(i$#Wh^KGd*yMxjFr3Zj#d7QUB!#}IN>34=qBMhuL5_2SQ#=U;!o}ix z2M6E2KGv2rha}ZAy{d_Wmr%5sW zH#=2v!u17q^1=Bb`SPR{U5?1UK?zPb`hlRT1#_gj`e$Z(cKaLR53If}!}Uo+a>@ns zMek`1Ue4uJPO$!ln*`;sa+{FuiDjB%Q>2AhR=Z-gtgC|bF>S$UGo{UO=5X9w}HIt7QuQHX04a3IAtAow*(3xPhR*aU8 zcbk zaa1s6Rjx|^j)1cVWbt-BB@StYA>{h!CcpKjaf}GH6mvA!mfD+IwUd%^{ewNO6!);V;Y-+_CH&dhvTX33F8xDe z$x3#tX5UDUMZ^TcZXdwJUo~VjpRyNX-?r_R%luazcD&Aq1RmaETH1(J=ASSKeI{BXCot?RU2cz|i!6$VJk^3Hxi1M99hDXT|P zrkojUROFaq7k${*r%IMA;k@du_+riGmq?bu%(F+29~pJA@qm+`35+M%821|?KJgz1 z5B>}`jha^+9iBr}Lce_zGl(p_5~{NA82mM_vzxrS|5LD?{&L)ct(WXVQVzaZ|*4-HHT=luRkH0$NQ&=F*1iY{n;b# zQ6`pcNS$FVTUFlSb|laB7Du`Rt}l6Tb%E+8ifySZ^hCHO-VDwi6FXU^=zAF5k!@eE zr3kT^M$403eks8N-MRY#+xl&Q(`)+Ppu^}_BY*VsY)Xy28Miey}SW0Ebok{OX+W&hA z)>&7cTZjbt2vSyQ|Ix1s{88g1six<9Z{cmY{Wsf6fI_?e*Ut*L$MmOlM)$*pcmXLf~bVOi{4%=m=kwjodbRRh~^r5}brH~}}I zdPBc++n@!~_?f2FnU3^tOwb5Nr`00TjoO0fS9;YR3ajsl6`j_*Bk|dMuIpuD5nx}@ zO- zhb^Be`C5CW+*^19#?lDgL_*VVHqR&Iv0yeTfR`BCo-N+ztxPuZNg~i8yFP~$Py54* zPa>w%%w@ms{#IK5AeeV#x(LO#cZ7B?PF2z{2zNcWq{yWGHfg*t+0NnA6wJ+7NNdU{ zk}rN*=Bx@J){$JLD3>y){FIlRQ|%3{fW!Sh8vJqi#ZUb+ARW!{=h!JM+}u^^g;!Ei zHqP#jD_16;WVc6}W+ZSbhhy2^xigGp9Ox!Kd1h}|*nMo4S7gOb8pM8(=Ce|eXN8?l z-?kBjPI!mvjF!CfVd3Ei36pP{lp)_rbt6hds2vT3+4z$UCaFDq2chkPZ}3DP$?ik} ztd?C}XSyt-s%816_!xwTYe|7x%&KkUT}ep3Ftei9YoW2+Muw-pcR`y(LY;A%^Nq&k zLMIAfyx|p}=vn%t<`joVt(RZNJ4--Tc-Wwt{c@G7YjGbwe9)-xUyEk*@#-iDhiz0g zUU!_8=Ur+rxAew&hF@sQ`7DD9qLyayl&>E_QbGHsW$%j+pj~#U2=(l6|{M!`XJ+vle$CL#pCg>qZ_mbjT(t zo(~QwUG>oe)bgZn#)L9tAJp#9kyyx^HCJ{`)Pr|$(Le_Bwze<}^%e|D{7f)TyA9TN zmNmGXWf%>Xxh)`zU7IV804t}k=huPpP_<$u7xLz24odrpslI|P{wJM}O@isG3lG;v zFjl<1vS*y@f@&u2uQgiqGps-D=H&b}qxv#`Yjjqv!wpCs<{9kE%i)l`CllY@|1q3I zu@F!H$%*+$V!!?pI6I7pbmIUDbCeE=->Tm)lsdBwIeRRd+nUQlvr#8Pr~tKlUmk#Z zE%>Yxy|GU;wTLkkD=R{6zFc2O$!KmhrG+jli|^ZZ=t&61kcFtOPSkPTU~ zrDq_wrUdExe4{Rt&QX@U7n`Lb>g5fJZ(YXoB*w=-vx(ZrwKwtBnPaLrGS1#>Y`Iuh z8~KwAf3v%NE!0uNm9`x8GN*I?S4>L9W_dIL{e0EoUHVt4uGi(Qi{%$So44XPeu(-6 zQx9Z24tYnGE06(cmG{^Wr;}=X!2Mmj+p2+@k_y3DPUlv$aKr=-+&@85(#bYx%1S^t zJJi(1iv6kN=9V6AqOXr#RS4pk0;wB7zA`O)26iQ5A7K3x_=b#TVSKeDX+jR59iP|g zwFU(HIr~%FV5M806|V)2GW;tkGD^zoQczcpr{@&6&>7*80$Kt# ziZQvzq?%*Kh0D!m9t^1++$wF^x2~W3Yx^7rA9$Ljs`(oTBr)VKR`&eMl8xFLLFAqB zS;tYAH67=e9M0P^=GOs0sII$`Jl&G_{{4|;@hidF)yJI!z8%qx&6Xn-Dq9SCx>;yS z6zRie%(&P-_`&toI%%Ct(~FxAI{Q5EzC1`g1gym1PwIvX7zJvt-1y1XBH6=$j~PZ5Lm;hQQS&qMOl-+ITK%44dTo+A~qqxacgD31Jocbr>lKGv$ds&U?R<>-4&7lI#; zG+(-15Y3I=w+pi0_V?3l<&_nZ+N}9D-C`O?j!fPBy*fUzfmJS!jVg5Bj)S@Qrd|$! zvr;KlFyc0vxiwP?Y*Qa+&gCk6efqdvvMc8wAX;SB}V|isOFe`V_oR z6z!Wz1O;}qc~9{i-9YGv=_uWK77|7@i5e@J3N8gilq`F`bufl)@7^*WHZM;Bp3|pM z9LCcxAO1F+v{?8~%>F|?<4U=JVX+P51;w9ownjG$yTe$i@Ypu9RITO*sektTZiJKb z10*~pE9=VLt&(Rp*P4t-+#lMCU-$m?v1}3;ijtJ^Lj8=2BOyl*JSI2@!xuT7E)|Ja zaK7OQxmMM24}o|yUw%w7R-`ICIBRHY8lv!GqOZ`T)(96vlsI-^bG&4FdbW<^Oh|3f zs*0{x>r9j35nRWyJ{8)D8@5f^`M;z#8^0l8nJ1h*Z;e(dGER2qmJC1u);K8A5 z18Wz$mz-dekoIaDM)b8rJRXH7^CyU~`h%+>F3TGj-Pd~)5nrD@GHZP<-t5O?+1w<3 z4^MX-&AUrr3;jX*IO{fjVU=g~j&9Y=T;MgW3CQ+ zmdO`_8@b4i`JmMMx_w+3wzS|PAp z*DSUw)*+BhQ!TiI5ltlxAW8(O^OIS;+?TcL^K=f7;?DDUq3qnG#>0Be?S%AkYfXvk zp(8)y?k}r-a%5|@_~Eo|$~_+pV!xngjUMhhy+6U3$cpV0f1U108MMiNAS1LjFlQC66u@$`5^`$fKiCY@;HU&qbbV&Xd_#ykuG-qiG zjHMFfm*XT4XPIX?u0vC8^9S=q+(&N+Ao}6MGn3S}hx`x8OZ)DkbR2D&k$ok5{A6id z&#wa~%SXm9vgWZ3m{rIFDO1Y$d|COsg6>Ycvw|C;T?%FaTL{r7uDoXz24X zjeK++sG&hI??H(zZlsWk?ZcWQRz(_CG}gcRY9!F$RdFXPOJ6MWiP$6pUtzA@8FM~R5ay1ea_c)$D5!MSIf$V@-sU0 zbaq|~1PwN#eLvh8>gzC32~Pw1elHzKkX0DzDf$%4T`G-TZAcTf_iB0txsnKkqGTjp zyl=$0J9bcZGk1@Ewb>p!!F3|i=r$Qo^a`t_aW_QD)(>*<)BLi_`=f)4zxU7CShnZF z{^0-qGyd?PSg>h<#Ix_m$AFXCtiQo+zzS+#-Z3)z*?X9}x4LgZ!HYl(Q0;{pQvH6O zDMwk(O8|TS_>+he><1^2-9tILHF?ns9W^F5fv{_Cn5R>~A>Fq=Na1?$V#+<2?5I|T zRG7qyU+2#cw;U`Z70Tr<#=4Q;6+xOig=H(W!r8N1c!UD#w3>1@Cpv9=CTY!qeLr?3 z1hRGGu4&0XzU@k;K7QK2jOFb!k%kni7fZntzk{a` zbWYFAY=b;DP}I}0QPYHMIrQ8n2+hw*WZb}2RjXEYO)SQm1_bvHyR*FZBkloDQ8mOJV3?Wdm>X6utKZ{e*N%nE&) zxiU$c9UTp@aZgMh{+wC_d4fxpw*J?Dst2TR6Pniy7ydrXxBeQ|HyLUgV zvRJ@IN}VZAtjr5t^{I63o(qF2AtdX?+7*ed10TAcz2lNxvm3ifd9`hD+STecMY%4h zuUkZ_SjT1UP){uIH!8>+ln-nSG9VSJ?E%_OixALVZpUlM@x0JQX5dOFk!ZUjnfApb zwywYZQEZH_JghCEll@W8-1ua0_j?6|UH-?GucK&igYKt&=G2;f$p zNGs1hG7u+C`ZL!nIE%o2uXfm?bO?c67Lv@NmZ^_R^dwoM-lZ13USQfBmmpa>V7fx~ z2ft9vyHZ#^dNhQ0s}zp=kXgP}=dVr4OiEEenYyw`bP7;Xw^=Gs77jsaI1|EKVTMb+ zKj4cJfi!kZ!;4A*71ZsUy1br|R(D#RZKy(E#RbiT2>XvV^8S$a6@Cr88-xW*mN$|* zoEhxgLiP?TMg{~v43=cnt-lV_q_Av1%)RZV&j(KrbMx==*h8-AksUN;rk}H8FSUM^rnHO(OAFfBkaf8V zb1BZx2Ng%w##@#&@MPX)TF#8~lZFv8eSPC6KFVPYNZ^wRB%b3osc&)t_s~jjT3S9I zTfLz?M6@9+$?6+RRP{L|>_*R`tw;>zLo(^ju? zC`uWx`>OfsC+?J5+eF92OCaD^tXq5rW!L2J>DglGw-r-}yBDzSlr{vsm4Y^)q^NUi z*`8TEI-2O-UF$Vjx&2>5Yvhu7A_!@ENWTx=l4kp!B z$*JFFc>_d}9^PgrL@234cd6GWD#H02AsN>a^Bhmy##6I#$%`i1CG@q}w%6Ws&oKWe z2GgJq=D6U-ETWbsv)@38TjE_4=Y_CjYePw-Ux&gxC81-N7694>RbF+~GQ^>R$Q~9$ z$p^+jj`Akl(@>#yJ4Oes%d$9}WRr$Xq#Wr>~)tghe7=IqvT0~8H`{TY&m#gSc8 zWf|*7-1obI{VdlMe6Q`F+a+fBvC7CP`=}YEM`IX(yb0076e@MC5u7$_;rm|AK$(1* zGB*7%>WQNIg`rz4@Zc;Fl{idQabUAze8=FX_urX3N5J+I3((Tw8h z()N93`D5AA4&>)H{+kQlD7x$2Hcc@sLhQ*GoK2jpJi2zGd=Cf?nr~keG3DZKkX)8v zXlgEumRSOrK;cJ7GSO9kE~;XGtYJ|5NU7b|yTQr+e~{Z3@)i~qJ3Q*H$U=k=AhCSD zziyK5qxv4ZZe-)#oC}j%JA65tbI27TTyUQ>b?w-c?t)+4zSj+e1T7j47p9!APpP5x zyP560gxl}-E?$^F^viY`u)gdC%89$uDDxEO4Wc0q3@86hCX(lDzw779rcxAt96Kwp z>Et~pdATm@WOPDGf1{B<>#HZc|n+f9G!D$`Z&UO7!_RH4^Z>35wq|6H7i-- zBBgR4f2VD4nYzdq3+mK*FL-bP!vX~aD&Gc$o>}PADL0J(WI$yXC{qF8d0~+^Wz+Eo zc zoXML+A|KI-1GNzq>Un6A301oi z?3n$ItsL4A=)^~XA93*WGKHo4SXli z`(u#%WzWg1CpFyDA29?S36R_%_eA9|rK=`C6)u{6{A>pGv9gMk{}uC#ao-`uCQERI zS9_TTnMOBiccS*(diNmgeJEJCXXR8lg)!-9eu(Cei3|dA2!GY>;$?<`q})_ve~`!a zwDs7Fe@d1|B~IVCY4PKnHhVsEmz~gkpV97`y49n^VfIF}SoftSVrLxRQx|GW=b#>! zp>)@(*wVOE174Y;tlABkI;Vp=#3ixVUCc?%3kx9TIXG8-N*Nj7VQq$=Tu~W!7e2Tr#E<) z>#WSZ1rbdAC)7flTW(3Me)V5xGtbBDJ ze<%4qf*MTPQlSHsl?!ciC!|0jF?OB_fxA+I zlID44iR!&y*GFQWI^bE&4)lt5;g1Kl10ATysm5-vx6ddCxpHcFXDX)v&dSaX64V z#On~8hO!%y@-O>WWvqVi*g18M%`I!)gfgH~9_FA(1X4|{?g%r@7=&dP_odPA$+;ob z-IB=lU5EBi=VS@;vK7A8H8ajor2a0|Cm~GDX&eVr1qH^M?C$4vC|zrl!a0_+H^@q4Iip8+*W#7iqw zvIMokkfb+M#-g-^+nnWTY$dTPrgsxYHSDhF+m_8+XT%C%BTV@2JoSgTYhCZ>cL*jG zq(}4LpAidyqDAZ<=lvB!P3dlW3afyYuE&Og68V=sDjU#>bvkB@zhihY?n^7<4M+$J zMjZ4#XNJitrn^!8f;PSOWt~r?bf?0|>+$*v zCaf4t##Jv%N1w&t^p|P(MXAX@M|1BKBM~^~MoAs^hA%T0UZOy|Wp&W!nFd77J6*1H zdPL>>EkF-n)-`&VclN55Za&xc9+)*hkh3ZLX~sA*w&8UtyB}X!oupu zu|4YM(R>PCPFfxH<0;Z^(88KX^FRqw8eGjS7)+sU!FZ@tT zpQ(*@STeGq{7fjOTyx`HC*~GbD>FkcTpX{EOeTLAoFhwOmInwpk?8$7mPUVqT;E+Nv-9XUG0i@SC=V!HC=a;ixKl#&?0z-=OGV~lUEM0b$)2ot^4!?fal(|*chk52glAjL&P2Q1-(=$mD*wRxm);z70^Ws zsLYg>m$^@lsn+mU@0$G&#j?Dhdag=ZuXNmJ@c3>7DZ@XJ*u-8_V@*!`vLre4HQW0H z+vtQNVtP@U^7|)bPbSR$rodPz)5B2G$r~9;^NhIr-~sp%h;?2@1b7(Z^msHEBhpm> z6?;Eh7u#*oHN;}-a#K-Ak<$I;?5b57`;DTm&Te>t{#XM-z~e=@Gn|>8WaB3 zR@>_hy7-s6AeN?{sLzv`G6kvl^;U~RHVf@YotrAztxBYQ>mVwDLnUmUOe9L0IN!0D zE;szbx%=zb1#=H?wX;B!U$CUC6t99_%ii!Tlu&HG=bgR(BW|$8etwQxD#(!b@9%S; z6G%`BKSmIXY#GBON(1ADYV<;XP<6;i*>^nF>%sC}9`YHe&uEU?!g3%g{d=xnD-x!n zFIDWE22#ZBbEeMaBaNrxpZPyMNc0C1GYq&*LN5BUpTjJ*BhFMOaez^Ob|JAjA z&wMK{sF+lqV~TZSW^+~b} zkj)iFT?Tr;epy29)MSV$Jl;W47Z*bAi$xUy-E_66WOz#Lt5=g8D{|>-V!?FECTCR! z9`ui6l29BRl-zs>s9ei6{(wNrGg-e8@6eHivHZe&`r$L~w(u}xH)T`6Ruumf=vdzV zDa20+wwdFqR}1g*TEM_CZ`L5xRi$#CI0Hr22vIi^qz1iLoH&4q0}P^r~_{DkxB9s8ZBTA-zBm<{uSaot<0cRI8jRrGFn_ z)}I>>FFq{_MHki2yk@DUG$^`w(RR-gIG0wMs4n|?J88syJnd~&?=r%=7P~M?6c!2! z61EdEiLxpPqnX2^TKATPm0>G?SZJ%7{_WoqG1;(8YqyZ>HWFmc)~_m!_9|P%lDwPbsPtb zAED#ZcoeY}tDIl4ji|Q+tyqy?sLEYu+e&eI=P8x@)!A1e793zO=KKRHBYzhsTp>5)pkg^Qvg!SHrCrW zPU_n~)__`(9=U(TW`Y23Rcq7InfG=3J@AT1p{I+HCWEsQ1lDKXjZE9j+`K6_MU!Gc z*VPUJ;p-c~FO@~F%!%=G}@0e9kbZ$K8Vfv#i%L`V4oK%t5#xOGaMT7|UfA6szVS zsVF%&>pH!#%zO^JQrssMjr=BbVt+ny&>M99PaxEUL1y(aTz$8cGt7d_2_D3RfR;G7q|jOq0O5Ne@ot4V(D4X!3#n>F)n^WpC?o%w;7pg$!h zAcYYWOL=d;mDu`_%K;Z~*04eWEhyx#_k_JVBFA_Ftt#L4UddAiJ5t=RZ1uc#n_J76 z>$`_7khchU*u|lrThxC=Q?jKsFPX+(AdjHF+?4mDc!eb&AX;WYTeo-IlhFVlfmnV8 z$!HJ_qgvC~(d43prY2dgzV+zA+f0f?{aO^;eRe%C*Vqp!{QRe0Cks~j`qGY4R zw6cD*yhDrw6|1OE`*27pQSLp;Ni0ZCQ|AEoyi?9;(uZkpu}~3@6T&y(GUR2}5(%O& zr<+xQ|I$t%{{rC_?ZDX`Y%Cg}1DHUHzw3JK$nFGgJ<6ruz1U3Zld|H zHOL@r*it;wGfb~~BYm?e{u_TLk5fMVmoA=!zGE5%0r`+{^pZ$NBB|Pv{z(K-S9YsMxdw z2b=w*8sD{2CH;EclFBLQ4~4EMQT9(DLQl(^CF{7d?vfHZBhc#RU#$xotYO>Uo8sKF}Z&FAG=zIKt1z;AT%DK9bC|4@Db0V3pJvy@@al>6RiWh zzUqts!P$eK@u{h8pki~GDt&J+?{H9No~+{^Cs%V?fd4{~jg%W>buR}q6|~DPi~hVW zKowkzt^r{aIh7PQ$T?)YTDaR3szHoGo8LUm*g8A4hv4osw^ZW|v3a=%CL;L-K=nm39wT-Gh(dQF%N%gG(pe(b`Bi0H(V= zwKI=f(TrDP0ipSTGDgFN5U}tyTP9)&Bq2%AAY!-*I$^lR^+Tp2JSwtFwb&Vqet9>q zC66pa=((jDD8;d{FYz8`*$A5sbjGm3vKo)jVbFv+J!8H{TefUEQ+2(VbCViCR2iKn zryaB#DVMKL*_%eR2htpi1Io@FelI`}MJ&I)VTV8Q&aU){TLW-{$_@IRe_O0h66U|| z=g~PBcrXxD~aVGbl**m_kmL#CVTXbXLwEKn%e`@C)^^tP5mkLJQuR0#Ihz0!JYchTx) zU6aUr3p;HD#N8sg>nAiscdSXPtEU#s>vwo5+JU2--5;t+Ct}6OuQL% zbD}0CVgQc>p724ez5LlFgfD?l5B{)Gijirc+lC4U{t;beLZZC7g2VOtc2XUrpZ(2b z@yTjsWITf%yf-v4Bzf%-n}<|5r!{ez){ZmKs$mcgV$=LGDE#3xVU07(^+YBGh> zpXm$1VggJ)bgB+T>u>$dEx#i+b1FEzcg$)U$h3reU7^mF3C)HIe;bA|0yhq4goE7r zj6~aVQIII>vp;uKnBFuI--@{LGJ@ zgw_jEjg0o7wQVXVXq$ijv-bad{zVfj;I)ubJNbJ6nTEGMgpj#)*x8{^=}PvrK)Lu1Xrpxp^61B2JYZKH4*W1XAOnjfS%S`y%`d zBGY<|UsfFcJZ={MOt9qR2yGVHyY2d4XRNI*Lz5#%x7$!UG$A8Y?6K_1T4)!BF4bNC zd5fRT*C)gbWH)Z?fJ}_+AZVRJqe5n;zJn0FZ=zm^US1iT4wAMu9=j8NY~s(o+gxFYHWp4}Aps8}0$O*W9d0-501|L>H7^*)P8vsC?tJLs6P)mgL-wOI(K zXe(QI@GgvBav)mX`sZr;-1LFnmDjgGt=x6k(5ZjZrl-rl6RioGmd|2Go7_0h4=EMe_m=fd;}elYt!Od3TZ&d` z1fNEmUL~sphaykV5LwRtJ){*tG9_Bwe zZ>_F4hbbv36<{Y^@$b)#xtyDPowmpF6v$~X|6O{6_i_29irSm{SF*&Fx&=U|BVr|3}( zaXx16)nd*Td!Dv@GI=ET*`;%!zWBJ^h+F;tWp$&7vz z=Oj|iIi&P_8qU1oof7&z%s*lF&sUosZQ2x@7Jc0$!|d|ru?+lnm(lX+g@uQgKx1jX z(6sX~^+VTqONS+P^7kQQSF6BXAIHfl5&J7=bd`B0;IIkNwQOnhC=60P^!?|fZCL-i z{Lqe9bRuT^91-ZPW*vI;{DJIw>?0PIYEpNGoMqE1Juq#~cOANaP9cDAirD<)kp7Gj zzbW8Fm0pNSY&=MLA#Tfd|)k^qwCsw%Q!Zd1`eP;Nb@kJgx}+_iK;~@1Imb z&m8dG)73bB1Y$O^^ds^|&HL}z@tcl8U;WpjDTprWTd0>3(5{95Tn2C`Mv&g5$CH|< z^xyfP_qw((fddNqQH8IfdHQsJ6VqV_Vfw$9$T754J}1ACjHNz<;~LsN`$Pohdv3LS zq84|S?zy^Y7b2G7588)+uPAu?pdRDcYZJemrJ7cDlz=@}xIK!)sIDOYxE-}?V^G8l#1 zaCFJhvY38-BHdTX(X2O)y*L1R(hEUJBY9ageBpLF@+F-`p?2WnsW&FLy; zbyrw5fwFc+d`Ge8w|~b1rK)w}YR}SVwX4OwBo(P=V)xt$s1Q#hX{&VYm7Bp5tq%8&=?E<#i1r zUH;D_Vj7wvb=WZ&gFFkIrwTZlf6f~D^C!^%*0cll`vHl4aUblBrT+UNe5TT@vm0~v z!E{Og*zyEwLjM`KDGk9oX?E*f`;wT!?2-{(CyR8>WN<0KqEgocJ@@@Q0g zE)7Q%-ml+(SDK%{e*6fm=V8CC{>VCo4WkFi#sEvK)bAjhHo-X&T>p&Wx1~NpNZhw- zZFwTvN5Zx~Tp(F;4;rE@O~5%T$d1AH#Cv}mE{wKoA2~wN`}d3J_nSV0LB-dvU-J-jwY8H%UWJwd-X-fF?QH zcpsM$1HU04AfV8P;Y#huy&US&AWBYkrZm0`49uKy1fPgdT!WsmaT^Q{hDAhVl$7Xs z(WkVntkN7y@e4Gust+HI4pS%-b3}P^QiMtRyLSiR(3mV8LI(TOr%#vH7}F3>j|}EP z@jo45u8H~vW~8UPEdDk_Us2k1vgxM$YU0`|gC8D?+FeYaa1SQlB6JT84IQ|vqM{D_90Yv4|DTm4NXl|GR%<`ZL(A8=1sb1KOB@` zQ0kNf25DVjA}J}!De(Y_uOFr{l-1O9bacXr3w1;y8DY{9erf6M35n|=JXxLj7K|Nz zS}@GO=ciF3R-ptRXt{Igwkx(;S)751p7X5u_3KVja&j3rE@5eoGVyQ=PiiygW+gSX z;Hhe+@9^-jN6#rqNqu-U((i#9AD(GgC|a2>ZE(rc8-XuvVdv3e(5P70Aknpvk&$T@ zIG=WxPNF<>Qdf^(Ufy(OW~R2NobVAxdF&%MUB$>I?@X2AH&kw}No)-vJl}OpMK>JkOVz*c}oL>4(zue!Fn!hXswpOX52ynSa> zQ)$~SSjUQvg$~XDA|fClQUlDWC<>xdr6bY>M0(vQBUmU3LZqWK>C&a6C{a3uUeySp zBfSL3xu00Z`Of$Atdq6gd?)P<7knKP4;5S_d5R?;VelY}^j;X`CbiOo7 zGVJ{FAtc{*)-+ni^$N^qYj?g?DC`Q@zp9CU+7cXgM+R`Wa+g|%@4Ilx8QdN9|S>JglW=9xu~e9a$z{E|G?{~ z{F$%bIP}6Klb!~Ssj8`c5wR(mnYo4c?n0w*n$TvKcB0`m>FdWR1aVk*ldD;6=a_6( zrl{+-#0azN*Ue{#>ZP2SN^w93ls6@ngkfnX^{AHhzK=DSkI(oRllq;OgocA%~TZ2#3R>Xrya>T5!34 z|9%o#SxxPUh^T0QareexCs;GtjqXlX?di>V7DRtiAk0Sm*&q#!5Xdg(^3{-79_ul4 z-(VJ-Z_`w^ch0;9XKLJ0Hd5R+bKaxiM%QOM+T@$9-b@&7MH!_8RFS~}Y6ZL&(}ssx z!@2c3IDP7RjR`VAn!J}FI!Jfhc}^?4dO)t8G*3)YYEt+4J%htE{!1^1-OQH{ZX=0! zU2Gv&lLX~@gPDo(v%(}22_{3ZaB-^lO=I6QH=meTo7&y+))ZBnAWeP!Ob}R+g3{nI z_BgpVSTDoeW~8Sdi(M3m-reu9^vO?fg4&xwgca7)p78wodMk_YwSO8ZVF`SyN#^~f zuGI@LpL*SfQjI-2kp#FhfEg359)oOK7ty0HKf12AcD5^1WDgsG>A-aC?x+Z@!dW z{d%e*a6~tJq+Wbr?$y*k+x98-40y~cM-S!NkraqduqS&ApK`x@_b&XX`$ShRA&YV^ zD9D-hNlq5AWUAb%_(qpV$JdJ4h`JbD4{1%(z!?NJ-gA8hU%@1I_RN{0^tAZaRy7k0 z_Tt=dbItqrH&YhWw6umbk1GZL$;Ac!k(BDC4@Jt?JdX$oi3iwCe}2cY@qC7MehYyU zOK*-pYXwUfA?=)bAwQ2YRWXp~HF-y^WrVN@;;*SYzvAG2v>kg!tpXoBcyPjP&K&o( za-$7F((dzVVtKiCx}bL6`#jtYtn;N*Jt2F0l-%s&FOF!BiT!%O$DiQwkw<0woO-PF zeK=mfel2LdtBjxU;IRT;n6Gb$E5cA;KZd8qkI`SGeOLbbY>e%xqOYYDVYG+ z5bFA3g}7G(tZipbb2DzSm*f}?+SX+I1<3*`mRnm>1IcN76Jat*(zmlc{VK`P15S&6 zm1*z^`ynM06BCP}x^O}D_xdiMHWL?Ma>qpJ9BKs$ndF2F%6qeJUOu!o9fZxTbi!ir z@$-XHp9@C1=k{yR^{VbQMfT+4T~{PWdkV8GdW56M*8VYbQv(BeWtMG6gD?toWrZL8 zpbP?OZmHE&ws649J%CIalZ3E=>fgV(Fwov*mksIvM>PYUVPfq zATTh~PCdTLkAc!lkUU2UG}fx=Z+Iza^@YDz6rFA-H@D&#m(TaLGoneef6B`C8xM+| z?a;LkzzD{zTgi0#u$gI+_Kj!5!epOS_d+s{>GkUgpz8c51au0PN&~VUT~|=>&7Kgv zWWET3I@q{xE88(mkXK0R}vEIJ!YuI`U6waJk-kBRM zVfW|9S-bRwt#tDCw7}~3Q(7M=*LHjK`OBA=7JKbgDpPN_opyCCGOc@RWLaUB2gsTzeH`k6L{dmYQ9m`maGoSzB;hzL^rQ$ zWM~imzIw}sZ+SRAsJ+%>RQe^v8;nosD1IL`SW@eEbwMA`LF6b6&PCX!D%quv8<98* z+foJkXLuf4NliVf94>MO^fA44;xdRE^Yztbn~i_L8-UfM*;8OG<}iF|@XME1|GZwc zNb7ov1Z{VzYc8Qi7tDhW&|7WKPHn|azuhRom2%(DTn%dfVe8uX!#E7R3Ql+7=Ej0~1)zKTB3IubFo-^4#^0JBzSS<1C zN$}2r0z4rhA#2;M+6f+i{`uzyFK|E8e{~e8VmxkOt1o$qrJ-!XhFp=VzAR2{R!b8ZGTS8lob586-ls%4O{G zs+%}*;r>2ekN)ZoS)GC#!qx-!jW}5!quZ%0PT&ow-cA4V<$CPvmtt1!o4%BL z+NFP;`@kFfG}CQnkU63I^l~L2muoix)?Tj<)5?NftfJD@iY4$sn%QQWq{7 z`uMR?v7nGpqZ)l<(e|*m46#_(A!d>pfwsx~cCijEEHtRsY~Qg%AR5dPW%^%YWhdNO z#ck$_8dKHNO7*AM42#wlddem_-+!#Z`pevvdt#3AcOg@<%6FGoc`TgdSc=PzXiwNy zNfTgAxZ^Y~Uc6Y@4hOq4F*?y+Vrn!k>!erzOCujRT^pRJ8c!wdlVBc|@zw+7eM?2g zgutY+a_YMK1;W^p$IHU7Cv^fm5Dp#BE)KUpBcDbF2uoF(fC2qa`_x|q4?tP{dB6J< zF1FAv{rZ<6n8J>+c~R547lq3p@m$Ni-aR;wbl!MfJJ0-aqbJ)&&4$<9X5=XQz>cvg za5RVo#%=tHM}>qm!Sl$;$w4-EtPzyOox@vd2WD2RyOGFXq2McHr9u9}vAV^qtMqu6Sd=p8iY^9lFMFSdgN;Cv0+F zrGy@7zg=ea5eL2^v!?eyKHEMvwsgv5Nn>Y2L&Ji0;^=+en6J!9qXQKc78D_D`60nG zHz%jJ;K?yeNXWS;{Q_PPdeun|HX$Pd`$OQQS`d?pbSG6p6J2MATzCD2Y&m_x;C=w%|BM&W)U^@2m3A=JT`ZB4CHeQkr-f2{@QI_*k`(Z zS3gEw)8BXsmKi*=_NT(etp(OP^VoP7k<8NGhD`pt#RvSEHz`qeXy%9oB;zKKAF5ht z&za>H;0?U4aw+&XfxL5UziPJJTaolk5-hy5ta8ikc(54w1O*#N*3qE59VcvL%Tq~h z+~Y`z|3{Hr0a0h+4x<%({Rk{e90p<&#H{#T-(I`j8Kn^F5G3#m3TjwdrdIni$HQqr zmVv`sL$tJBU{A6NC$XSp^dk7u;?_NjOB(hY&L(fr)5FcY z=c;amNhlskiUd(Rb@J`P>#dxx{@D{9%a7z4u5jyaw&5$*N>Xd9%O8ChPFS>XlXCq1 zv7LCrt4y|2VQ_5{nGzo)J3712@R|_OK+suIN_FkrMdw`|_)8ifB5`=w9A9%8^pzCJ zU-U^tL~0DqJr%+d3W+J9{F-h!eGrPs>q3L&gLnZrqiQgn;1MQ+@xWpf4ZpG+ZB0os z-pM9`U7G05CxZKXL$|%q)_^DrVjbPrA8Q%8qzYDcZMKxduwleQ+;1U|A3qBhHH%nK zf%O`iY#45i2buH?j+EnQt0LS+6`lfypJO$%8QPCYYm7qrVX}Gx1^}!s2H}B`f*u=tSAVvS;ZRL|VLjWd&uy7{(nz6JcgMnBONvtigQ%xH zrm)W`a>VY~(W4@PWQhUf&_kG_(-wS5FaQ_GDVn^rU${oXy&+-}4cDGmR&Lv;BJ&LN z_es5W*xPvW0tN@c0S$P#=~;w`$%E-tI~jd&{>$1`x>2+w`lQego7`L*ktz8(7!18)p?1iq#r#5hPo$%Z1JAviTUr3{8Fj zksm;L$=TDl4o+x01Wx)msyJ3}>Boae2yZtZtOV)DjZ^=S4(EY9nQgmwxBGE=4KGM$do16=;OCKlTLlK) zPZsX(Xj(2pa|cf-%3Jh*@Sp|#&}nUh5rldK@jSe|{30SPSv?G#vZV^M_k)5u;Cn40 zd&oOw?}%7vI+d$YYG1rM@~Wg-3&(-vx^i7uMvo(uV{l}tlnJ~;nL3s|R09Yoeyypc zCbn5&atkgHE0(39iyNJ^MA5(sueH`BWma}6hjXAaoY|{l-=>R!L8hhdgA(NrbOo46H8{j&^3p z2Xe|~fC?~Q|gJgApT7!0XxaR9UJ?T;19X&i737$>Yw*kWnm>Z7ou zv-<#qLbMBRgqt?TWWbBMdg{M@_s-(;+xx{!pC2CJ;K<~}R-0XH&LwZWLQGcacyNGU zX3MA1+HkX&6cA7sp;bW*LYDIobBm}RlBn*`cUOi;vS>T8hjgnEhb@{{decKycL4NP z>2gFO#})-Evnnz?3; z?cru_nUS9*!p5>H`pw3`0FQW}B3IkYEXT$th5A~!`OMb3r8oe0olgFOD6RZyD1##l z!>~aSlz=gpUj;l)?@DRN_mQSzho)C7R1XiQ{&io2Ay<(P!e)}>sC_R}&w(AVtHq`% z8VtU!8wGA@U%5A%mO&75Epz)|7FhD^vK}jM{{fXBYjhkVALTnlNJWv_>ZJ|&G4s=% zS}8^KTj{)u&tapIZ371Pk^TMcBhR&e_+yaz>*I>Me!sKrty>#G!pFyFWBEdW*nU$@ z&c^(qg9(;4P>C6r*^o~8-zJX72k-SQ0RiFM+lIFLKlBD~L>m74QL^ zaynjg=9aT~rjuX`#?FK1(FdKi7T=h81$Cc3M)={6AND?SxVJr5D!rU)^s6A&2!hvzCT;V{ZWP!^{;KxrWn zrg#NB`D@p%wSs~^gBBVp3I6@$L9L4Riv#=`X!isspdjhT*FK;S3*5An&#SA4BEc~? z=YStWuK219I<+x49f)M*Kql|s`G;NmmpOme-|TW;y?JHMQ~GIsYF6m%RDAyYc}r0v z7{ZY3cx`>WuB+5_79ftT4jeqFT?#Y^|9;FBfAh<~#31w48+=i2MaVa>$hf?0JC&Lb z%9@#)q!fN-Y7seMm*W*uUc7k!>KFzF3iy!7pKj08OZyPQThcoT(PBtAgtpbfIr;hV zK6vM*){c`9l46%}xisLp$S%NO^%2=6jm zYl-7sx!+O5f80^~w>L%tm@Xui?)dm*a43XahEodD$BLp4$N=pT^Y`~}9iXl|6HOq# zmgBbl$EE0B^FQDmh`j`Vl(+zj{);0O3^P!XtUZvR(NKY;*}orwO+4`|&%jhYuz&v% zNNAuiwC^rfhxs;4GYyRYQDSls{I>?#(g0>WDGq6mVd!swE3A88T}JMpa3;uBeLFil zTFRvdRs-qB;MQ#8v#@IvgJ))EaSe`3Uq8tbCe0gTa42k0uy5bK1MKYVFA0zyD;Kr~ zLt~}1KJG6cs1j^6dChZfsK}-Th#KKCiEr?=$tUE zV=|_|{?e~9f&@}Ab)5ovGqQ*vr4!u0`A>eFZaXs}vsl^b3{h3&?<6TlCLt##Y7Fc} zr28VPhh&`2f;pHp2Fr$RWO@7G-n}OHl4qH5XYN2Xf^z!hb7B)Dqquq)*Q zI#wZ}2#BQ*9(-TTpZnkZ50v4Q5a8t{@GymeU0WY1jt&xJA=ErPN=H`U@sK~N&?l%v zK8}_Fx%w@^fD#ne{iR(m5_qC_XTlDk$k;eJ73|#!K1c_Q^G5>1BnD-Zz&FkIBn60% z!o92I&SIe5lg4aHr>oucI1OOz%5Ci&o^zKk5Pp>x-eEp9Mgj70CKLi#Om37|u`;u> zXF&WK!ctFDU3RgaR06vrrO*yVECd86NNDDpr9`Bpv>}Ew+LHJXd_kUQn~A3(AtcS7 zT6nieNIVSP%EX=*7{1+MJ81$o{^B>Hho8$Kuqm{PbtGtu3_D0m{k3gYB&1V!!`1wvrPB6Z1;y0%D>fsh((&)J0@ z+bq1KSOSCWQl=NtGyTkw4V-5uyw;@bI3(}`RPE~&~7INqo?hqcB7s*6f z&O97Mbh9XOG?xqZ5qSKIuWWDK%HeSNNFUSzn>^x;Y2Y7eNpT=XY2F{CRm6 z&Yy%_Mt}u{GKE!WLfk8Z@Nr7}#9?qck^iVPAt7$tubb@n>&(zu`rLf_4rW@p2ZVqI zgJLkanYP_A;XBi3aknz&JzB=2O!4b@z;z@zVL@<=Ja0v>0;M>ze z-rK$vWq!XjF8dH$`)_aI9LP0BimSks>Msg`0ZG%rkn7mK^c2KBB-2f9$bm{5C9)nd+*x+F5BLM#3@ z2Z4x+JjSe2dT;H_2E$qWz2`wj#*$4{Gcz-~M_>O(C1^n1%x~O07*BYrUjVbH0$gmf zSP$vd()H4Q)!GjNecA-mPV8|(2LJTIERo%M&$m4-#fM%?1?VgdLlf*(jiJd=s(MP6DV*Vp93uo0SU2d*xTPoIiMdb zAU(-p8v@DySFc`e;Y72b+*TW90_i!(KJ6-S0!K?MLlwzYwc5J6o#wxRk<w zGu%z2|5S-ZHEZPTWtR1L1x|4iD6lLF49g0_1P~3dZnBdsxH6aU%*QBrKRY{?0Xlw5 znaZylFD^U&mKN7OX66(~fnM}u;A{uYHwQT%WS6y!X#68AEX)FT?=a>*eE6uSXs!Zf z$#Nr{wG?cX#3@Mn?w5x?efnZyqK99ib5_+7`D+l|;!%Xu_*~J!^|~q>c(3z{+=|T* zltra0=YxwKMxH|q8a589>~M6`{e9m)q~wm^Z=Rvwk6nrd>ZZRT-3PXv1`-;nK3%pt z9iAI-nF$HpaQrtra0G&_)?3hZ?AWon?kz+EWQoBEYBM|lsi}WI-o-SO*?<24#<$w; zQ@I@wm;mGI)Zb)2H~4-vR6+nOWt6 z0DoED@gZbTgJCOuJMd+oGRnKrSj^0VhsCmSZ-F)I{AAZDMg@Jt`4(^>()VJ&6z6Kc zgc`=V{_ddps|`@Vcjw?xXeg)U%9^vqB@a!gql1FWGry#P;_d?5hiKIx(7__>abyV_lrr*@7{!tTDmHX^l{|0SBn>Kis z8z)1Zl+t?MswZx-L+smA?*2NI4VT4s2JEF5rlZ^NeTd(l-}D9=bO8H-j$`khjdX`@ z{>K+%IY8sZ+VZQ%CLe!F9!gt_cl@_5ILpd!x5k&vd*RVyYO6#$M0?d%^&(eJ05b@l z`^w&-gV?$vxc_zJ;Q2D6kxiH`7I;m-?sJSSx0i0`>NTKyN8e*v%$O1unW;%D zY){jlUY%)#Zk&~|o~cwUFI@>b;1q+Q2jmj;kwB+joAlIr_j7S*d{-`X^wP&IRhH%4 z^}CAWY2Ev63FOO`M_f`+O)#x!_)-%csY4&J9SndOQl?C-L=YG{06tZp{1m;SdiAP>_88FBM0iU0 zlkv8IKV|{_xyC)^`p^~vL|CT*PfN*am~FScm%cI+hKB`JX8$p`PRd}%%B(>ctmlb1 zpmCZfR~+jIRa|qJeGj!QG%Zqj-zrhiiseE#0lVDco|WaTWpOneS|w7Ntr1xVwDlGb zjq<1DY^?kpdW9qI7Wbk}zb>(` zgCT$pWeM$~h>HTK9_E{T_6cNszHVD_b_^Fc=(7H9bJ7H0bsQOafDx^EfEejqFtlT- z15Ts{!+~Aj>*Ei7JXpFe2b>BADeyL9UA)vd`v%#b@5PwwK?kh4%L2GARi#XMBMfT5 z_18dn6x<_#&vpcBlvNbTH^@Z|EWje630)FyUvIk$q;MAiJ_R(u9=YG{E`Q+?2HUoD zh{poj@Cvim5S0Zq;%hK4{v1Rc)D(9-er^m$DxmWAol|NcU?8}l@P#p1BFrSTYJDn( z>+cPVj*gyN-cFX1G=K`G#vvRB5PO|fP=!qc;JMj+MOA_ensW!e9w(bTWISOm4>epZ zCw9}-V+H_kn>6v5%gbv1QPVwS#?e0Ywb{U?2Q|&y<+xR!@0G6-OM_3AV-JMB_1fq7 z%#Ql(_3Pq~y^p-vdpP!_rDobRN#6DKJp#~=2m&grA=0>b5|E#gWKP8YMVPC4{CrEa zoOd;u1}(LLW$#f`0SfYOLux`o9TaXq?hLI^0rG(FY;)dJ`_#R2q5h?nrYvV0@7af@ ziK?w+J@96$DA!S1L0C@+y$eC%>N-K^;;g|{1Rgu@O8M4b{Gv*3H4gDBa66X1Dnl4v zh_OYdhv==(=)g$PeBFTE~L!46m4zT^U(Sw{f`8(#}Z zlz_*eM{+#~&4d8=Fj~82FxFJ)r)>PtAG<+3)L8o+r4C`5!QT%63f6Z99M$-vPN;FQ z0H=u>00AO}Gy~_7Mo8!4A?vCNmh;~)l|gTRTU4#@DuFZ9$h+8g`W7=YBOxYxBu zmcwB7#RLb67_C+BxF%72NW;3{YNZS?g;?Uf5M3B6Nxy0DlrDY0RjDpk)c;Vo_1|P8K z#lGerarkNqnDto%Xbx5lIL|&N%5*q-ceWk|INhi@J*SZZ$kxqS)6hs7&p1?}A(Wwj zFcBIFw?OegtMcHqn|ozl2WiNq6!l&=uEAC6Gxu~#GnWBH8C_)&RF_U%k&v4jr+@2v z@E##Pn?`Ujzz$>!D;@#yUY8|lW%h>Q&{=s68bNk+#SUG<4$%G>5oZvUW2oU6E>q#i zk6ZqF8n;-(dKBmx`zLv=+Rhe#o{4E!y3+FT_WdO98UP9?%IF@{GExeLrrEd#d^L3J zFhx5qTs_i4{`%c%0i06sCdA|g@S+c^wab{IlrQ*n7(w4x43 zxb|^K_#*-lpsNEkWz#%ehx8K&T^wleCUS!{fTBcU&->|Dp`oFufwy0KIa9a+q#Ww8 ze*#iokRS=13%{$O?z`r&#&U%Q^-6@gPIzz~u&A%E-pJAb@sj~w%P^4c+{nA`P&xqZ zt5$6f#B~F)MIbJ618gm#3BV9rez`X=3Idu(5l|&fyUl2t#F+FoHJqPwr$P4tg=Le2Q7v51Ki1q`sIcW&t;u(!6pvZ@ojL%99(6_Cr+@( z>hyCk7>s98Kx#&$-UZa^3(?PoP9iG9O*d=+c1WrWq2f~esgdFqVodyjsU_O z2vyx;zy+{E_~J9@VWfPjybJ)M4}k=65JqGVyX zgmZ)Hw%W@h_xFW02*b-je=OhE&*LGxV#5mveK-ZM?T+iU15yEiAY~;O=m-N-Ln{E1 zLu;})(JOfWH}O`FL4Kzyk}|tD_9W!zFpVy}(XrV1?FqJHrmqc5{$)2Nze>-mjlqoQ=luDsKqF#t(|uB zX>x}%s7ucRjf@N+GOY)o|7e&I>;-~#08}vMtPhAOufj;REMNVu*{@lfxXm|H*i5Hg z?%cWa4=^^w;4?8cOn#L(fpAYX0Qk65)f(ulvD{jf*ej#F;o+R^TrBP)oV>yGK<9U~`b zMg{;B;&~R4&mBb>`eSy~?j_P3A4ipGauv4E|78!H3Bb4!oxm_a(y@?*MjiK^Ovrp1 ztp231IUS|~=rx?SU6b5;e|_!?iK9xJ0VmPC|0yD^9QgDSq%b0QsRnpE)2qkpCP)04 zIXD^I$F5RVzMUSHc(5r71*H2j?BT^OyqvGEC|8!K+wk9cU4l+M*% z#>8{uzt-iP+2LQS5+yNmMXdI5*|iW{9583BWt*2TkcZ$KlrmT*|NsG+S6^LYX-Uj-%X}ph8HxArtkkk)yMmR)& z{Xsr&jduD?13*XAk0l{odk~>_??xvtFzO`$f{Ist3GfTH{@JFVT|2sPx35@#2yn## z%zKQy>T*IM@>W4a0rhLs_1H127x2+jMvZ{a(s6si^P&GVBtg5Qb4`84P*QfikW8KK z<`=J)&K6R@-CI@{egVWaZp_JY6J%suHhLmxVQb2E!nx$Mv@X}QoS`WeNvXLBBYf?O z^yU{xM#3ZVwZ#b<&tS1Bj;v(L;#|}VECo8`J$)UZZ1m&eG(aBc`{=G85nOLpyMF`N zoni~arAG7vl%W&+OWNx8JqQG*UA3`jQ&oCq9lZr6WzIvjDz2 z3`S@u<&1t=;3Ako2Y_#fuXMU`AMX<|7B76QWp_6bmd1{X^XwWXi|vOOMd%i_ZY4wj ztVq!g=SH~U|8jBV8oYiEP4`46K*|^c389_tX2=a!+H;Z#Rk7HpJv7@(d6m8$%&rG; z>BPkHj-KXaax`O;BtT;!>nh+;OvUS$zAjOgvo1?Lnfcj(=dj=|5N%*@uJzyTEHT)6 zYg$cI$uHXSi!~4KZeqcWzD%>B((8&YpW#K2Mi%@OTRaM!MjD0oHf0uzcGyBPP@g6@ zkPtv2&_K087e-R8W~R1vUO6bdCx8a0CqB*+6#6)d-wW(V3(LL3S)gTR(pIj-E$wST z&|XM-ja}A3hxULiDG;Qi_J-U|0F4f}(g z+@ucC1zNg?wAZ=1)Q^{{Y!{G zjfiO2iYFST1)seH{G$WFD)6QXd$S*aj*11XSKoTyH9)kA7o-UQ-Q`Hg5a;9Oew(;~ z2*_zW&KbVPe|>2%RS26EF{!p60qo=iO0~Oge8L}GtHrSheO#Gd0& zx^+c+Q~=y|8wqlf%-{>zn*J9-?bdUi@;kl&`zG{q&%bZohI!RD1E8v{u(fpI)!qVJ z$9^~1o)<8;8M~}!Ve*@+5d9%ydQ!&pk9SzPS4C)0QmlvH*{zR>-w?@Y{*I*{G$Cn) z;9)>Du>ce%sD}y4tqNo+riiowQY!+s2LwX?4{X;$5>R00befE9<1?}Wrfb*@RJ(m+ zQBge#Qug}s0CuyX`)@>w23S@AWiu6UsRwq`+NWvW`IcFgtqy(buAfiK2br0j;qNL2 zbGFt+uk%e+3|$Wbj6*3+S;0d zjhQ5Q$_~U>b#pi`KHZY$ls8qomV}sVww}o2TIO`?0xa;L4uJbH?dFjDT&VA31ZJNj zMX*3vHn*-bTvuddIb?&pc)~(>5)4(2*1Hh5kG2&F;E$@ycB~t-^e>j6<4-2Po>Ff%ezqR zG+h)_NQi&c+57iN5Gb-ii$MCeP!vQmE=Jhyi&QYI5uBw)rkeo}#f;ijTx@1n{*()7 zJCUig$l;w9+sE8h9dtk}&4Nb;;|PLgXyqO-8ubGRZB0^`es6!?M57g3`*$mucGT$v zDut3ShOfIm@B?&%EYqzvhU9#kM8MWui!!c46mP%pMA%)RdE1sd8w9B5*M2iu$_@M4FG;wHCM7R;pU{9s*3kr zaqJt_;IOW3{|bQVX@FG5p!M()fwFj@)h?5X%zuMy#nE7YWPO?!zvdO`kv}~P7L zj`(TK-S*h-;7skZcCs}Tsv*!neLbE4h|I9C|v@qfp4G% z=5)y(H+pt)NXQ4z0aijl<^wGN#Te;+=7ZC>Rpq-5y1JPXMrmHl@8d&TAS5APP0ijp8coh(#5N_G;AMG+1=78Q2!7|;5MHpBB zTSS-!(t{J~N*9LNZHv8tZ8WmobQ)_>st4AjpddxiCBRo_?Vk%qJF;E3Ke?)cBTuiV zrKZi-MdAG?K;RLzrR2^cPkW{1|pql86o>8ivg6btxN&U*TKNqvG#$1PyAlDvz7FT$<#mK{%#km zjuk@&rS1+J@ctA1**;B+>BGAr&mSR>gFr+0fFO={wg1hS{38~HwyyWVkBo9eBSzLxBzri zmmm zu=Q;Z?jn#-a9)*H`FJ4WkE`yNzS<}&Ml>m3ux2X##p8way$KAD$O{4QpT zIHSf=$V#7z5Cu+=Cx|0H|3Wwl2$$0?9fc;d`3HV8kd)(%7qpq|7Kn|7=|YAsG;Q6E@Ttggg`(LAxD8fhc|3Qw4l4<6k7oB^2I62z$>*6 zIes<0>UR*QF<{tc0jMh~OSQXdVG(X&;}kMgK^PPvry{Nj#N-}iH3xIv0b#zRX#jmU zulu9?EG7=GQq{UL3tSd7c;Q}LKo%_X{8;42Tg%E^epZ0iP9zOW4EWsj>-F7Ag>6Hy zypTT)AdZMT5{`o!gu(omzj!?+|86;R0&f-@Vd3AmEG)<;AXaY-K$Zx$9TC+I$pU~B zY1uM1c9x|p$jWwNSkOTo!|!PZ!2HO+BNrqkw(ybM9YpVbV(Sn{QawwwHw3Q=dAAp| znj!(Rd?Z{_XnDp2t~Y$f`%w z0(#J@VGKp?D&sCntx{(h?RhZ@_;8W|8DjS>xhbN03RyyLn9!(kjUA zgkZi{rUL+x5)m5Pny+#Va0Bm8MX^Va$f*#27P-T<@F>8O+Jd!+fJ7?0+%ZkU}w(8SnB4klI2=c0WRyM`B~vB0@7~@5)D`dwiE9R&$8li`#srvIAktivCTSma=$<#55j6 zz*8#P5$$~tL0$83QQj?V*WB#uXb4&8lRr(_B@iokH8NaIfosU%YS z-WwJgNWhb_V}?Lstr4^pwu1b=Uu)h`$@{%63s6NcFNf(3c9aXrO6MvN^uy)@Z#hq7 zyFIAC=qbBf40kd@cD({F{>YBgNT0?QRc2k*^7!^Sw8foH^Sgg|D4CS#Hq+(m1@QY^ zA&v-Y>XgE+B6t5;FBzlm7`sMu9)_FN0*I6C?AGnOM{3u$I?P-zYEu;+Y0dU=TO3Dl z0!3wB?2q0$$w$*^}m$CzOl6YP$XidSeaHupZ|Eh)n08k_HRL=cNnN{ zRsox(@u?N_K81<}YxAO#1@qbhU4&xQGqx7Z^>OzzVC4Ls2LE;#=v6~fuYVnwnmpGG zuyvyjJIR3$W3H(|{Mu;Vp$rgg0Wu=2B!i=Ur?d%RE%KUa{|e|PkF8M#Tlt7`DkCrV z1*C^2NK@Av8`tai(j9vQRAi9%-1*-S!_vEv*yQ0>NM|Fj=7&)n!399u?B6lllogT# z*?&OZD7W+B&udSTjNsrsf4}3;JQJNv&}6x8org6K&lA0|@-nOAX8}g& z!B{%;EL5y)V*0Ow{Q6L}_T5nD?P>gw1Rq_1KGYjvGy!v*uJFIVZ2J9{s(?`-Akl65 zwd}*3-pkIic$n|(JaPI8IN9n3D6$RDy|@wJj3kn>9W$-hBAhVwUmj&G|3 zH>xnW7*2J4a2Z_|J9r}v4?qi^VOXxRZP~1sPaOc$g$4fFh_E5aam;?cMEXiPheGbY zzU))cL3TAo<#FneVHw-veouhPJN|1`$RR^iwSmCvMvW>145A8f?_NNK zL~4JkuVnw(Q{Nu-4M>~r5N&)-8=|BfYvvk5fFPokqBS}uyBdv_baQAGUfYu13%W?q z$8eL2sJ;3Jn+EAD51S{^F1oF9i+|r3Snr|Fcoug^?&4ADKMr%^M&jTJ3Q|Aj&3kVg zZY3s4D)q~I3HV>a1&&t94VC8(p&UfzOFM)5it42LMjkzdhGjn;JdYl#IotXdzQIWH zX?zQ$tXcpn>06Z!oj;wYp2jT+Kx^`L_*p{3CEvlrXfLE_a#fM;-1p1|9o_K_!|(tu z8V4yFdgX6T{V|l0mxA`2y!T%G0f2yB$XJ@uSk0DWX%F2a)t`G^!TWKOx)nFP$jT}B z@wewQ9j7dmF_$<|Wk*DAUHt2H&*zt?7;tgI{dKzPB3td%Y==HZ(S4Nvxsx`Uc1#Ws z@HTpn9hfD_s1xf?vqkNrWBcvjsXCOa+PPxJ5-e!Z1e=H} znpV72RgFbmO@4Quz!~Yl4DWLMn2BJ~QZ#Su{VADcr4}fK@;Qgz$lJ)+a-`(X^C6jN z4t;hPJ~?>yT?5YDSTotX^9_UN@$K7ZLIiCq{cGi98YGwhdCw|(c;pJ* zA-jK<w!4b>&2&5O;6FOh^Bz7TH1fT$(7V^}?LTjpXzLE! z;hFtUU`F|i_(wMM%zu9WZCsYNwl*n1Zc@3}Kgu4iNw|OpNN2d`H@vkiJu6ZNK9xDa z_*fhI<9Go_`d!L1!Gg4((9ykT|6kTkxp*g?;nSb4)0Vo-zKS5BzT!V&Z5xthi;iEE zbaiVnp`)wvH{qejPg~O;WXVZKojz67CZU`GwMXS5XsdzNV;C>!NPhtv7$VbxK z31wXCE3+4Z1-Z!k_m>KcK<$gbOD6TrF{NPBBvzFJ_dpgVp8ihiqEv0m5|l7w4AP)l;k!06Mf z&U`E9c4N8zuOV6>P?X28<}3RcJ4jUSn;(s0T3AJe1UzF z_O16HHlHxRgL7^TYbNe6&cYBVxIGlA2vMP(Lj+$8>jM-9Slp$wWmJYeQ}EPAwcCFgC({KkWA>fg&4BPS#%gXxcV%_^*pFUOv}Fh+f7t(^5Pt)f57 z!IgWQ!0HtYXPJ(h)*;5E=Dtw6*OgMB}cPSmopHRgAR{2+s?LavPK3mGFff@cA zy%^U6B!_OwZ=V@xCrx9NMYCYRz1}`w7^%xwf(V2%61tF9%lYG%{hoEL96?!>*T&g! zH`pp44{1@(SqBl!Vou;jen;xW@HFl4?SB8tZtC8fH+Fp-1F&;tR);|dZL2(F<*}?e zP1+?lG9A^w;T;-9W##XvSao;j9fp`57)CLf6-{i2;688Km|tPm@APmns#43tOyZO5 zig;S*!1obV9;~CvXOVTC*OZ}}Q_>tZXZC6>#tEyDN|;@DXeI;;!YQGnQ(!|o`lB~G zZW1sBxR_k4j>%VZGh?5h`E?M=Jt@J0%--~0>ype!L~MbaUG%Alqmlm@8zV2)_%z0$ z2;JoP=Pxu-0MlO4sLO&5qkbbL*}Z%()uB=R^Qj#}??9px_U^0Z)sx!z@%{ZXiulUo zFU=ZRvnh*GzwLI;swn4OpBRX}?{ zF__bG^5@t|Y*`_;fx2EiKfUHS79tq=*Dcynx&6my=r9O#*v5SWHaw{{oR#Y-0d3QAG(vqAywsyQ)}&=x@U9_%+(@-w3EQ!k6dYRqWrh@zC}igmYohfhnj=g&*xwUek@I$uiX7!R><;9~@o8c?i z0l&S!Pz?Vb5{FX#!)VJ#Nsi4e$t`+`dX;|SRm5+qJajbL!$G&=OX?B$wMMtGU zurjHUs>`@+mS(L!=hOtfBg%Q6!~oSk-W>J~D4R;UBd{bpWL<1Zsh-Kz%^`sg;AC^_ z(F$st?$h@3B{s?IBj~ZZF6*V8x#WPQu35RDuy!im;p)yuNI;GJ&U84_z)ih{=eYbu zLHDdgF51q$N7InIi?fXx9;%u_FE#VdxZK7l{!q77BGvq!?dt4CMf}@6(`%He3&k+Q zFb{jJ99lM`|3~{A|B4UFrMJUJ=F*$s(;Vp!WB=+zI;M#4x~^}E%oyl?762Q}w$X9_ z1fKQ2cW zx%p>l5*S%yv$N}8{p8+Gf{wm-FC#!`aYPb5px}ll&-!@o!)tX+o)_s15B-qubdPSe z_>Vy65!;jpd9SeB+-<+S1}Q72@!leSeb+(F={G{_q5$AXoG7aBu(MhvVjUVg?$KA$ z{j>*|K2-;@vnEGNi3ZBLU17WRzLF|Bvow8I*vrF6Dc0-SiIe5OtJrjQP86}zXIo{i zs8o)&b5Sm(b+*RzHwQGOWhLY&_l5%yc>^y^}sZK6&XyA_gUa@OxJ)faRGm!GCP zp8i8G(fP}hW5tD%E($2;`7(u{_Yp-M`)Q&Z~2YNae ze#*D)qpl^?ZuB;y;ryd9A3D2gL&)sRTP{i`T$FY+_2yL#&+D|J=AeT}qZhjB}SoMLgQ7a!b7X00x%GuKQ7Z*0Z6Y^1BP+`N$m zTgF%Xo*#s>t8@&Dl@?DeU((FZ7|DN?Fq69x^J1UOTGoEDpOblS^Hhg~v5khq|L&5Zf)nMx_e2z!;hd_OlVRE`0E$uy9KSl3rN{Gnf!5ut zx=&(7d1-8hDdCe0itBxHi4M2?ISrmVxNc2|cLy?wl43Rr zerx+XvYd09cj-mN>DvOA!^KT8r7UA5snHjLJzH0b%i6A7@wgu4-Vlyt>|RZwqCXrj zP8h4aJuBy7^rUR@A9~D_&i5eA3P+n@r>YK%!F{>BJX0JrQu`;=vhL|~lp7o7P-vV1 zgE40@W)J>Y=@Dxx11EN?(!ffGXoVKOv&d@E+h3iUOE%WQh*r%7wgcX+h4vkjY5^O? zhAxFO$U8gy=+0qES|7aacy&>QFPaT-I27^XhpK~w{P_|?jIBtuu-ctVbKU|1;bVfu zAARho8wV~}Pj%yt(*p#@dc|c*=RUna+QRFX9=aIqdL`=Ss{5a|bx%16mN2*Flrqe5 zx-38F?FAi{+(7Z!FF8xIW9!GN83V_hqRv`j`_&6q^(;#!v-7iIcuRij)F&EbJRbX_ zTx-5CWul^F(Dqt8bMm}Jzd=dXVe9)%e&wpf#H80xRhEnF<3}H22c(@R0xOnhG(U$c zHzw8Z1Jjdc0l?(>J`-PGB4C~EdAPA`-4T}d9*>jOUV`7GjIEZ0silPyY}x#1fu=T1 zbelng_4B${mb+{SU9by!XXo4O=7U*3uQUoh>@XN0l3Y44udBV~Gnwgsr3Ox0a=QO1 zxO&c{l{&uc)fUOT^Wi<0#k>@Yo`GU&34rF_-b$5z;#!Bbwg`U8d>zr4M17%BpKO18 zXB_WOF;vxbK}Wx#xFR7z!mQZW@yL-Q_onaOy&AMeAvPY?>wno)U~4emS!6F?y|N;{ zE-7$B43J%3UH^0@8}{$DGTC)yd((ZFN4+TzVoHy3u@P_MaOh1 zzpQ7|P_8k#0sUd?{_%|p9RKdhQ;j!L8;hy`kOo2=EPSpF)iF<39eeiFmyGQ-Yw$8? z8QkUCNL^*L=n1FK#LC^)=5ZFas!qooJG)kAa%rnpTd^9d_9%s`Q*EJtUrU4MQZr}L z-HgNjoSch&vq2VpuLQ3yJrk@w(+{)#>SL*^-O?vx_w+)NBp!3StXnX=&!SexR4{*; zGm_O!PkY8MNG`mLJeOQIR%PfF1mBha}z z6?sw)???sI#c+6o&oAQ7=S)%uAL@h%T5G-EDRv-`wa|dHN*MBOC2b=S2-H_ziiPC$ zdB#i?0TW4B1M?cKE%Wmoi*IoT2GJ+=O&_IJ!K;y;O|>RW=uqCT=#P0?TrfUXG3|>j zjhlq(-BAm_V7*X~cfF_F)50j{!a@Bf2Zy@b=d`sovyh$VDC=rmeLcD{#8FN%Zkg`- z&ys=Tbx8T7QwWR)tN-x(GPL0e=)l0>;Ll!q4E@_r;It|6d6!gAzvY!%__W3{m69x; z$T8JfBv zp6|K$d`(&1eoZ5SsTWal=}JNvvx~5J#kpJJ=K0S(MZEdxj2b?{Fh##K7u@BLxzG55 z*LRa@BpD9O?1y}4aPq?7j%!tnVjS zSEar%;V-bQpDO}JW3H2D_nztv4rJ`OA?u^I$mVz@rZ{L}mk`@w+BroCI| z2BWa{OuoH=?#!oxf*Gt%q2-F3egOX#vV2(|LIWuGbZG@P<)AMuVVzT#4hFBzEu3)1 z!w?E+^u*($JO^6oM4Yl|W2T^B^8lshLO6+UpJ;kSy>jSE5wGFWz+9872Wy)_!18S& zY}r|*pXIy01N!TU{BqSQp4W}dI7a>nBF68Y4FSU7fY0GK%hC8yjyJNiSB_?;#?#u@ zjvudYj)8|LPFM2Z)L96g@0{Pf zZZbWhw6nO#^tCs%h+25u)YR0Yg|zUs)_?x>)=I4N*%OH3s<=OD6o)?FmUf45Qk%2W zLc)8zjGfa|3c7#;AG?#6Orq|3q<68WY+$tfe|gKVSO0Hnxlx#Bvfg-lTj6b^u$ebS zwmV68D>*kUl_!GzxBMRBdX}+5m~GNktu?80)yTfp>~Lxw^FHPfg%);) zVWHugBF+UAQ0DGaGUCk|WOQ+J!w1K@>VN+^iT07~aQDgdoOHEZ6s1h1xab_LfX&Sb zy&tEqv0C&a!@b8&`o4LwVp&j1Vm{F6ycJr{iTrziqSb8ns_@&GGk)g|;D^Wcs}g13IpbgY z87beLmYN`agVoP8fcfKI;uS{D6tWeEDZIATV7viXlt=1vX;4t=bZe) z83IHH8T2xyE~ZTmMz8xB!II)0*&pjM`{SP%vXi@5$@(LV;?SdLz(G!qC7$O z;@M*zvyDuQ+5Go)5*>hN57zn5jbCw$2tY1)Pmd5a2S!ULw+vQ_#Zr4`!%Y9r6)iDd z9wE7w6eH^eSyAd!j-0XC6=50-0ne1XaW^>VEuo`7^%%~7Scf@h(V9I&0*(&0(k;$Q zDgIyU6i8B+o|@vZk3r*&dw_R7I?}^j3qXKVIL})&eeCFMvVZWV7j@*lihgX|YSIbf zZuN=*1`3DeoH*FbH)!Lt*^5?+^kx+~( zrWXdD2@z^#9475PTRB>k=*HZmR+B-?JKZ8kcEdy=8zE-|zNfBttXQo%JH!RpM;`{HD4GvIhkB z0uR2jyZmUW(Q#}Y7<%;6=KC;-4L4B`SXwjF+-W~=zxU~IR}tGaU`bTO;r#t-l&|TBD#4Rc+YgPiH`|6F>Jo$I9X$?lwwkH4*VKMtE_2+1K zZ0njjjyy#lxi!O3n~x6~e7-t!61@i8gm8HJ2BvJdn_t0JX8Bal+*2PP=8Nu<^#k7y zE_D1#%|mYU7`pf;R01+Duasrqxu$#*lAE>2Qt5sulL;Eq z*FFZosoa45=_RdB5zcxcs3CFaY{i|$yW;zT4s-{2zyyq9EtbTb`f3PGqvPW=xtJ*Z zxw%2~l0>`6HFGZ}uj(l~o>Q1i@-pY46|SPfQL;<&N0nR|Kcm^$>r-_`MsG9y<|TZ7 zJ+YKBh<9ooWQ+XsLZL8ppmj80mX+!!KshF7h4E#Ng4_?;H$hI#g1LA<~(mj5lNB*;9sY)ws=?2NPYHY=}O_&JWe zH-%7>cSh>%p08m63Vbi^193$Ge8w8+f(puC#CO^#WCRHS4S{`-=&iags;!AN9X-RV2( z6wCDr_(*6)I^jW-+86NORRd6R>j_v9DoQjzP#u~81J%s`?o%(mzQ9)!MB(RLVQhxJ zZ*JUC$5u#P|9Zq6$jS{Dp~9A+&z{JkrFQ2RFh8Aa0kZ7p`MFV}#*mT9U_ZpknU@<& z$p`#*(}s1KjU$52SaP?{uO!mUSe!zIK|qtusAc(HDf#&Ss&Vq=)&AU@>86gCyqF!U zmo00qy|u0ti&Izq$kv!qp?Z1z8)8NW8VLKI2i($Z_V$iwOj3YanhJ1!ZX*G@Z%X7o z&?^0!%+M&P|K-V-Q)ej`I)SjN8+!Lv5|!L$5F-^!D> zG;b(|oUg3fXaoe>egCa83z$^xgN^rnXh5$WbQ`W%TpZQeTje!2V5&i=xOMJ^Q>J-j zltu!1B4fd&GvAlyp$BoU$?`^dVqrQ~@AzZz#g}zK@WOn%LwglRL+I~-YwW*tOAKAc z>U=P91p??ai!+MI2O-jPI|#uo{N4um(1U4q>eda1+ zqq%d4?R>@2_37h{BV?v+i4RJ1$T#Uy@Q^jT`3 z#di+rIMliOmdM`w4fm6D{h1p|>)H33qsRYc*ZEMZ)=oB8t+b<7uLAm*d#S3fVcq7s zyKhWrKhcyGOw6{Jh5QROk?$a3rL3&Uy$$Q&pF}DpBSRa=jb2Ct!H9-i3S^{og8tI^ zMu!ZY+P|N;`d8flr1J5HHh(K8gXddlDPh@GjkC!Vi!Vh5sn<4oqz8oylrWi_;4RF) zl^V&LWR8iuBZ z4zi(0^pk!7qv|9?c4C0G1^B=6e+2_!bM|WE?P~R zAKrX+@D`L~(DT}-0EJc7YU?zYLnK~fUAowQtk;%#Zp=ZC(WhQg`2mqxd| z%$afOk^3hES7s70ej)%0E2jqezJm)Tqge9g3r=j_Amr=xmj=>|fa$J7_4_&!cF+H? z%!4}vk-o2FS%f8DK5#|y0i7)D;#R-(6}$CgN3axW%zwdJo7dke-EX7o>gw89 zGcz+=Ks}{AeSbTHvA(gfF^kvI(voiagVcMeZ?AUW9_YS!C&d2PF)y(YUesqq+_s?f#gY8pn*w0C@a%tFc^!^<-m8G_x833Ir5URuQ&Fe zyn+I);Z+UNQrEw4(HOchI5_w?D=QMZ4$5o0pD;_-(ORzi;dTOaCp&Tav{1LZy`y6- zfk4pm>8(T;+*+^wJB())bVR!ay&w-lA33Jf4M`t`?Y`pR~o7e>zWU>6kPXiYROhohpBR$QzNTiWEHp<*<&SuPS6 zxVJwsUT6{>9i7E}JM91+z+bgwm_E$Q%fkx;d&&vNY;2%$S;L=IcxbD)2L3A|!<1A9 zU8_PsmmAu>b1vfb^1|8c^?MH(^u+REgqa2MkJZN$SLT{o$u z0q1y_E_sUpV-B&8LMMiUVUyYTXz1mB(8|gxwwalK<^agiR4`faijJK+)xp(KJe6k; zDsF)|3LCbAs;k$`d{&~VsjJ6e&~?kmBOG2;!TEP-Z2_U7p(QLB01X;rSuvrjS%GI) zVOs#Gv*g%3{f)qLcXtoTse(6y-ZG;Xx^sdQL_ny~DT;%l96oI9Qg`g-4pbg=l!&R4pUM7znAb0 z4?&ioel{Y)x;iYlq^BK5;KV3$v$=W#x{c%NvU79MOPMTdJ&nWRNE=TYznU0seOOwm zQ#=c;*#&Nun^UnM6)!Qg!~c`%4SVmPfi|&`4KgGA@i3U_!X|k6zke$#4P6B@foY6} z-dmk8@kJ#iCg$emIrthwkdje=Cv#)Z*WzamcKGr^=R*DRqxksvPQWJH?Wo)NrK}kA z-#NIXYECnBNvz1tee~#&DQ2DR)Nud){mzO&pB!ApZncqCt4q0?VNo90E5CN^l%qOx z7JUvtu78;_DA_db&&mdiRZA{9_vVh|Q}F%Jw`?sep?AH#N|Te5ZbB#M&S*nAY7e%= z5NE&x1Cy`B!?bG%UP#sI>bi*@oHmeMP=7U9wot+&s zQ&kh>$pl5VnU>_#fEnntYMx{XjTlk0!1^$~8#6HO5c|}^@i1s1C-MfdhfRNfgOd}- zpnG8(ogQez*MzowXn>~i+Yj^Lois9dL)rLQcZNW%q#L_K`03fArEAj{CBxm)3JMD9 zgL89p-AwmGTXR~WbA3x4s8vP56|bNfH(AgPxEs`v~ym$+czfk$UWFCTGPo}A6=F{eIz8_HhqK=aC-rsMDn3vBb<*DmYQv5D>G_pK zZZscgJX*x_e4gI~^_TJ@UK6ge+{(eB_ajEo2M6+~3Lm1NF@sZzSg>LueZA9ZTDJ)d9ptMlM=_)!GO45Wt1EdeCzlHok=>uH7G# zA#c!~geHHbv`8qf11CN{ox z>?DhQHGl9>o_R0bO4a}2tiiP>g>USC7=N);W2ALly=tPRh}Y{N{Of0np|L4f!|}u- z9zQ&3S zRkOXz;Co0z$kDMDwG?k{g(lMFxWR(lT(#5?PF4`KqGbJZL+K_vH8ij9@{`$R-4iFl zgWrWtl0Tdew^{PZv5PF$>bzVW5dpG|s3UJ%N5>-pMCwEP_hb9vx>^$>(L3wX3uiW9 zGh+r0w1%Vf+>+k)D-0%%QYhr2yv z?0rioC#P6#@!l0u!*}L$zcwR1lyLp}^#mh~XiKNezU%WoF+1760xLC*)(_-&Qye?~ z#Qr9cyiAQKtZ!Tzn^k1V$T zeM3h9)EC|(A`;xn@Ws7;k?l?Wx>AmxtWTj0HKNDhBiAb^RD^jXUd?vudh>kF1WLl4 zc(7@E*z>QByWbJLZ6G%Wvj-!Dpd*`onqnayK8z<_@5U!CNCn^gI49>80FwRHH(%O; zZdhAen=x<^Ire2cylk45gXafbh0h5(zGoYf_E73w8$r-(*`I3I)Jx^%LAPM0Gre5Z z)e_6nKY$F$*yZxOQqLAhY{>Cd(k{2-o;~H-lRMq8mh>F<$y2B5?rya@JKO>!U2>dy z_0&L-_-zWTg;(&$10XPBR(=jPL2#fs|B%2Mf?1Arfr?ijr#+i>x<+&o4GqR4flcVc zN_eiL|EJ%#ZHrPhjZde-VqnGFd9RzGH!gw&?b}13*MfkI6SUebz(OWDAbdA^(ZS2h zi}bb!LTgJ?7XTQDZbGrEXku7;Z!E<8BH=S$NndI}CCqqjZLJpMbm~s$I*#G-HTdxY z&tIh`zHDp;2<_m2JPHxqQ+aZ0M~BOssQvL|GC4rG&<&h#xtWyI5DC15(HzU@kr(FE zng0un^eJ(9WP~9>ojO(^sK=V zq!kwK=%C=0LCG~5a6l0YJcdDU4GB-wEnsYFskltouGaMrj2Bd_2p#O2S)Pw2it>N@ z8%k*&m*C}n%2=g{lYN-9&)BoO6wodt3(ukJYZ;MD>gw_Uy+Su;CYcx#>ohB(*YGDN zEp+<-YGWzS4w+P?_CY9MJO4tb;K@t!!i5!m2pF^#n5vhhTlN=MGYXweYnuANPYgtE zUT$3_tpafnHGo#{f36IBhq7pixZFV4N%HskMvzvpsF+tYHFZ(n+J)2EK3;Pz9l0$@ ztH@_R(!0Bp1zz~VpO36xzc_8_A08guOLGg?!Zt{g@bGX-PmHadUF(${25MsKW5?ck z(ejrAP%x(u!arSAHp?z3X$}I73scxK^oy2z+hc^w$(T`7b8{@}OKeo!1MA0m>#K(8 zNfS2TDjeQvp9e2d$b!k|(mh5fU<3}M2jIPup5fT4hsQsEZi(p$8*h}(5TyDqC!%Q% zKKur-lXu@Ztgi#!Fu$uL^DE;Es|o_c&j9^U*JTlF_VKZYXTJdcR>}j@1K5EB2<<|r z`Lk`0k7@Z1yh6onK=ojKBf=nApV@-&_9rI+!6z11r&j;nJ60J(t{w^_Qk~88V!M1R z!Oj7iQrdlUSeZ_9DZ;)GZf@2%Xl0BQjgvtMu!zUuaEQ2yre<7;H?AgccFx_)D;oYx zUFZqlh!yw`w^+m9#LnM7cKY<5_rlx?E&aY5KyNU!W=ncQJUYbjS@0#iS(~FrS3(|R zR<#0}XglkIHjHQlL?FSxKW#QlqV@!Dt&aF}DQ)lE6w83<0D@{b+x)F6V!7W2o5|$e z{HrSrJ9!C!ZIIh&3$Q$s>IohDu@vftvoJ~B7wgq*nt2;uK}TM53R!Chn;{2+fUXc{ zM`N!G%&21ACbF8LlbV(+A-lx=R_5jLYi*jAd4X zLlD417DNn9W`f~|kM>p>#FSn9_^)9!bVTp2hNkg`?Omkxq3bCC^LgF9o_*B@yqWgw z(?XrWfA4K$3gIpeAT}qc1S4&mhRAQOPX*ng9C3x%rYeLWzN2c|A}B5@niv-4Vt{$M z9xDJE#0$WP4I8(qFtVRKDX$uxE%O1yTGd;^uN&cpzYgH-_UwGip1%&jN%t{bN$OJn zR_16-Pk40~bhBlQk^zR`n%>OFwDnl0U`G7CE)YIC#&=@Pqu$*O&DRoZ#)TgPXRClp zL+XPmC9sE8o$ZVfdx!afd;r8QvsDJn(JASRM&%U&3z0-x4^hzY+xXLSgRU9yv4F$_ zU~DC<$&carMr5`gcV=2|e7240+7G)*u=zZ&#y5o;J&ZiW$vs;MP3Z$`(+)(s*eM@44xC#|)GDht1QN>D%i)+#92gHtxA>8T;u7uE&u+!B7jB>)- z336%DdT3`29mx0cDr3e(pQGRRP6Ync5UsGM4!S$@W<`rbh(sbNC;68dD$;XUuAo=y zTf63Txza}u=jFwemzM)X zhynfpB^Tfbiu#O<=BJwj*RfF2z~swkL&mDzgr4(V+Cf>_0$4;kpy+m-jqTCau7%FX zX?c0-yzzI?qPowyJ0|a$qGGx!^o}-^gT~r4P)X<<<=!Td=pV-i7RlMa%}DZbF?kx( zR|b3l%?s<3jBiui!HNuDR=fEMH1@D35o3QgvUUf!j+Rp(D47Lt$ zI*Et4$iWOxgZlw7ofbZ=Ru13&(bhG93Gv9ZN?NZu5FC{oSqSU$EPQmKAq`F6Wa+rr z4PGS9kmStqGK3Vrr^yxahpRi3rCtY1C}J7&l$uC8(9oG=mM}{I@s>30tXY%HtAu6b z!rKBW<$|0^=TtlB^N*c!0#G4_9@!OPV*T-?zS-Hji!O3my?QqQ&Sttle)7b<4-zvm zJYwr$JC2!`hEX~Rq@Lkorx8OO!0fifaTm~dBsA4LVd>~ zH9K~Nry}SC%9riHq#H+M+IyP8{>pj$pN0Ch~4*SX>x;r z_IcT7kT>w3rg}l3K^ZgEyXXGy-ju*zaQqEEn>65$a_azpTIW0NC;7Pap*t`EE1amw zGxzEHqv?d5odN=7VAs}EPaD7>hah=dy=D!sId}LNJ5LHSV_da1c^ISz&6!Oz*QfS{ z4n$&`TTrS6v6)gI3@d%iXI|jrCN8X6alNpCFReKG&J1GL*g%xVBSMAvgR4nXrDco33z zQzG~Rs55@NWF&`yh{neF?KXE9c@)a%=V_1&+yMe9OrAXWvjXtCrTlX4S?C5YK6$`L zi1Jh$qZKmopjX>wz|YEC4mT|T-hOwM%iCI}LtMq%TUuSOG>El8NydR|Bo7mP ze4N;SO-@DSEJ!$Ooq6$BDaeC~XH_4^gckX->Vh~~nFBqe#6YtI&0l`AOx+-XujFw( zqnej<_75AhfGZK|dj>riAD$Ws**K0ZxMW{EQ;(0VT&N5?R?_Hq4t2 zzXyY5^UYw%YXRYdETTi>u4=8QcsU zt^ls=QTe5V#`eJD|6Z!7#25%VuL^8auAT>{^zQB3+mn?(;as~c z8e~t>8Kx&)H1mOKYQjK@uE@uF#Us$z7<*7@!4`TL(}jM!;VIne6DO#@N_vMsyYS!( zssNdAxoxNOZUOg|DFDW=7V^V1uy;e4B$_lV+63TcD3`NAs&4OO5V&daq*`|>S_{MV z?b%nSmkoewC`zn<-$%zuKcNt@$?Pl|uvxA9v%DbLb4zJd>0J^5@A|E{JpvjSgw(U} zOI8*o*RT}4N=#3jGc57bd`xi&8>>y%pygj|HQ$3}TnPtZtO%xeg_@e@J;;_3S#tFh zGhEms2mUxqXn6zH;$sTMQf;&C$&+h9eP%;ZjEjE2h@P6~qxEAP$2} z(!OMk$18(T&Q|lgMl7W%m-CHAr{T`V?oenJd28ygS}65liDX|cCbJ0{45af3KpCPi zRsgCvrSGpSZD}eSt5axwVGmsTF^H|QS{oG~z_Xy`=2iCZRirR&{(&Io!Z6IQ{p0T8 z0hX@~?&aY>|NKJWfr3p1Zux{Ggu0uEy@f!L_UO@XkeOK3n*z(TcScZBqj(RyckeKX zvH7V>nyv>+c%}&|MpvvMiP&yiw58Yw zCFuS6C>XM0Zg*bRLy)pRDRi`|g#Pv@M1c(foo5!VdK(NpqL?t}xIMB{4yD>l-}S&X z5*`FwxMJ^ZJvQRJ)2Y1(cQ^#~Un{!Q4(F)bSmE9PTtX?%oUDgE`;s)z{J{`_4DNcc zacLf`w@c4=uEY_eZr*G$+ygVZLo%Z>XTpEljTjwCnnPGVumMUaZZv?R%S-bR!8ut9 zUX!jW?F00*4))>p0ZHZyiTQcpkEzwYVSoZh=+g_=L@<>R&&(MEj;B51PyF#6kr%8X z*f1tXn~-_Hi^<|OsQ$8a>C#qesDdA>E?U|v1_F%*lQ{IxjpYmvNtsGURk91?fEmPA zpsF-b4Gh)e0JS@ivAA_Ah>qfn0rVyZj3D?Es^|knGei|23Jd}<;U^3SHtWWWH58pM zae`{jBei4J4h~ANbh3C>mIm6qnba2^0EKaX961~`Ge#5p_aZxPV}b2jLj~_i98!vp zO`$Hf1@;QTZ%Ns3w-;thzH$RFvZVok!|JFApjUowp@wpXfLrPK7_PIjd}%nS*y#gy zInLbE7v@J9(MN@GSe_ssSzP7PcpowsegIYpriYgMadt1znz;C@4I9RMD zF0QxG2Q?!e6dv@sI_MdHIC9v{-JN4~Uc<=92-f#;W|W+Ug~41>&2-XL+i~*D8?rF~ zy(e=pYNdr%z>S~g%tS=&Y~H(*7sQr2Yy3*C`qQ->#8=f~AU8u^QmgnFaYrl=#PYAO z|2RL_H?Kcg3YlXQga>anv!f$^NgNQibs$^6cVn^StopB#>bPUXoBDcFJMVnB_&1AW zc~ldHH+Im^&u`bBJtl}Mv@QM}F%W{8hEFGr>HIUGPG(71ZO)CpsRpfEf($4OC z5w1KxMQf!}K)R_dd}zb!`Vpm5;IJ~2FCO7k#t2VA4vjwT)?W;(v4|glmW$NO_Q!pm z9`0BT9Cm^rCG2FRb0LDfuR+ycHCm_K*V=0iO;68g_@L)^1G9eSN3<_mV?*W!=!HA+( zvLI{x6vSEz)HWgj?@CBT?NcXD)*{(mUJ$rD-~`FkbB?Y6UZ7yL2DG#KF5%hL$hkw- zHbVFF0YNj^;hpCpZ3_7ts)<5Hx+hb3(j+0j0=9D#i4dKtXa$h!tb)Pg;(zPGFI~Gm z0)r$<5L}I(C%|hIf1NvIwpm_sWb?x+Noo1|Ww3Ya8e%vcx!bhcyIWpX7IFAyNxRhm z-d5r-$HOWP0tfE|YPSU-NT0CV%XanJwRPQPenOAKRp3WDfr;IB zi{P2V0hbhv79{9$WdZuOh(?{grqd|I-d#8DG@6ex41*q*uw1NKbp5^oY%v@7yk+16N<-~N_fE4V~NdSzX7fg?>z*I zwB?Ih)kYHu!Td}LiIgsg1#lHNu}gB3*Ro%Bx7a^jkpAU3Gn{EQ!*-QF9!fb(mt2B= zJ(B{?CGb`gv$=0%i>%0T2Yg}6LGC0~9nE&L?n zhN=Kez7hUDdFLh-*nmt|#Yy@tNUo56|E8R5{0h7F(n4p>0LU&UwuLDMn{>YZ$T)2q zt<+bU+qNNokBgbR+nXnLbLwaZdO4ILzy65aSeB`v4-i=Q*BGmB$qmRb2(@JNKWMDJ zeH&i{$9aRN^>4rlNh$W zT67Z~PjJV>`Ic99S%sUq#8qep;p}wcMDn+fOTE4TySE1#x?Eo^VmC&;T%nv4FfuQ_ z`$)fXuxQO+q?zIc>*Tex)OKi|(AuS^rKPuP$2Lt(Jx$G1*>i&=HR}K8Z#N4L*^(Tn b3pV`v|N86&mg_Y%fWrqZjPw3*yz+kl6K4t4 delta 55859 zcmY&=cR&+q_jRzLB4R_N+Yt~D1?fdmT#%yDLlY6{AiXnKSw%oWdR3%`-g{O-P${7Y z2t`DQln{DP`0lv7?{B~S14=TPdCI-#o^$RKMcI`|+4b6g4+A?h!{H*r$k~FxxIV<{ zN%7r3ju@&xy+~C&L*3SL(e-B2`)OsnOr5GiwU(xAW5G8z_D!2Qu@-TaW31&dLKawK zl}P^F%$s|M-k!N<@c!*z0!&p`==9mx&iD$(UHc{S$fND%QkN`SeeuIFPULYvd0xyv zFYNJ8X8->F`)3=nZKt6e{E96cz&m#SIMbBFv+KYGgFK-E@7;>HgM+C>{yv5;yX!F6 zoL|BTIXv6iY(5N$_D!C^Fu3%6XNdJCT*C!2pw|j`^zaz^Q|t=)#?z_IKOg?{{}}8$ zYA9Ftn;+)=MUMDd>02KT^9fG+W;?LC>5wiX_>}Paf+wkw|}aM{V($t_a+*(9x0nqsho-Ix;ydzE$dd zET_TCG9mqSauE!{|E_f4Krs707uOG~(MM|+vnq|dbgV}mv`MA**(Q5o)VMyGwcJ|M z-0F(tmG6w&+Qe2U*p+xo=G<8|Fq>rUQHoMt)VWvnXs`6CFOw%2tlozSb7p*K+J%+d zhkk=qVIg8-cyH+d{C>B9X3E=jDpaJc*k|U$OBhN9Wy$QAKJ9rAp#cKh16{$5Oe46u z&J2rT$&c@Go>yULSuuZ1a;d~KMWj7gv_InK^1;7A3Qj*MO_oRGReQefJ zzj31n=ja)K=;XS-+!6Si;*$!So<_)U<=~_lre)j61$3AmvK~70{>`c9O%IqHXE$s} zr8$T&URGtW%A>O}$?lGhP6UaDKF*-bcF|fA(2$2S#xL zCUp-kYpv|4@7&|=Iz>b%f~K8I#`}JS+x*|lWQlS(y-7r~BgPWDP&8*?xl?d0OR#!Z zy>9kqoKDkrjAD=?Tt^1$rUtWTD7UZc%9S;f?)a<%@85gy_k{us;kmSu5$oVz$i3_^g3GQ$N1c(2Y4H+IXP?tuGH41bOH$s6+a$tm#L z^7Cjq%JX0;|1G_~vtih&#@ov7o0~4n9{3JL!4l~WW7g?DzRU?eZ`a$;jhwC9!y~@tDf_mrYA$)8biKS)K2bLZ`i z9hiXrlUzMdX``zezwc+hp7^v5^L^_G8?RRD^WcW%pDG@I6w(=YHj1j)98#$5Qns*& z`1TFSyx1?A^`u*A+ub*?wFD*#AN^iST}mGl{n7~M)JGOnbaZsWH_wwpzkL(%Lq6y; z?hSHX-Jo_)JZ3^ptK7a#VI8hY@2=a9NxnV3_Cqj~7|bO)P^cT081JT=uN-nLSDac) zb{>2Y&e|V?zY(=uRNKLh!L%@s^ePc(ZWQRHX;? zY@E=*-TAn`BQyvl8-RkCf#3pZCHKAR74M! z0DhT9*5u3P5yptI!*fN*`bHOUA3F5IUBQPlBCX?UDt}gPX8U`#C>ytB4qV_i41&QN z5BTA|^l+o>RO98)Vo%ql>Gx5z^=#MbV(p>_gzkR4MZt4Sz|pBu&8^&*FOiBG;diE{ zxIl8ZMHI6$=YAQd!mEa*Gi`0Bv(mR?ZgD=X!~U7kw!ip--41esTVl5oCg1^ot%oMQ z-(9xK>cyOgd0^eAx>!2Pwi_=)C-F#HvzQJOy7k*GzEZDfC6kjKSrAL@dK(QP9?rUX zRs-RW`McxP`H7)-Rvg%-iYQ*fM1tQBYq_mv-W(oj2$L(+t<4*}*&e3XiyV_A6Wnw= z=`U8Ziyn8EX~w(H2t2Hq$r0zZ`W{d#y`hIM^p1w1=2YLfW$>i{Gz^w%+7jEIU>$1F4`8Hgm*JRQnR(i_rvYf=$ zp3e=}IHS(&=Sj|7(da0pc*TnQ69p$fFB#wqXR>Dxi4 z4ALzLJPvhc%WK97ve`)FjUBP+9fi)YMA=E)Dbo6KA9m>&Yo&yR@xG#b`QpBJ|K37x zeZ`_1N)KR+PET*#!*?rL6^0z)72jF6&bQc|MU1jp>^_zK&z)-LTPDF9!s0b!W4sBc zwJtOZOP>4V#S_Np+@@%W;mMKhm;<-?G<7|*IW9l^o{Ff~+C(SRct^`XM$JH1pf1Zn zt0RP(*}{f2jSFr7qg zr#C-R)`@rlx8(dU<>v*mYJ@|AZ^QRA(M#mC20O{znzG!(1t$YWEkznfNrtMb9UtF2 zsx>a@T>1XT+->gj>&o!_$86h*8qc^P@=W;RY$K9~Z!VH3`R=jY##=GZYW_v zR#nntF-)N44v%ci=3iKvbb8TWYMPj9}p$vkZ_Vy(#N-t?)xE(Y>WfIKSA774=%Zpq}V@Bj?+BGIa=xCH z3Qj*hkO<<5=MC0lbuIHmOZ^hs3PwiN(+D>zID1s2jSv1MuiHqlX-!I>p3XU#5IQXHZAQ^IYNXlf?p|+jMD~YqUSzHZgLX3L4Yq`yeCPLYZYH|&Z>kzws5rWu` z@u|bs+SshVEM}cQ=W!Lks?5%Z!32*qj#_*pl{8Y^wy*R3YQ7Ywrd>FwGhudp!vg_LQ@Ce+>>IG$0NrW|&kiRF?` z`22E=QrKnR!mq^%Rc>)a8h0LV+{LtyJ;|_iVRf7{AM#B~a&oBf3j@$clb(S9?`=#R zO--$|>_4xSTKfD5*O3n)x4FeGuI&w0tNmmzqhc8nF$52csPy>Fz9BU!UA=mQ+wWa> z9WMOWFDBMQKjh#3$W+CeOWxA|@XBjJoc1V?=Fi)G#D3LN3umqOXFp_ni9}9Vfmk^% z%+j?nz*n&`XKtE`+Y_Rt4F-inbjTX`$kP;vSMsDp%lHa^NGMz47HNfgHEG|Ik1tIf zwydPRL4Kt05O!mWT9@jU^ceTbB{Iycrl`P@lP8>h@yaf}+o0*pS8axDVCw z#wBF>#$s>dlIAa5yI$xmBPx%;+Q^NXrDbeqDC>$^+}MKMmfS|ISZhO)C)v}D>RwPu zA7(qdvuY)#wV2SCU(mz)buy&@_ZBT$TKbSjpH6m?8GLjYeVXd1p+}|`jCW#-X11UD ztN4^-y6*kjcYCZe!#=#7y1||#CBG&{EFb8D@FV;Y7thRDqif~PMM`GaBTv2r>r~fL zh=@j-?oZ={I9uNbPP!1sD>P;8_gYE$w|=ET>)g3A^SJ$*)0Un$@08cE92glf<9PzJ z8KlPhSAwCr`>V<1)gd`IfRa=FoX8pjInlbP*onpw%G-ue6z-KXy>WxLFwiz)LRN?p z!#%GuJ%|%dJ79v^9$uEFJzSlCzccNy9zkw6aak1UQyuH*4U3kv^U~{A?T6o_X9*BBLs-9fLZUd~syV$3nkx2++o28Q5r ztE;c{{6$xK;>XNGt5Yl6==)SBHrG0r*0j7X$z<~*VN{O(&&y z^1~W$czip?x5=L@H%evK;`7P2;)0MtPKrtYIZ}syb%!OPLvw3QC9Dh5W||+j0RVF@!QCVI02^SXz9LDa{P$ie({nvS0fos|E<5o zLc2ddH4auGhv zwAaL~FTIcuh^mmW9V)1Q__SrbgjL0un_u2$Z*nlZUkUNfASAKdfwfZSCl>p@zoE3Q zcyBsM4F6hMH<1%=pPcSQctwPC+|<>J&a>{vSaASedc{i(-^F4V7NLVluE@;I6s7&O z)Ia6;3&|1`Sy^4HKgu({yzm-u7|o@fp0~f8=Fv~5dO{pb6KJV0EUS= z`t$?_m=BiZ7s^=a>_aH8t(RuEokY%_#a4Mxb*v3yn%qbW?h}(g&4Q(Q$l-fFbTS-v z91cQ3L#X`Fhxbau(X~iu@C#k2b5%KHZz+ZE+Mv;Y0-~ZVwUp@q9xN<$9*w3QUbnI8 zxC`N(+@W$UOxF^R15l+se&kOT56>hN(2-QFV6OV#R_XXG`yrXkNp>Bu{esomQ?6`3 z)Rp2RVNPGGTA9o#BYMk5R!sp=C_cHsto2&KqfWZQ1%Qe3R;~I@pJjgPg74@hMLV@( zm@YkdbSuRzfB!9w+Xe=y5sq|qm>)ZA695}yC$F&Q%qrSZ!U@~geH3A<6cV#Lb2sJE zHl`3FpUIP5r!!2G%CUI894L+e#e17CkY z`p5ZC4{%G2VNgSC`YW+u0*WBB8OrUzSUJVlsX*{SL5=QtL!+s~h`zF^+$YJ_w1FI9 z1uJxoZ`$X{Ti_i%_(rseUD5u&v+1e#jU{!rUIxEdmZH{w-2V2L<@JLI_QG?S0aJs+ zGTV{0m*+=C|KO3&HYoMqhWX1ty@l|NG*_3AjE8*y_F1(D-5lKY;50j*1=+4Q15h!c1^Hwvfve2v~sTYd^F)>*Z45_6uD_&XXQ-H<)$%rToLh@OB|$q^*?TQ8#?BRa}(C| zKYwmC%0)K6?&Kl={8AuZnOLWCV7Y0P zc<%C9Y?G;pKF-|4$2^PZ=X}{t8#oB;V#ghtsCj9VQ9Lgj*{QE$Bm2_GjCGn{d|y%& z)P-HIGQ(|`Oa?k#r~tA>(*ceWAE)yya2!~0;@cPp!F8!tojW5w_4csTk}NftPHHQ=-81?)0i5 z?s&nTwEZ5Gx!nMK@T6E|->+f9st;7GQKT_CE$R}Paz&)^=(CU!Q59*|UxuCTN(mE` zRi&%2>lx(SwY4{}6zWV-3Tu|FjTnH!VBRIP2u(qvtx+a&_n%6`JnlYTH?ue%5P<}{ zdUYV_`^K0aBZr@Cf{gLqeqjJ>QGO5*-#RyOLurq|{!FQ8Ygz0cii{Kvb>-m zPVBPVm zmLhuWDCc4Oby__UtJ*QBWm&nSZaOr$YFG+Z$W3_c93qy7Up~v$QO#`+MoQs10w_gn z;hoev`Nd)RP-~XB3d8UMe)9o&0*^)#+iUk{-Qwc0Yi(RnWc{Zt4+hE>s91t9_Q#6L z{M~j5Z%MzziSn6{3V@0aw8{Ph@rr8OK zLlc7Hu}kYVe*-feJQv}J$G#wDj@QR0T&p5rx1gBfYkEcO>*Xf)bEy_%X%g8psy2*| z&HL_cc<)d(%Hv_a_yD6wgI%5R;U}Xx$v+KOqlVDGhO>tKETLTeO{O*J~+~j zKJO?D%KE)6y$0VO-Yc`Vc44wZdi0qZ0b%*GDlex7pX`mn#HKGYY$*2>D+sjXBI5&21$p&qm+Fp&VUD&)MGQvY+LCTk7G*jh)HC@`{Lj@@lm1c) zuW%IgOOqa9hD~BpzbAX+uVREUQ`7NHV(Y>BOf_0#Ow8R-A2z!$JD&`zE7zciOn2_9 zUUR@tJ@nP&SD7fXtSIg*D)V>UcRS|`)NDmdm& zNbGEsYL1m7o+%$~38_xN7A}gL`=t%~M72usby?sy2YrkmOSU2r)dM=#C^KUpq(ZjQ zH`i*gnyd*|vo(O)K`F3Sr~iZLW4*i@`g4b4m$F)^d|T_|q%?aEswU-;V57jpq3U%7 zL|l#l$S@n~p$l>diu<%kt+1iiJ0Cww6S#XSJg@D`PeITyGEhoKSTjkk$vp0}J!wJB zTn*uoS6dnOC~NYTQD~$lE&U1Q*|-ZgrEDUpP0xkbtIc%)$f*_}+(zk8Q*@E5_73PO zyYFXh1#ma!rgiFJtHrd|KHL`$`ImGae_w$*-~##-=C5E#duaHW zOLCR&MZ*kA%0V%u*HFDCAn0Llh|+78Y&F?G+a;_K*o+p(v(i z{8GpAH?MsmS41@veMT(Ix5y@*eUt(&yrdz79>PWjK!wR^4{^KtW5MbP$*@vl$1lOR zJ`X;YGQlD{z0j;l-T<=ocK-){vVBNJ}El>{Npl zyxaK*>gU%A`lrCq%z4{XE)up-)F|zx`Xc-7Gj`!IsEzU{qXTY|`%b zJ}vHguolCL!Dz~_wB5Pb#(FL8WzfCBz#-X^9=mfPgA&uQZ(~X!psMkm@2%sR(QvrQ zC95bRBo?UGQ(^tC0=YkdOB+Qwfw3BO;LuRc(px0H$Sj3Om9OrrqTbY+wSux?@VO%w z`FrmA8wr>;8@q4EQ2rII#(-5Y($M!AulNkBQi$gXWOnn(L2N?Fz~Y9=G6(THsiDR2 z6(0D~sJqcLPbhQI-Qm;+kdHFBVu2*|6c!eKJ7RQ`N4}Y|(FuT7$a7K@Us)Vaul*He zuH0)^Y1Vu$ifr)k*6Xt$Qe!nd=hCrpB97Vqq+&U*4sHj{`r@Se(eewO*BD*T8BwFz6*{H zfcHucF_>3&VCQG|pA@lv4QUr& z7oY0luEmdSJV%4_V{n42cOk8k4Wk%_vi{_#SId|f7t$^d z_;^=|L|RWJ1-W7_009i>p6IM0&p3imndba4*N2vMjd4~kdy+rD0GjVo+E7lso1-Xq*YCL#yyt9KYhS$cul%AY~TL97ac}dm4eKp)xv-cwSFu9kS)=H8IrUS zc7IEf1d+nN1^C|*h|gd3TRL6tM?e%PpxtvpJbDa17CsB~F$!(C1^Ys8i=3C%6c)(h zvd6g>XV%x^_w!2Li6toC)u{@mJ~U>nCP{L@P1h)bxH8>F*QK~D|uTn04Oa&4^S_J{_0V>%Hb8W`X7d!x!T&?lsW9ur&FE;8@K>_11 zkSBVrm1WgKWfHBA@L$7cX3HI6%K6pCzM^F(L|*osgIgDDD~T0qiLU9^Rk`o3RG|^C z|Aq#{eWJi3zqxW;Da%p6QKFxx52g20oogcnFU!XQDY-c_D@*lKn#SdxUlnVbp2jIq zMqQ@Mn_wrA`2i=0x30m0t!T0es^}CONq3t*OSsaul{DoG(*3>lih-tITiaxFJ{^0XYc-->v$1$ zsuV5N`X6>oZeie_c@ksbqoPeZdJVXF#F`VeuRNE_t7H8k03(mG-m8iyMS`U%FHIN909g4(wMf)Jps)OJgyU~YK~K^a6pmLbVawlCmkCR)_5?l4ymlzs z9dMp1>2=p1d9wUm9qLGGJrT|~IMH#E5cPJdrp(&aA?Md7&p-lwkDoeqt<0WqDM#lj z-qYLxLRePQvxMqIwFAhXC9bj~*TM)?yG^OgdXt22!#$6U6X<>!Sy^G)D=+IxDjdng z0EI^yh>1)qA&?>?FA*UXoOG&CvwSUlnfC z%<`t@A*{GF6Hq74c`?c#1XDxu&Vnc`Pa8g|DQaY9h-*Iw^7`)6x@j|j+;2i6KAm0& zBxuUFbU!WD){1Vf=+IA=Lag`&Mfd<1<}}0Hv;NJL%lS{Bq`NR>e>{GmC=gd=%!Ttw zYq=Qp|)=YrPOSz~qZr&2nNef~-s4gx>j9%t$ICUm@_K&(Hep-RjE-`NFDQSf>Bx#IW@SxXWB56Qw)O0IL z%$@~*FC$ZR9Y#SM88QlGC{SETclmQvWt=`aM7ftodDRe%Qz9N%NYi_?n7F48wJJv}j-uvj_ayK8x zJ7Pj6WagCI6SE0KZ7o8tA*nz;dKJ}TqNEa2?~_+kp2XFCFlPE z#75eM81tC%=iiO4f2J!1Ni^x*&+OdE_J_1F!)LA6-;?+>#D4Vx^LA+|QH5tq4#@*9 zDC@3DI^ocgTMrrP$6!Srs5E0`Cqm9HotnQy5V}lvXWLiA1R=gPpqO9@C;A=!V|J+;Isx2t!8WFMguvmd`K>~Yl~&Q$@uey$7Yy-o=H8tbkpPsv5aL3 z=h;nN(j@L5ggUV61ypfrq}GT(H&G!@J9w#ecqolIDO&Sok=}$V;7XMzj5ZMX7-U|M zjHld8C!L3Dw~2y%Bc`-w=h~+D=C^m=G$hY4_Np%`sY(~@Q|HJA1%KBf?Y^UXo_ zG{8=Jk1IkvL{&g|vfVy?!_D4V+s5_nr*Ye#rCsRDKkowZ7Gz-)zE+2WJb zMeH*}Pkp|jZhA|-#xjf`5)n@>nt8G$PY#dtzNy&2gwd6`I$%XW?k*kFT_4~xWK=rwP6SvCm+wv4bc!Atne{<@cX*G$~@T#_d z^_}@3BQh1-6lf~iMWKgS&h{eH`ApgqCL1j0X5_6>Q-|&ikfC%#d6!WZ`H+tn_-8GD zEhufr04&(rx=$WB`PUkwvj?PnWqs^kwY$A0e%G^x`+Qnc$7uYv)ZShF!)u-!S2;1U z;R%PeJ&8*KwMu<-rcN)y|CChe61Gy-C+qc4xQGkt-F_;GH>iHXH6-W^%ru9>5ECl>m`BgC<|M-}SciFlW* zIXqW`3Z-o;e!Fit#{9w*SYLyR)Acenz0QnN!qI7s$4HtRq&QI|7U;4hzBt1i(OO#% zw%(#=f!3cpJ_9DzTaI=626{kLatmN}ly*ooF244)E^wknkI*0U25%8B~*6SEtOktKAerj9}p4w?_ioxM__WyufEYkoh&uW z5^cafS3S*u2(;LCDl|s;Cdah=v9VS2=cAmfz#>?F)V+>qCWnw1`VpQX>lc59iTtUcgxG0cm?^mFpphoSnSUIg5Qo zU8y%MqX>e)FB$pQ{~DMeI)ut>zI@lfM(d+iTN=$w>|k$v9fN=>h0D$Mk!)UDcvYtR zh4`xaTlp|RHI%krr)}?i<%bgB9>Y<>3VR3rNiC1O&vE?bHt8YxeXu|8APw8WtnEC{6$!?%Cd({Xj>PgfN7s!6BalII>dYX5b+4C97)98_ zL|l@K{M-FhNkk5->RN6f2wE44E;*>zc!bSiUD?En^CCt^XEsUEWv>Np@q!NwV|5l- zkm|h)(u7O*zpUs1hxzxnY2?fnbp}`~5`J+6{P|sqt6qe3W1dH?jB#dTcD=t!4+XKJbiIALdhDGkbFTB!_m{*l$?c2NPA}R! zZcEeZ0%^moFtpu#wNd;1TzV-6OHjRdW$$w8j=%%hOOgP_bjkB^^XCH55~Om#v^D87 zm%{e!R;tvGQE2t>Ht>10;6%^qkfI5iJcJ59}O;3 zjNk)m^AVzd^>E|L>Ych>Yt!$l3iyecU`;`=`E85-F0f#@&z;ZeT+s;GiP`p9>Q~9+ zQmP3rSuv4o!ZYmoX*BpYYKVks-#|9L?OfmWo33@y zw;O0xOaYglB@18^FXe!z5;L1rW8?~iY9%t@pLPTAj>8i(rbO@NDb7JCrPN69;KW0H zGg+^_n8MdmOp_!bhD;G_QSAEpBGIiEd@dGrSTo)Z^ux?|=7E|N#wgY;bYHA1cxDb% zwJuAU%Is3AsZri~6qzv~?Id`hVENuaDZ(Z<5Ux3r;^JF?!a{5>d^66l1;r|KBk=k2 z@=g6Ng?ElMg6l)BbtV2(8DMJ`$SvZ=4;+IhnumDOrGB`8^)EM8$umn{BVKZ3c|PPe zC5I>W#pnvLHKJ8+4SY%JB;yp3>?K@NW;N~fv2^zmrUMD$F zvdiw=yLZS5Z=leOhe${*E?J}T<}WtKO$zm^@5~UMB!y=S0nueGW;BV%M8R!jRW9Cj z{XVy2e9|y|*0LJLnkUc2flnHg6vEwT1%eCGYZ}zbWqOLfL-CPfvNPb;7B!H^fYN(V zjG0@!at_u%WVIi}K0!TWsjkIceMBU*e*5~$rIKVQGZ#2&U%s?)>>nQNQwEU%JKJTB zUHoy!uU2q)^+Xk5kFwE%XMy}a-L|NVJ?1bten(UYE_sHT=U0Jip&u_oo_62~POV*G z@2;;s!VvKMr#^FP_D;Hfyu6PtjpfJBY8OI@Hx_f?;o*)u5f>j6%daD=S~&(Qk4VXO zfC9i}v8Hq7#NXL2PidIzvhdAHyJRQK_eO>$5I3!6$)=wVI6t-KDsVFD6cBK>{gQ$@bk^HtM-+91zD2@H$;3agdH0pJXW}#vH4&NG&5m4Ou{>)DhTas2N zQ%@DhsxIcrrWCg=sP$`>*Etae0|}M1;rrD(x}}D1O0s%Z=0Fx+HL@jtPbniSi|{gB z_K0YZom(kc1*Rp9_5q)u+w4bNKffmuE}j7{iXYG6zQF+TGsZvFfHoF z5YOwQf?6rZr_d5vjnq;y$cEAX2m}}e2X=+iAnkK1BzC2$->}Cgdu(R^U}-E^kJW29 z#j;M5`dA>O0l#GxXAe-neJi0&>&1EPP&IDYjZwULkjFid)D(Jzx5V3ud7PYt^t?Is z*#XIuQ8+m;ITAAf`wjeWkf^+A{uN+-LbIeD9}DjT@8L+)daTj`mB;F)uw3EbTAIBn z>q}rC+&1K;#@25u>yJaeYNUPWq*hQUy|#5E(KFV-7%Wb|Khv=MGFwM}c>6f$*ia&O z^u8745IcTlJLBnNc<$_0#GfenqCROx@Px+4y4n0nHvuPq4c}UvA~+^J2Zr_9TBT&m z`8{yNl^>k)Lq9wG#z5kbSK#%^8X7ysHSnfP3!*3waSlqTC;=;|NhTI9bpl>WGE*ck zM;goSu%uNGz!v3Fk^M7}SKapKDmETPTuH2?c$W}s5;9lP3n3p$jo{dkh>N}Mu;_H(KmvZj}M~0S$L7vCJ=7esBD-aFE{zzIJKPe{mL5IML`ChwJ zpumNDwPtf;LUWHVVz_;^11!#JB%w7Xe~gt?>FkA37cJXy#|zMXAm(@FishscBJn5* zyx2!c*!e(Se^(!`t4SuhEG^f4sflTdO?49svZeL8Ky{-8d9-5 z%pu7QdAO+u^LYOh?X+lReHkyKQO@-Fj}EAi(kLiMW*iR;ej~XaG*I~` z?yXb`C=srtdj+scpuFY>_gj>EN|HUnrEgeja~bx}F`Ys#snV}+H5iW%_7%ESf$s78 zzC&5%NVvLx4vAL8Ggqzw9)sAcmI#Wg*AvcxAK^056gtPimdJ$zy7XtV=(DVKZHsLRiQmHaBH`_n`Jej8($A*Y4|-tw~#4 zV9)W|)q8kSQkcOY1dn+p$k6(W6A&moP>%-swm_+* zXa^Dyn$|ohh8+_ud<$r99xRM7@XDm?jR*9O2Uo2rcP_(1MQ!3G7E|sKkr6=S4OpD- zn79z;Vv(OHCoKBxEm%X_7a4gj^Khy(m>2n8kG$!#3=4TmED%Cb)RL30MlUZObkEC3 zR*6(@jSOt<=u&E;v=#`!?@^1H=i!;e2V7JjkfN7|XTP59booP?yTN7OWmHrGdm{&d z*$30IcDe-&t=6akVXhbExg*IF3#Rpm(tk;b$dKCFC|I365kcWLn>epSr88y-C=3XRp0mm7*}K=Mt=DV8uSbl_L546Df(B?$c`#<|X5qjT7-NwoDe$T+ZeCh25-xid7}LK^ z^K!O{@e7A`%z1E78aLhC_EXRZw(C8L_QquCi(6glv!g0uxe&Z z!PMWlhQS=z4aV-QyDPN;yb`${p}NCok6;3rq}^{dW-LqJ@ogTftnpdN)J&4Z1s=QL zvr>sDw2Nld^jBKv^=Y35cvOO1(kq(34?>c1KtlqEIwi=xzI$>jpPJ;Koa>lS3@M{t z2VYS31WWU>0fQWXW&q`CK_*5vdr$U(5hLxx$L-<+6HjF(TxFUSCtx{&NhagEi11RM za{&NE^{N%&P#0K7C`iPdmJFH+tu2sLj_7V)cxK`Q-i4s&uds zs13WVnSay@otWce^Ym-=*`PzK()gkCuHXSdOYm3Zrlz2yzg9x!XPkmj{sWp>x+hB{ z!Ko#;iwUI4w{yssC+q6-5j;<9-C^5<5%;To^#0kOufQ;sRJh{NGML{wIrrDcmI{y6 z8fvjaxVYH-MO@&(54n}Go8M?3@fgKBRUa$2!hEuMF2=r2X;c-P7>eNm8NjbF_gJic z(5Y=!djY6K&}y`gqXYQr22ZiKcYx`*!peL3!Ra+buA>U4r!RL+%)E80RXXwGFRDGX zpU}MKwndS^6;rY@SjvBo4>78Hq8Kp zDvv{C3K5oB8uI2I>-5?gNj5;wtuiY{%<3G|de3l>+CUvneHj$`oA(3)eRbam;M<9f zQ=d_rblGs(KF+zDI3>+tv$qjZoTQS3Dw;pEs@(cG<0rjITajLDF-vI~382$#x@`zf z%+6)>J1_D8xol!XD}l^FMOsu+L)MpPS0=kh4@GSh5;|K+lGCh%%hMZo!n(lr3)K*{ zps;#Wh0CeNr`(Tgy0KV60-#zqO6}0#VN3iP!BqSnnhhq2e65s)eDLFM)b{3u^9de- zB?x*SKiH2gYD3J?wySqa!;^m(Cz?Qwdvj^p6Tcu@`OUS;_N3CeRcY|BSHloBiqDP% zoP<7=2FWyXI~iSEOXlYAqMw`q-xjr4Fg-jIyiO} zt@JG9fe;Y@tFo>F7n*YdcJ)*d&+z2g@^{zK=_`Z*6MqMITu#%n$guP}B|77u+n_i& z3)C=duxyVna&Xa%i@HfKzXLxGO)5pYkHXv94urB6b3Kvkm}q-bMIxq)r}zy(px<%2 z7#SRwX3q?KZH1yd|&rYI6*Mf{|Q{oQDwM=Kexc7nVeQE&+O>)r(M)1qL^ZL(C4%OS1vtVm({{;*R&Y& z)8W~}%-jxixAC`18QwhPDq@Oy1p)c2a*e`bIbv3{X2*Ymb|-77qPq@qm_rk(vEiC6 z1exyjY(or&<5Sy*kIeHG{Ic0jNb^oiawghU*5T-Q)5%cIj-0#A*!k<{mi3JWv`6-C z`-w8*;6utEJ9j2nFC8$f2Bv>0579x{HA0qAE2Z8V*}fmsgQ^jjTP!6~wv3otKH#lV zb`#zx#s6k(ErQTR_HD3I-7AUAc_ka1UVCUiSYf;5f|P*AGNW-Fx+F?aX8)ExF(#6m)Xsyz}f?uYj6g?IfgvzFO}0ov9qDjd8p+F zp$wBJEW2?T!iI%b>=QfRUj!rmY%X+Z*3>L3B}f{KI1iTEZ?%m@@fpa~OE)!<#+K*T zwV9u6K~Uni?>J(Pc1yjq2paT{x&yHY&D5oSeiGX-R$M5L2QT$VP}9JWp`S-WLcw~H zu~!$~Smf`fn;>X|wgNzu)JE$_fZGZ*5RDbwH%K8TARVDr2zD*%@)RA7-jbdHnbZDY z+%z4zd}GN#3%#~t=iwu+X%6M?A4rpCuq*gaybFaEEZe0Ew!v9uJwae#20Ya8!WsN> z(A9$UVd?W%J68lz;0cW`>SYPC6Z>(2ESLj-w+t%^9{Q4X2G;W4{Ux>b=JrpZ!4q~x zDLm^K8Ol)nJ?D?%rJ9`HuM5R`|9N{cVPe8-L_+5cfapK zKl}Hqt+@{uSmA=?|9Y*FC=08@cC5GHP^eCwg9iKc?tJAgs0ww@8)t(*vWzIhSl5vD zN;Io{J0`nv%Y56v7r`hpJ+#J4y1lZYE>|*h%9nV1mK3^8xtB$Gyt2tdJD7P)aEBoh zp}Ytn3A7&+vPV-m{(dL+H9SN&?uJAe z(QotXS=#0TL6@aHCkde&R2a&;_!vXEDvc5dC$?eQrg!{4%HZFdP$w=|8^4rQ_%vyj z9NDTcJia`il_h|7XI$C7VJLTvjpXq&6UoZ@MsZ8YtNk`P0w%Qcs7U|s>w7zxmZ5WI zM=yYFVxGva42=YT`=wj^8bcy}1&J(I)r4fhYUtwr{{}8%&8kkBTSoWZ(uLD1e42?} z&{;n3Need5E%p8r>Tbh-Ho@<2Nk&+`x1KCTGSk@zT^)n=cl#&Eu&K3m%w_xj_Z@{7LpS1qJnm>1v|8$b znsxfWgMoc_g|=~M|C4vc*=mk7``}7tkJ~FVgI&3UYj7&SU*hlAC(>;qA|q|4dMGAJ zW*rlEdzCJo?bJ8~s^EW*W+=WIwH(xGDdT?Vz>}{Z-y=$(>i!YdC0-fy=~HWG1vybr z#b&rRVYpF>dzWD6i^F`bi~27@xcHZ++;>3q*nR%f?+y6-0Tl%!F3%{-ElnSSuw=G$ zt8GW0(!JnkcY*kP#8xhy{C}em8vzCrtC?wb^vH;Ar7Nd6VkH&v6_d^gF3`-`t3$CR zLGB6qIR<64J>;KPGAq+wX)TG63hb#smhLE1*2%UqIQ3xrbrfq)zB{2Ptz1I?Nu~YB z=Gn{fb?xAYw^hS^)}wwY5=FzdU9a_FX>){0DvGwVVQV1u7hC;*3x_r6MeQw?mgsE2 z8jEYWa{Yxuc~>6aLr*S%iHgmNVu1G4>;K(L$>}3QXyu@?bYEyH{&!d)3mV;Oi&0Jvy2}PD zlnvV8&u0$a9i!)W=lS8`+=umX(_yUKeaUjM_j&*Q7Ov&N` zADw+L9A)ZNQ|otM1{b*de-{JW(E+qyzSn>}5gP(c(z_ZGx^r=$pIa zx|JZs{qH3NPk(kMBzM$xL<&wk*<~{VpL~0u9l}}io_QUq^V70fE&sWtfWJ;~_0;c7 zIQT)Th8GUCnC*NFJ8x#-T?fEKd(o^^P|SFZCRG_B7spsqH_bt;htY zejKDNq$4&X^fPeM1Hb8KLGl{E)#y%}@Jj#D=27|o- z;i&D;_f@Vvr&tE__ES#f@W8n+zh8d^cXerxQu(#g&hhhqojf6)!Dixl#jw;)w6v(O zFq`KPylWfsCqxCY$MQ}!&}5hK{OtZaw{E>gTMPewdFDHhsg6=@^J+>z2iBF^_1%lT zgeHW5$Y<#1Uc={H>)i0Ssflt&4mp?p-v`l;=jmNT=NN95dVFXw|=!}LHp2~-+^n}m>QfH^1oNH z`!X6YF$ezmZ&+fAc>7i#8jT=a-71c-yGBtux%8mJ_MgLD&<`i;2nYyt4AuB7;kP#M zmB{LkAaT3i3!m%jGt}dtXZykR>(7R~$lNL&mPvkTVPS`ieJCYiAL{F`UB7X|hdcDN zeP7{c%2?PDcJ>J;-|p`2_1QWW9bMhDJUkt1>O1%B(BZ?Z2M@k0FPB{U)vomMquZ8L z{)Z3ONM(-15N?^%Dtg9l5tVLJX`@z7N5@e?VPV}XS02f4OkXngo%{Uq#f#g7M7b%~>gTt#wLeg6H#7qGb6*sm>PQXR z#mu2S)mUCxi4A1o?SV%uF>l;YTOAGM5VLv>x7D4g#fJ|euX4Fnt2;R4iZ;MQLH|>K zv9%Viv42j!dazWK+^L-iTwm9&|D^q5t21ZLybKN1b8;#mO^uF@MudlJ;l8V?sJuwi zOqwa>{xE9s_3Kyfu?W3^=c<+Nl*#4ZkSNOs4+O-;yIuX<+)Bn<;?frvUGrABSy@xN zae9LM{F%B_5pi)iA9&eb6<1LaP!5{`+4 z@9J=w$nys8Gquva2lxa8RLcgOgU+hQet?0_YlI8lwz0_~%}HU4t=`O3QERa|X30Ot z$0vt;s5<`swT*sZ;o;)u4f}#Q#J`c}Ogu++=1o~DBqDi@#5pPN+3#1}Cc9LkOuRF# z<6~lCyeBi$)uL#Notpm7O7`yEdxV$Q(XPs8-=>#*FL_~^RCMRu-U0TFJbVgcV z1_g=V|8k&a_7jsoFFqsVi~WUWFOAwBK?w=s?!mP}*nn=7{%-A>uLiIVq&;S)FYVSg zGRl6mpwZ&FcF!N5M;m8lgG8=D2!#S;r^vv9cXbWL7n_KWql7u2L_p09Z1u>X&^ z_l$}v>B2_AIAa{gL=qfPQIVXHWI_=TBxgiKKm;Ub4(bRhNl_%zNR%u&gNo1!NRx9= z8)%>jHW?b|y9>kce&79h*Scpd#}T^EsZ+IU*N)HAoTM}_TK(k7Hh%`b_oSuHz=LdT zld~A*ZvO4oQw2o#sZSh=-8hfAFhhK8eZ8<{zbiRCaU5*`7^x?L6zj{q?V~&72m;nw z(lHlPGBVmeK4VvW_r|ilVW1pRaI4qS1$GijIanZi&#ar zrRyd=*t(kuA0=!VKH|uZT?(-ObAm;}S$XSjRyAAO?C#p#@MUslMW4OCj!yW=2AKNH zx;lks4-lwuD*=a5wTcIPA^MJCBZZ5Li`9IG+LHN-fJ{1!bzppl&NwQN+tTf{r$H27$Hu-N z)S9Dt` zk!IuUyI;?x>*UppQ((I(5+Yu|K4Eg}785>q>(OyoXOAs&V{IAwsbK=9O!z}E8`b_8 za}u|Hv029(qeKtsrD67pAzG6>PuS3eUe#$au}-qPuyvmvY4Hvy9pc*L*DtJIrX*)2 z?>#C9fEU`5sG5Q9bKNq@pamWCrK`QIoN25_=wkMu_!`}&J~-RL?I#ri+X<0U+L}8Q zb6rCfgY4Wsz(kEKk}#Z8C&9qKw&=-=l9vJXc)!5F_6+@!p1umZ?b1&8=kaicsP4~J zO}bhniS!e4WsAoKK0UYU9uzAWL`w?&6YhNl_TtXdtJKMoT$)|MRai@V^SpW3d*(C4 zjZ%}kL7*ryKw_D?BSfqn%^!?NSX*1N!ZCyidM-O>T1-foDos1IJMv3Q_nVf~$lPTy zR#SUXzA~pXd!s#e>iMoV;>MAqBcu;K(rqzO>B3S0uru2PQCL*Ss;1wpVBPRw$FwWs{NT2yFp99{teo9@SthR zSRP>NR7|i-N%dJO*OXtiu2{(*$==VYS4pe4;J5CR2%IOm_r5ysqc^u$Yq)x28T8;_ zc1D1TDyACz?Aa?DV)aRO_R54V)oa(D8G`zF;^)`0G)q=IdyFLfc;_pSO2KNA+Ltan zPUM+4%d&Ycyn-<#Az6QSe=~P$(?r?4LGx14b``lKwtnhFi4wVOMMe4M*Y^*tQZvO) zadWp7+7RvgJ?4uJkzSlsRiG~2&MLMaQEA5JVDplN*r1hbweC_-I|TU~de*_;$@s#M zj8YctYq<)Xce-xcy0s+tm_2Ecglh*)?3W+dmvVXY)~yjb99CVq$p&^zLbqRBjS^2V z#s`eRwkaNbsarZhE9qaUUpND6Yg{XP$*I>Nq}}0mbE5yr(}P&YDuJ7mvo=?@KDa6} zki>99x5P2MowEPnLARX|a!rw9cFQBT&JmGH=0}mU4x=X@6F{h(2f=F zFW#H+kzRgBHa>XpU{YGzX-V4h^c6)#q0L)-pFB|*KY8%9))(Q+gJy5N=o2mRccTmV zT=~r!4mUeGI%4^FkJQvu#o&QN#fV$3?xXxTUF)*EwrR>N3sn4x{ewbdKQ6BWN%8R? za>qF@4^B!sHa0dIoL8Ch4`qK#P`od9 z@kf&#<>xeJwc}VNT~C}kRkQDcypmG;K|}XfFy;Cix9s?C(G#RF&IFcBW^C*cT!GkR z>9j&O7?kGUzc=VR^(KHsFm=O0S3J`sKihDA_enm!#z`suYt)^ zGEvNVz4h&F_`M$qBkT#1^Ugb-h-voKeEM{)JpFZ}iv^QnG#k?gyDaR~CAlQsrIiZWJk5Thh?D}ha-vs6 z$;!sB4X@4{WLCh6>u(TcWMsS|FaP`~*ixU=Mr!(n&@M-l+w0Wtaq7egWk<(+1J{vb zA>UQ5UJU>XPY|=}5rz9RhMe)C!HGKynj)iZdcJj(SNZ;f2Vl*|&yw3yVW}}toO&sb zq1{GzWMss3Bq8Jl4mT+@5E>T7_}xalfN^%9P1jmT$E4VTYotSk9}dsVH@D8$aU3~v ziUkV3NwoKzYhrw{rI zcmj+HdwTR#c~67E1@dmw?bwvT{j~^8PcU!H>m$WZojlnH`g*oSRVi%3ZLlV|cxA3z z=yuDA5SuGWl{b=;lQnp7hAa3irZzVwCS-*)6u0}(3SfoVsyEpl5TT?mN-$H z_asaA@f?%d1I=@=9|r{(4!lk=0%t&2{o|FZSLYaafO55zndm-<-=@UoG4pJs7~{aX zH(BtWR-VU!{fI3S*`7Rk!mmFMQW%jRm1!LS-a%Y^{L#;00+(QQ6UkH4(~A4QN2Xra z?$w$8#DNoBb-cqh(xYNK@bQ_I^`u@x8kn$6=on%7pJSxJhA4}p-ZV9JE9(<`YfDat zkc9NpcUUlFjKlLK)ittH!IH$YVR~fh$rC5O7X=nHbb|6R>S6`;*={^IG_>W8KgfiB zm$taibVlhE_@#l;t6H$T=u0rpIv5~0A&Is5*8Nt?y$SLMn;qc1i%K9mxJz~D%3m3J z8PuGpIIY!NS9Bz%b($WOJ1CeR{x?h|Xm>Dj5>H(Nr4~Tw7S-UN!i$k2iL*08F0~uDrBj^1hm!w7hywi!VqfHkrN7#@e?Q~&EXVcG;9!$3I12?< zC%OwVN5o09a|31beX8K98GaSR+8Q#@rXwl99+H>;B=`KdOKF*1$K8pZB6=LSY+O6n zyKJIh!W5ID-TcNQBUv)lQ$RWJ;fzwwKG+hLPn!y6DC&UU~0ejHM9q7GQE9hpnBW3DY1c+1uf`E3?Q1@ z@$Ldl6DQ^2o?+yn54KgcJ5R|Ne*uB<5;!X&mS?24fOE?Ca1dn9L&L%ibf}=~j2XB! z7fhoZUJ_O;n8WSd>q|lRB;M5qd-2=1ZxVfNE>h(N?Gr=9RB;2#_f`;q-nxG0A4y0^ z2+Q}`!T9a(9m%g>V-3cEl8NA(s+xT~L~5wv%q;z8TMDa_keGOTd4U4Ep()qAS)qNh zr>J?baD_TAVp)8 zCz^07Ocay3n@!I9>=Lrvx^=4sn0nQY;L9Q(aL)PS#f!y5;T8(?!q};X*7ozeC2ub4 zgI~yORd^N_(CYjhbkfNG(YRQ|kMx52~geB%LvW~{0KS2 z;Ig3GgVCoBVF2U{Am=ximTgk27r+O4ST&Rr3@rT;M|F@lL60SSV}Ce)p#OA#0vwD; zK)C~-5t+&$9F#~NS1$$s@im^+txk)YQ!ie-?*+(#ZFkr7OI<5I$pCo2Y6=Uf8xHir zfdK|8oWa_w!KCllZi22n1GXQ)`bdL5;JgZ9l`^rEjB2`O zW=c0G(|Pvn88VRCg&=Izlx2+93jirfgWVL87jkuL9qe2E`zipYp`xMjQof_U+nqV~ z{!5OZ-c_#@heigbm%O(5K2!K8g0fU5acL)*JM@H*xoEI`ksH>VVR%>}T=3IiHt3Cu zva(AK)N66Ft+%DXrbEAHzR3-{+~KSMoAdRFl!B`v(oxQM=Ttn(q{W1x`b*RXL7Yu4 zh=)QKIvMtFG{_tLQ3`0X=Gf=r-I4}4(ZULdq2_~Y-1Ww&Qt<8%wk)ceXx*y~BKcZc6^=Mg-V3Yxi?oKXV6(M~JVIGP$L#E~UG zza8oNB^p^-SwZmv1PN7;1SJ3PG2~-3PuOi?(IR(@&An zq!Y17D0{6WVm<4;g8is|`cEkS&k`!LS>A|XI3uGx<~r$9pQx#+wVW8=(hKg?v(L9I z-fUA+x!#rve&J+_@+yjjeXUGVQh4O&H^aDP+9@MwgKZiL)ZX;tQ6_i(73d$V(*@czv;LAK^Gn)vlUC*<}G*o#a{}C+fx6jW@KSu`JQK;@`|^_+c_gI_prKr z1RV6U4!2hRHzh@9c6*(IMz1KdeZ7H5_xd3?Y!J9U_64t~ppcuZcoLMl3;NFQ=gV+G zjOilC$?gRk0Re$O;dfqMvFkh;blY5+pJ5a94+!EHt}c-szJGZK<$(PBO{2;1ode$M zv=Uv*xd>s)mPAG7wY0^Tz3Vg8aEF-9*pdGn?6sS6{j7Uf`v>}u*}eb!-PWfS;UoVy zt-yx$sblzr*8!C0SbycptXT!W-yaR;|A(>sc>d49{5QV;@5aKV47wMcFGT78ZV&&L zuJi^Hp+XqAJ9)5Uk)l#fRJf+164ZMBc0Ni&z}7n?ZhzULJtO$&vE#>29X~D$2edL+ zv;Ui~l;OM}nii~+cRRQ`kc~%9r^q$|#eau~%~E|@)6_IIE&57bt|%!v6jPl#1i|7!K`0c3j*X4=TY-~M2*QA0jt!>+?TvV9i)+;S8t^+<$wIev!t!vdtzaA#TdG^!m;}G3G_weCEaKsxx-_BC- zY-9{9UNs08v{qo4KZ{s)Un}%rJyAHDgd*^nZ9&LiEwqU2>Ct5Q5N;5cpriZu?Q@PKzrGgd9?;tuE$IevJCi#cAUNXU;@1N|Lm1VvD*zOu zO9TW$!_OIk0ZV@fTb~!E`?3pC--$6XGVGTE)WB03vBhh6gB5_x$~_q=zFfNbW?+<8 z&%JxFwamK`jB5ula!)~E(L`@9>lgzg_l#&e z#B@f2H4XUuKf8{1iN)>i{K-au!CR9P4O_?s_T4<3i5PMc)G>AZHfekxy?9wzbrJErgW7Rt<*0gn%fr?%`CgHjTLGrd*fB6dJsi zmdacN7h^2|Y=W@a*Xf^zE0N~2ba!>Y-;?J(&CY%vjI}m+QIv%MXQFu)9hC>rO_1V*idW`O; z%VL8@x{k@FH%E&$RkFa{i_}IT{$l>rhD$s9gJ1^4$>DVV^oO@tI9RaB-`bJiIlbU} zfNM&WfP%Ychgw1*H6a3^3@43vLtfGmv#3eqQl!|c+FK_D=39K2PL zkA(&2-63=O9Bh3!_tKN61a%q@(AX;&5bdB=L5ky39{W04Y@Kx zisqX44R_2OXY(jRYA5|z1O&LZslHJRLh&nHAm{qK|8+TD{q&HYKIf4m?=!o>0TUiB zL@lGX+HfMYce{gqm^hB13^MpG4y+FcwkCTsTk+-*drDpG$^e;X-Sv9=d!qx8 zY2~xTS5?8f$f-cY)!Yp{{yeE_*wZgwytp~5#)LAp8AU@($TbQvxp^};MQGhkpg__F zmEd$jsg=d-UzqYGU<$JT(;L((6u7K^UQBSTt=8Tgr^&wKjrZSxmPs53i&`I2L3!e` zH*UnXHk%1TL_IXQSO*N$TZCxKbzC&?L{!L=qXoy6c}ZGY-RM7ZL7sqa9g*MM)D(Wf zEd3;k$-cm%qpj|6{7@X4cX@eu)(=(7CqL+b1%xoosSxCWQzR%JavNd!1yBbu`!qZr zc9LS0cx%UThTlm7__|l({kvLw|AKw6Tn2uOx~8UEZv|*x(?}a{ilK&v##=m!Ps7$V zrY2)a(b=wH;gN7F_s=moS;E4U->8Zz5o zfGTTRIK#nl33T~RR}A)IqC?i!N~TepUk_RPd-VRQli~+oo^rF43nhUId@9w|Nvq4g6wsZr!>ib6yL* zFCg`220r7s#|kdxf7chk{2;}Ta2DE+b@0LDxb*VCu#oqC`}Xa)fZD9!SPuU~JD$g5 zpdew72#$LE&(W@UD7Z2_6x?zT>Ue>;`@$P2y@@4AQ5E2kL01co_&t8y2zM`KWryhj zUq7!4ugMG*C6|#Aa+eW>9HANnRjr`1hIj9xaVlWlHRiP&aLSEBsR$wOyD6(9ycc%u z%9SVCIXSZ+T$h6&4447mfv~&T(#*)Ls~)7WQ55wgSof2Bi{fj^=3j5w@D%E<&Vn-! zQ?FrfZy%NNpwn8n-(!Ph1_X>@pquSk1HfG9$?rUU>eN)%mS#inT5+J~o6Yuj|GdkO z^UrH(4R7|>o#AifX`J_k93#ge@D2^|X0Queo7a7Sj_x*;An8cBol#i-(-@)k*%$Pi z{}#w|}mC9YmvA4yt@r-8OC)IB)mv%0Jq9R0fCL4m%-Y&K}dSmhth z96#(2vxidMS--qXxpi)q*~8aN-ieRaXs`Zfh$fyA2DhMsGH{K7 zOLSeR73%D_(epu#*@N}2SInn>4g<>U$`9JN-}aAUdn>#5$*EmA+oQ|U{JjB<86_J}VX+K&HU3-&IuMv~StM`@fp>wXOp|YS0X$hpivID#s7a@Sp#DYMH zIQ^XWoi`>A8jcV!ZSz^)hDTQ;(;9HJY?Tm>oh@tC>!NF;tyjHqI+$OBOQpy z2be7Y3cFem3>b(U2w3<$v{-0MstNM{oMtcGNLPYZUB3OhZEo!OQB|r?$;O8l5ppYP+k?WI?w~6- zEZRI`;HN=mWm|K%K;wV4g+UNBX4p`>P-`u8G-*ld{Wl14m+W)UuZhZl>9=m%eeDP7 zU-(m8)Hkh{>bs~FZUKK_fA$bscQXD}0@O4@z|PWaGS_eswZ6cD1KM!|;OBPbsj}QE+&NR~wSot2;dR}HP<@47`goq2* z+joSdI-2%1xv}lSy>aXU!v;d(et{xiIE&%3%8Gt{zntyd~cKFdB z)9Ejs2)&RZqs#PylBCGJw`c!q`1hyw`b#+>VGydb6+dL$_b3*32}S&5a4q9h6=O1Q z4lD{ci!NP=jFX6Zm5r%y8Cclad!s1?cG4_#+M<~yW^cqEUPKN5fKE|B=w^fFZ0Mai z1QaHR0L<@bGW6@UK?urJqcfSDg9N0L5W-|Ao4^fMJz-bXHnNNWZ zMbz>Kf}AJ>!W7qr|B1O)q{j-qGaaGBI)og6%!ZCUSs>}Sf`|o}bZ_1)k##Wua}pW? z9(Dl`1cD<1{Qj2lZ(0r$Gh`k0tW;?F13CdD2LQmw2pfs@gdA`wbr7dNMSyzoa9hC6 zAQ-gidPz-Elv_qDU}BmE%5Nx^3LtpMCC<*zJA5rnoFhdX4IzcCWH~t*!rCvbUd}?; zOhBuE)D@5)G;cl>H=64?G35Man7MhdzI%?c5PnG?uRK*52-w@VO3U453ok%t9eo)4 zXyDJ&!0fNwgPW25*GXav3>l#$9ng{VJ3uNZNEmMNw3!vJCS2{-xKLFuMb#Yb43w_l=|EeT+)3t$|S9YDs^>r`rl~R70Z>?Ip%#W7d8OAe7o6H{22-F zqX;|zAPna^ckx57JOD4@En|!Uayg_HL8IzNOJQy=<$dd5E)c{tOvT8Yl%^?ewIABt zuuvg=kjR$Xl0UUrFN>4^(PU^dMhL;US1|~)AU5RXC^W&guK;9iGY|wW>?mUaOawsO zK%9XF^UMIG>a;p1jC+-WwlmhoE`=ve@cr=uI!r&in+0IC#v`J51Xl&^?Kr$P^elJd zKlf5l3#B1_3^dTLn7=z0Xu%3C?xT{=1p%~hcGqJu=}CloKTTL}?3kM^zMBsSA^bxt zDvZ1U&D1=5?3i?DKoQ`t0r>Lf0m}F4Ua~>Zt&#!KM3g{PilOL&R{sd%&>{25jlzaw&2(}^2 z{o6bRSgK|jc(Z{$Och`k5}Wu5t-~RiA5{P_Tk3&k)5w?4Ku%>4IB!(Xfe>U(p{DiF zjgq-)HPotZf^!NMHh*GB1Q2sW0L~~ntK%=VIa0_hJg`8iYIND z2w|H3GR;qM)>FkpczH}dn9sT#P&ASZ$i(H+oUMN`>mFteOgx3n8&uH7KX$%pBw4WR z^!-^dQ@cEuzYq8p#Xl|{4N19<=Bi=G%Gis|IVrURE|%THl&wj_BL@vt7au?0=_sw; z$u^uVPt_8TldJST8ouEaSxRd%|8|J+Lq551K8NgLeT*g5*Vx#;Ewf%l?$hoia)dQ= zaQ@K|KsRWf3ON{liUP1)+;U6J0^9xH$^y#m4@kE6m36I|O~=DGDk3z@2Cq#3G^q$p zs&Rv{#tE+wUsenzjSJ45k|K!QmkGe9r=%bbx|!M$T|DTJWLaK1P!&aVE?;G=`99uej^z%l`jz6r;22b0VcgvA=rZLJP;5>Fnz8Cjq- zHK!{cO3C1I!-P};AZ;vvThEg^5rum=co;&NldZk#=6eBu${dg_Mgeb8LZd$$uv88S z0Mi|t$8xd(AVD`%5co;v6Wj8M+Ws2*dvk!qhrl9giAL5*BMUuKbIuDluwFLkV^-t3 z1DFHIZpk{t*vWm}4lodG7o7ml68dielIuu!6ptL@@sf;`A~-7mKEp{M$tuYFM^NI& ziB4DBQgCQXT9#?Q=Z_yW?1;*KtrB`Bisw<29pi4Z&rAMYIUrN}I5n=vOt2s2=3XXHgeckmc9k2=m z$M?e^!tNV^(yU$)Wz7MsNod)s-Y2m*OjOdZ9;x%Z)1L2=gcrOkbN_gclFoI=+a}aC}jKuUR1K|F+ zEe!Nkjkkpt;W80QguVkDMhD#C+#g+~PRuc8Plre{S6$~aMX@f!l$a7oF56CiapX*#Bw+d zfS9H0j|nh&Ndhd# zHlN-G{(xzOr^fV=5$2&>a=GkivL+TN-!c?wr_gS_;Qaig*$_9wEz|A0n?bW*ldHT$ z<1Y{*OH3FZgtMsExMR$I!8&`7Bhr(gOA}WB%u47p2Xsvb(#L^modD?lue}gR515AB zXHSz3H{w{a7Go{)J!(?u232N&fH}jWp`($3kdaV#@agTCcPAp0!Q(6YT-(Y$5n3c@ zKj4R~v8=u7LmAub0(^FYyED+@2TO z0IP@?H{Q@Szoq}vAak-EuUCap0|<)bUg%%86-j$;FMemg?Odlppws7%sS0QRAziVA z*-5->2+Gs|N5sIcLv#WUA4a7DT6N6p*K?kqO8`))fjD-}?GJc%RZ}x(TEpaXzmTo{a6~0HP~2`) z=5iBK1+=I(^IjI+hsOb9sogSivcK6bHXn?RX~w3Jri7e+JRqmK?6h5s14B{uST=wi zdGy_a)@mU@5CjH}@0>Df0Iv^hfa8Df$SK|sA|2>6O(x>70|oDAF9Eo5q9wLEJ`CJr z<|SZ{LTuFmvBCVl(!K0-KOKJ-eS26;CoQeCCl~Z+J$V(T^3=)2vs-=38|8e2BY=v< z;3q03vwbPTib0@bU;Telo9f*+oao^6?OaqXI%)o0Ci4|9hP9gkJg2cKftK(J zr#Pp}`T_vOqsOZrA-D;!g&Y4nv%J$=sG6u2g0Jj%Ojo=JdaK`JWI6miaV^{XJO7gY z_GHpa-z1&u;$v!itvN?do<`_Wl43L$`S*L_$)j*2+xjDt$``=DLl|93NOxu zaqV26M333O&^c0(cqndL9ZUL<)(9uU)>;CyF+rM=3npLp@Vhr)YNpqFY8tC@Y?6Rm ze|6X~BI=U?@F56)z-J@K0~x$WF>)WW0(zym!Sh0+MYJNIsdHf(LarFyYwF;dW9fD- zeh!#iDpo~tebA1t!;<={SDL&Borty5vB|fGHL`Y|1ZSk6-_b7J8aueZwQuxQf%y8+ z(U)#l>N(2fY7Q|<;+A8?lOv&#a&^;Ntw{o%0}E1~|0 zM)qtYK)qp<;x`X{M0`mS{}ZxHe@_rMSt2~|shE7@`q}_s=7zHLqbls~n1LchL#lQJ zyiSDmHa^`6@aD+264A+v^DrC}u>=;IQ!%d&eWEqg!k%$0<#r+qH_wGOR#2FBfZ>-i zUkEnL6`-L3mKIDIpeRPf@%XyMEL&C!<1>KWlUMFk4R38|9pQWE>r>i8WzkXpM zrsz;JBE6d$^?lER*|yac9EMvCkK5mqH$(+e$q?n|cE`9$-$P4?Tb|gxHtl$NA(INI zLq)kUGndT6CT|<#CYKW4YXN{rBAG?^Fp$$AtQyxefJxq%FAEL;;sg&g&I|n1tCjv8 z*tn$TDRa&*UWDp?Oa$Wq`W}HlP-@=BU7;Od<4LQ?|3dU%Z2(C(3*Hs$JejGtYM&H> zOG_5-2meX3{1#%~%8I>}m;Nt_Kt#mM=2XB@O`k1f(hb>uCMFO0%=l5A)vHC-WEYAg7q&oQo!5d(oX@$+Cd z%sj4u3U+RpfuXjxNs7iGi$tAGMEK1qWm55Ii(Q zb}l$F%iErJ0!+O51O!mI-2ff3Tj)=qJ=jcjSN8y?v7l|7ZN)`mn`1=kv#MFErQpiQ z2l1U2ZHuLfKwbtx0u`_XJXcEYz1^Pw(-K&)xY(wP2%|@hazzP}LinHE%}no3vUNLF zdhftq9`z`9fUhW9ur#zZ;dV&SAGqSr2jaI4I!ct(N&n>#3$x} z{-fBA%;e^-(_ok&)MTQu3K*3L)sG1Df0_vBcymn;NA#RC4GaW?8NUWZXU-LcV+The z33RE|BndF4BqK2?!r&ByTlJ$W0TT7Z`&$A-%hXG3^P;9CQsr>!rIy>PAl3k%iJlxJh8+th1;$Bluh!`?~V}>Dc&uE)OwfsZ{&T zEieJg1(1Gw=z%+lvSO5%}mJ zK-ZKItMqBN^sjIoZnDNnck?OGb#~LZ?+gpWpd_2;AlRe}u!Zhlp|xdu_4Ap&Ov(AF#tTzbRfLX%+u?Vw8K-wpucc=%BE_bSa9KvieN{{;Oy z1puXRkg9;?vTgTn0V^WQ2{HiLt16FlUc}^&ezNpR2ZMLt(VRd$qOk{Ho;xh2px6`( z-NK_?Zv@o3j}vqI5wnTPNJ=?E7zH%eVWmLn+uQm9j@M#huTsQ&J4AhBOGP`9!5RQD z5l|;l3xx~aHSg^yw*^#NsK*3j{r0x=h>8Ki0Yd4GQ`mm(M43jIE6bkJi^z%W0n4cw zFs||p9((|zKClESrM)2Ooc#TF|13uEP>G~ASjrxg-gO{!BQDK<1>(mGdiCbFmOn#V z#(X@Zj1OUFs{^BS;tF8kwHo%jBhF~15b$_}-|=oo`JmtSpO?nlqR+AF>iJA|0r%d3 z4Yx;-q6vzYm2*!gLUslR!2in!7-)iGIP!2|)fDeNfiNMUIJNv(2!OG+qd89jU9U(~iV&;{Jr4>LaS^MM35c>C=sX$h z<^{`Pq(TrCs2vpXV@yDw4vZtZ1L>YF{rexy_n&w7iS(ivrWaU~7v9_KCq zR^rWVmR+PT4Do(IP_@t7POl&|LtlY#50KOmiXFGnS!*C9Pvsy;k>@KMb7{C}GmEudoJ!h?alB+>X6s zGX((*A3~PTwa2x+cHb}v9s~Zdf<^9^WW3&-Bt%R2%R#C@aMLv(4bK?guyR`j{5XKQ zjo@}0eJo~$+ZsT)ht_zuPfjXzK#a`fVN}Kzn0N<5OFCKhS+z?jj4jL{47A%!VeSuG zE3NYW4_8ip@XeOj*(Q0AW5>3Go&2Yw%ij>mNBD;b7YM-HT8o>W0Y(}^*WAfwt%CeR z<%^&{m^^0xKzs{(Sh9TXj8TF6lv6^YE}{1;gtI`Xx2wVQC*Rfr;-hLQz$`=TlxWMk zlE?EBpU8p%Rs3!0sH*%~x$vhiQzm+$|G>0+TFri#8Oq2}w#AQps68Db?8c>T^B&17 zhKG371JU-dDRV8f92rLRZ7Aqto#{dl#C2snGcHVmAC*zt$UsA)$>ve|$ z4&+`;qKDsE{ACW)T0B})J3C5SZdLQBHCP-?WX^C%ehV~cMSZmI?65cDAY%8>Cu)^& z?FUI<3cLY;D0_`$<+7d0(A?e2d+P`V=qvl=DSL{A95{rZ3#!JjQw2FI;!2y&Z_|+N_M(e?R zm+YrC0^2J}TpzZczraq#lQu8mDsEBpChe3yC%0y4-YiQf)ptvazTw;aH1ND-p#M-H z%@7cPz6Y>!HpaV|x|Vtl&3r1pVHVq3c|ic>mnBzQ&Q;8L%)B!H0gV>-vPZIdQ96>> zl2C3D8ML(||0Q)ETaDZJ&}Rm#THOfwsrRGFq4wiV+AB*lD&YI{7)ZOv7P&Tio2(Yx z+c*}S@JDt`KB#b^yaBzaJc%!@U%iSi18e;MV7^&=&%ya0kY1gcT)Ms@TQO&O2RtRS z+I$J*DRq-qqS(Ledc{k}wP$@83!_(!NV)mFA281v0Sar>VFxL|H~NNJ5!P=sYhMOw zas|L?Dokf>+i=EY1D!IDsgC42{UlRK{bA}h8F7SbamqP?Nsn8OUdwdf0pE1qA^_@C9DinC1ex_wE6NZ2AVJ+Ey;J5zfj<7_ zu7yVkfekM$UDk5iX$WBVE!P)aAnPhA(Kp&w zO`4Ub864NyMJz*7+$Q_T7h)j*qj~RSOoMU`X7{z}_`hfR#iFzlUkXIGN1J;rcm56K zQ%ZIPBmM<_mM_GhiNDv58zGd@u^-7k>I6m}V7MIpF z;L@3GhPpuHug2Fdi7aR_jY=&?v4?2gruW3|Jv&GGiRk9F=ga3h^Q=EBb2# zp%?NS*Ymqp=JMq{oy^tWmNWys>^aT!XCP77@ER2sderr|W8L$&#=2Ih@wFetHgC4} zt=uI!<5le}V5k0xrS4i;*aN!_UK@WSF6;=^FTvOhiybaDnG8fKdDt37+D^@EWFmg{ zdw8_l?yk`%5xcur>Fyq0H&u04jmS9j%f<0$aqM({_JD5&58lCScPr`u2P<}`$prrS z_RWi;fp1RH?#Iig2L$9oHq-4mvi3&a{>cfGPfjQnqcRPK=z8(n?h=%ULvkTD--mP^ z^_q^+$$VS)tf1w}8$GXlKGciM9oMfpdu=5w@R>B36+c%V`Ny)$eFS&xL+)#UELr8!F zy~LFo`Qo)(!%^V$|FW=xBm8t;MnCBXU^;nA9BESJ_T(pM@o`H7??nd(M&tk`aECyM zZ#F$6^T%uiqqKuLMBP)6I~ACS&|0C@im?mH%%Q)6y$G@YDYG&YCs^hK7RUr1xnjRa zRpsyb@$z(Z-v9iP%j<9b7zM-wLX|`9(L8`;uieT!H_PWx8qx$F{@c;X~q$_KaTbz!m38r5hCW z5PV0L&#avqOhNmfA+IdynX$4HZj%%Wai{di+=5c{s(GZx5R?i*R^6KcW^Qd5J8Yh> z#>eA{1-&+!614X`&$*!og5kz5$?`6I$fnak7Q@lLg%B?(S@{9}!!dp6aOA!4>nzz} zV~l4#z^22S**4V?|#vEC)A zDC&v>nd;8X-pz+skS~UzZF%-fQYzm*+v^)UwRih-ZfKrlko`TT$4>&e{BnjOpP>nT z%lm7O^7<`TeUtWOY*tbzh6i3}z<#NF2H9#q#=Yu>%e4zRwbH@%C*f;<{nA9db+A6i zxg{`zo|Ot~-=n(paZ&)Ie5@y=;kze8?XAbNM7mQJM_a=?D}b4HV$ zR4;LIgrDJ@)xtF3A5nV)^eG9tnn(4S=;$8Yv2WT8D_hxZo`glV5Gjf6`5gL3=(c_; z_n^A!Rgb2LnI46I!{v7ZWsl#|pZtjF2I6Vs{W!Ve3v|1J43|mrlB{g=0R`xb>pti* z&!_4pQ9iLXunKM##J$(+!S!+WJV{oZOVdoeJS-la*PgX{Owq%{>N0k0+jhKzn>z?8 z<$D~cqvLcB(kO$5Re>VQ@pwDe?fXzqEZs(Vq@qNxZ>ev>L1gD%O0F zO}-d51gwD#k{+*(XlH$cDVTAthc0bmQ*EZN532zibe+l<-=y2}Rv;qUUy0UDbdZAn<;w zrqM6ZZh-F|(bfPkMFw6H0h1QFr6A1*x{u)qEJf8$%+ukAB76Jj7&?b{gz_0zUuZo@ zQ<2%>b>cM3!BndfG4$xB1azxkpE?1Bvzykc*{++dZK*AKmt@8W+%z@#+})>9LL|S1 z(UgjVJJ5Q1{d1US1g6WYM-YRydL;5TLut)KS`YQ}0>vA5RR3?f2e0j?8f{jBQe)w7 zxezqDbjt_P#<{NS&paPc_?3|Pxb~Fv0VUc@=fc_Xz*rEQV{qzhjDZz7z3`=ovN|8d zbV*746m0D0_HyV3GHYd}Z_nBce286p>em~K(>DmwBHv62GzlFYnqiX(pA6Da@$%P0 z3q42Fw1{GyN1J>B=~ zT%bF0K`n*VrJ(4m)fNF1g2p}>LWx%>cz+_HXW~gj(afD9p=#PSra8##0zlK z#|u;GoW#Y0s{$h`@XOzp!P8Lb+}vtfk&~i?3iSdyb(^q3FubzTj;_rP9i4XYi+%Wx z%3Z#(k0<@WzDgqysFiH1f0a1Vq>dcP7cTEfBB2L#WX}dKCq+o=48TiBKvlpC&{YvTaPicbwcC1@n}oa>$C+(W$bfu>*F!( zVG3-KdNz|l?}ZsD{r6;XyZSUy80`brZMF6v|8e2N`ig9t{6>1Cr#~)nd=u$5kYDHGPT(e;B_^33Mxw1mK`ZXZ#z5<|G`$v13)*fs!!i&cr}W z0rWk?%U|azoo$^`bYIJOR!)6nu)T{u;4~KFi&<%uieqpN?2!Lid_+elHh|=IFLQK_ z>sXyw&JE~Zl?w@N?Z`65h#r$?j*rBHc$G66~OL+67`a0J; z@Ef#k)`rA+o=RIrZwl8;=iLb=tnF3qjDD}XjC$xexU1+MW|Vua~|VR^=zQe+#N1?7|it3M+;zuBo`+yl|nykk}H&ana~& zVW{Ufip`YYh)rEf6Mg1xuUG#`NoaS|!j(G~UD2~0jHZzdH`F&HiGMn-MMy$*75Zf6`P{8DsRxnHghDpHonZoAxN=)5;a zt?77}5AUq#V(%yyWGtRJ44q;+Z4*Y_;raNN)tWbMkCbc~aG1zk?DCYfhLzv5=bG9~ zy#d~Np^>c`lQcAp>MCDu_WicJ$Tmz7yLfSOY_4O9+4^JG<}}Oxax<${%L^9-m&Zt1 zQ}rdr`1+`1BH@m!@G8lYGj`M$6FS_!%j=cm?@h!|GS5s4a-h9v`o!nyZqzh*=xM-& z(HW=lQQI*?R;`0LT95k1De+@fTk|iN^o3%4WdVF6uj7jEunm*vH00$d&uk{dOshf1 zUeqfa@NN;y3zXc$w(|YqwYk2%Tb5_(8FbbxY$OD{3YO;X$5tDf!e=`Gz$u8^TE@P8 zB`wa4L9&)v;CM}++c~}L@3FZn((cQj=&^K5#Z0vDH%J-18~HU>2+@I-m}2I|?sAWf zD*g5PC8|QZT0+m8NYV8+{t5+L4_yk`+Gf%#onUXfZ{NP7`h8vu(&fB~FRlip`}4qR z8xfl~=I7qbC>g%LyLx4}J5OGj#r)e%g%ib8n(Vl-lD(KPT6ky5eNl%YIh*&C$;YL9 z#;z=>Ft-a6^H^Kee4EtYJeKBl;(c|`YrhdPsW}XWq8Z&btm!M))Ox3!lE;hjZPhS5 zBAdpFs!o)l!Ro_>8?F6``Lv0b6_2$|BioW(u<`Qn++*k1maZy>gl{pH8YI>Gr|_(* zkM-o8Sski1d~7N#86m9Xb;thq$>QC&KxD&=b(`nX@U{sSTGrE@xr;?Vv`ned*U;ny zJ{Wn6W4?^0;W*spCAP?EBR6PTa>x{^-my-VdeSh*}? zQawE*ncTlX%6-%NFNH`9dU&u3ZQ3y2(sWB9wlq51J|EAe)S1;{WIA0-2tccLnl`1WayBvGx@xDJ z(&aPrUoT;y2lB&tJ>7ep2-1lT#NwM@eTq*Xbb;Cac7J-P>Z?!ScvfEkNpE%JqD^P4 zQaqP+SW(LmcE@Vi!Tb}O)GE}6eG*7avg~)5r;R&Ev6Y*hm6%(2PQ`?96*M1g%u$N2 zbNKhMw!^Y(S+8y7606Hi^agPOr#=4aM7Hd~nMmm$NhZE?V#@wtPPQrpUa z*U-kN{=WlFywh;_WKXaeYBBh*1VwDW4E@sL(341YkaXs3m@yja^|3BZyIeT<5W!{-P_dfTzGxxbF@+eJD?z|IP3Kr8^ z5EfL#?TuWC{n<6s+i^(L${L#~C+YN(DM^9nYRb>4deP~Q)>nrvbLwL+OqF*{9na^p zTL@*${pyi^b^3X1&LU0wTcPbR=6fA2CbzT&xY??phQe7cd4|?9?l1UQ2=4NcURlk8 z)knN%j*DX!q>2@t!JNpfNmWY|gTDItP5)xx<=KU48j}v}PI)Qu?m9nQlkc(t(%nSeOM>HK?c+w*1zS-h~t0VDoI zds5Gmf--9f3Ef>t%)Y6Svyj6S$)>y;oj@U^x3_C_D><(&zJqWz`?3uF7S>#rm7c0% z803%u)d>p4`smEgxz=d{<)nC%+(ZYt8S!58u`}d2spYgS@}m zk_?0?V+pXoC^Wy3&tEH;mBW~v&z?nT{MHg3wDV0%o3wcYc`nR${XF!g9Z|PSW-FR^lh(g+QbAG8GlWL>M1R>hkJ>9prTi z@|hCHuo_LAAv^>phhCoCU)=Nw_6-_GF_)Lgff4%lsp4V-#%o|<*AcHBu1zHFW{slY zNmf~7sJS5o@QcRZbUZysYLB|QdiGw2rK_3$`B$ACn4@GSMDe@+3#S9`SM5}M4B=!& zUhP(OM07~uCMtqIz6d}*Hs8;lRUg;?CV+droVTplKz3r;N5iSFxxW3?w+~nCnE5)S z+TdPecX;@TnhEhe_rrt7Uy`nsZP7CNg@Ls;`nPCDe!laU(n?BM)+UoM8)r`sEL2@= z*pqK2j=q0Vfda~;2P4*YleW%h|M7I0*RGnG`#lxOSm>cAy^~5~_8S?!*BfEeneR)6 zJfF;!i83EtJ6iJE>b1%u+~$t~E*e1#-zR+7`}02a^XG3yzTQz0T`7#|UPB;_xA`)= zy5vP2)6w{-Sz$SLPH$wZ&gXB#YSg9s4nW_8zJ?H0`Pt-re(m|EwdVWm0Y7M%pW1Co zCN8O)K6kKJ^1Z_^kXo_7@XS8bSA&5AduaLFo2J-ZE*%aA<6lzD*Hma9F(-^i>ZR}} zqiaUo*oXic3HIPrU29H&93R1w#z3>x8!d_+Dp5ez`9l7YU#xHbz zviHrkT{a>;pII-Q*(!T`0G)ieZQJizIv75f z+kHu~mT6OX{^Bpo$-2m+c@!#IN9wBJy5nrlNcrESAT<%it_ zrE3T<{m}++T>&@Yi%%pr4;-}ynE}1m1YGDQ_0xf9vt;R5$bHqx8RF|P+c|A6^`J#c61ooeyovvM(Ub%)x`SJ{dh;-V`?b?M4 z7gEgSwZL)Mknh}#*y_^%Ii*VOkcQ3JgNTTu+W^u1@J2@;|C3;7HHB|J(*#H)(gza? z#+0z>)T6SQgi--=Yhwe>cQ0-#|KZH>_Osw7I?}mH7-_7vcrY2eSw22&1wE+gyd#jJ zZBLL2|4F+`TzP=1TY;=QSxg%5nXX-c9)*y8;2N+KoBM`$Cz`Jyt%u~s7Hui}x-yi> zJQ!>=1>p2$G_gWTNiJw=x&}cFu|rP|0*`1-;wFu1?8e ztA=1~GJyk&)ZcJ!W@1r`RR6i~f4v%g3Uvz{z``RU9*22))uS^eTGi9vaI3oIW~a7M zYNxNBAOp)j`H2yaMIK0?@ES@4aCd$2fpT`P8Y)`=Z{9+2SOyV(KkXDPH+AT)wJ*0N zPZ68^Vy5x#n&2c%@({Ia+`gG$o-^aWU!7=GuM%|{@X#(lp%1}>qlO9!U^Fu6cNl8F zGb3-nqlIlF>^9v$P7Skz^tHah_@l?2MZ!5(q9NbC{@?UvzBRMF4m*~qG^2K>0X2f+ zWaYW2;=AWS$7f8*wZq}C(IG9*-`+sVwC{Sf{QLV2jyLb6*d<75m0BZa68$*FtT^E{_AI?&cY9iJo7_TNwK*jA%WL-EyMK8G%#wEJ4yF9-jn=~K4Lof;qJjE;wML@Lge3h4g$vqssio0GG#$MXb6LhfQ0ERC<`o+^cmvYUhfAlx?L6M>& z{%3(n>!p0)mj^?m7p8zn^{7wP`kyDe`7d1$h61A3Qd0+4HEKA8i3(re&@5}@Z;DZf zO4_2L!EhwiljloZG5%?qvga+5eVwL?1zoo>jf;POe!E1D0}J-rcQ7QVl!rHC0@ofSP2Ms5CL>W!^;842O-k)S>jrW#lLPd$)4|}gr%L4#SegSBzLsm`rS7YIH{K} zeWj(f-RdddLK5|6#&&Op06xGf01(b<;&K=Mv=IcrNkNn8Ky|@7zu|GyN?ha+}iBbDP|u3`(Vvt?g|M zl}=cpU*h5~es*^;c;m4*yST4WksM4}0#@RPf-nLF~I9uacC| z|HUEg*|7c%re=CH-d{z2<5h*w(}`9ldbMg4x$?GA+eVydhiBWwzA2!OB}Z53xF*UT zS*N6+Dvx=}Yd_HH+aUQo;ulERW@;*b?CdG@ABzDI56F!$NCROLrI<4}Nqqb$z4hnb zynkZeyzxV~{G}zHlU64EEER*bE;bUgY`v1SJF;4pt=aEbj9L88X z(CT)iTYO#vRcIs}O~v_xlNvA~I z)3Jb}>e6R1-PeY=E%u6N9={ifpd=Y0hR55y4F=xt{g+c3qM-Q*4#Pd^8s^YPW-HFZ_`^PCwC+o zQINQlj6tVTp%i2<{54va&M8 zTuVtp_Seyj`%3W%36Ij!*8QWVMreF|NZYsj)z;6qb~-^D%0qa32ltHCi4!Lpo0?uj z7fL7e0{6zpXf*p1w`%W>T(ulp7s2Zkva_?NpHQ>2)w3~16a|{!?mBRwCVXBxi^Xz+ zD|d%%Z0=3Coj$!sVep)1)01OFT22!4U7!NU`etU{h=^!zZniG+#&5*aHlyyy0_Kxr znah^Q>u%YSma~%^2AwvSK~J+t8jVKk=z`9ZyPzLDR3+DpkXGutA4PwQ)xfU;Wg^}P zHj`sp@T7{^S3H{JF%T>@;*X;p!Q+!g+h=NB;i)7|;Q2FlPrIBrGO2Qx5*ZNzgyTB= zkN$m&S0?RLfmU)uO;nODfQJ1B27*lXyseV7fR zWw|aj@r?zU85w=Kqp&h)R^7wk84lUnviQ^1WuXCfAYvwY7B}A(Ort%J?d8^zu6nOU zgB`3i^Y!YZ!otFq+;>$~9D3KZ_h&BG)i{~YsK#J2`gy#D+Ds}n8ai(@20CpCdOstIjgPIfr|UdnT7)!?j0os?bqG5 zr$C>}^_rT4X9b`!Q%^e`(9B}R>&ikyjTLY*KljwznEZnlrmvtkAstho?hFVBusic& zo*O6j?p-k2U91q8Ms3&H#G_oGdroK#I6^VTta|}P7wuigB(_m*=ZXlnP|J2fQPGNGEZNWc7j@GYEoG_9JUL23@{*!(l~F)HTbcft2t7kb*jUN;R33u|m?N$-e$eM80-zrz>y zlG`8|9=Mf349hPnVuFwc+P;O}9q}s;d%C;7!&cc=ALpr%?P8wz!82u0A|f)W41)k~ zFE1}j?DJC4+Guc3G9iYthRij~HhY=$^z`IH-#`svpZqY4yWcnDU9GSLT3phTq$F;d z{=|~+&7jrWAqR&p=aP!j(o!_QjW*BL7N9lpD0)OCjbys9p^Y47-mD@4B0{%ToPqXj z*TJwgxCMoUY3lQA?Clk+V%5#%bTlze z^qac6XgQx!ZS~d1;2XJuBMZ7~4F~sU4i>V#t7vPxzx6fH+oR2th?~9Wxw+Z}r<}Os zE1&~ql9erHyNcIBjfT$dX*GK6$<#b>L8oCL(4)LkA*fv%Bn{ADQO{Tl_(bDqXRqxw#UwuqXi~ZU}~7T^(G zMbp}9>-^J)-04&WKn))q`^;O?_IosE^#$Hzfn%cH)aVLp_YXz28~JAoYelgoeb-Kkz;j4Dd%ouOBo{bj25H`z>s$ z&hfq{MBYy1VG~8N;{DnVhP=XywrHt=SBQNvx)WB6twrktV35Sve$)gqlFpXB$ew zr8=U(FXN0g6L`+^dB8$y*gQ+A8md+~Zg`a|5&nW;gU-)QPl)|Kv{8PO#F-o&z%mw|2=SnOP@1byMpZb)&xqwR=>r$}2x1S}0pv_9?A$cv; zoV*Ty)`A8~84ZG+L0}8Z(61LLozv=znIo|}6FMb7U=tviIdX8Ny3f!R01QO8&Or(^^;Y_71Tnw9Qxdvzhp~oW##hM5Fd?C%-~89c zj7Xd&)i9s^EAbxpV~e4w0*^BBwyKJE)&eX|MRUFli9})%-=u-_?b`$`8y&z)%#KCR z#uZ{^Co8Tzh9fMr?-HvaX zU+^5E{rGi=*4E`D;y?#o4{$(z6nG4{7HLl!?|J1Oy=QQKb8h~rtB9v2jx^a&W=D(4 zjm1&^!??urJ>T;nBKEaEle!K+S2`D(RHLJ=j{k#g#GN48ySP$yJ(^RzD5Qx#Ubu%HtIWb;rL4rLhfXM#4W-z!Nu74+$8_3Vlm z(2BkJmv~Os*!lVSv5JUqE!Ml8mW|N(4R*KWgcKCAZpx30jFbf$x_fyYSoH2&{r~|i zxk#bH00D)v=8+dtB(3bqP7@ANVS)2PS6@MAQ-sTjGt*mfIGmsyo1JM4JaXg+IrR@6 zZi`Y9-Ew3Xkw~N#xOPUvsC6U+GR8^T91`2ERlh;H( zV4ArqDpl{^(TH{6Fr`Y+{ED*?tyf@GC@>s7etb7HVD66=J=z_4{X5`4P<}yU3ASVj zN(cbq+yLQ`*RQVq`3Nf6fa+oQJ?rA^Dji{t9?LwA5Pb7Q=)~?&rqF;i@lZJ47OUN7 zw1qHg$ znxEdD2j7Uju1_^5z`q#J?A#3*On6t3pmN}x5@VY0lRk=sT5SeTq@gbnxk7_*l*f#i zSVKepp8eou@)jrt-)C~5d#oobkefQDtgJ3C9OFcb#zK%%s@sdShyjPVI2=Y|6-$|n zITjJXX;*^VIKno*9P;d*aTpfJIvW9#pu-XfwHr$P;LH^nC3xcte;1Jk1 zZrot6dDudMjXw$^VxwDo-`!Q4+k()T`>fk=$1}ZPt^Zml|;Gk;0(28o` z!HEMwK+i7tlrYG8SVD-_hq*ow`T4ET4>(USC~zI=Z{nNBZrsam0YID1E5ZQtW>Jl; z0UNL~pFEk^OpQ2Aee$G#lp*3W3W|zEp}yDy8{~gWW5b5MlXlj2cJg|i!Gv<^PFOcGTpzVNvz_eCxTjEK zK;q^A9{?YX*Z}CS&ICQ2hZz|gve&fDbVkGPPs5#S@E!pr;Mk5YiA89-CImJZ92CBJ zln8D#1Dmu1_oH?^YY$xsB=LO79!YIpC~}S{elh9-N0mzK1iQQrZ9COg<94wbMdQH11Q}8we^~nNoWyFbm><^K95A!a8#YTb2CK;QyxCYvT!- ze9xr8X8}gAyFb61QrMiQb2JDC`>(H%?&s2yEg)eqi8TYyn>q$xwUNk!eX4A3+00Z9 z#RiG&k@a0CjpF&PpqoLQ@Ql@;733UnFp*ju3&=q1V9oY>{#-jGyBkUrGQNGamI+dt49*=qC>@`e> zm8jBtqvzTBzK2~1fd*yFtpI{q!9krNuFVsh$|FnRSNW>|e-c2e4bQkoM4#Y=1FSGj z+fqhcq<;jkJpyI0uL*6>oB@NZg5(WkP4jU@X=X|ExsVwLjW?f51HC;Ez!S=KOY*O& zV##|@s&x%wvj!I|{dM^?yF8U&HEJUJZWCPzIxXpd0(~VkSdVMw3l=$y?WpgOl+e0V z0}&hAn75_!yj@`#fg}k!HBB^B?gqUmL9RLSln7A*PX_D+jF{N}1c2sW;6d;-eOC~+ zi-?Vlm7KA|@*Q7Xum+yRoz7q|sohL;YIZ6263i>&5=MR!~&1lZ6GdVLn^ zJ%?Zq!_`xB1!`8-O$4e6*=%-LnF(M4Y@e}UN@`t4qzyPQ&a!)uL})v|yVE!d4@KWN z*cwtTsPypiiiC1&V_?wvl%+Z@)q5^WkT=>a10-)o^~3#<#04Fm6}ag>OZqhPNkq|( za4;vRE~IF1of&2ex)~$R)t!}J9)|%`&)_rc*x+j7bX+aOGj7Ag;^x5SfLvj}aqU!f zQ+vBS2!;7ieO!Qx0Ld_8kdIY_zWjV4+14Pr90V{B7qvfOi~8@277-w^vB9i-V0<81 zz9n$5Q$aQH%#P-|@T9s9^8KfCWH1vVW8l_!+xjB_7E34kGu?4-mlpon-F-t- zAR^y45PSfcYEDLa^7SBS)USQ=<)t{Ikh6&htdeSZQ&USP{USFpF`!1e2A_U9Uz^U! z^WbB5CZC_w0N7qenO2S7v&=N$l>M?0yEG$*}?D>?>)rO1P&(C)dZ4r)39;!GPSGNrdoEL5< zP8q}h{sO?wk_|xWIzZ2SpM#=fT%Y056`%WWt-R9)5=Z?*z>LNB&s`zy+jG}y)W9BM z?w#1!b;}mth}b&XN|0iC1T1|BdDyOP)wMxZmagtv&yKkdivri3&VR=c2O;8R*_CVprYn)v0)Y}` zgRr~DV3HJ-z(fg{_nQE)N=Te}DlWo1{)PtBZB;&T%(M0B z-vB$GB|{<`AE$%4S^#1;1z>-_aR~GT*Ah33p5Ug=m5Qx?+5$Xpd?AsW6qKe2imLLB zJzIps!`)Ga0_2;;PPj#!dV+TWuOoX$Wd(q);P<_yID@MqaFgxzt*x@)!&cn^JoG?( zAJZ|S3RpP+3ka)&tzii1IK7Qg0T-AUXb*`NoW+JRxg1>qDQvOW?8#~POn0Vz2DMuh;w1yA~BN;up14sS;7etHi#A@lQh>n zoI>gRmoueYaIk-7EQcN}U+6Ce_AHjTr6xXut926*0Fb;BfFMLc>lH14*L`7;#vVg% zrJT?HMSj4Xn?WQseHHE>4%LDiU~c(6|45}Fa_Zj}J{5+sW%-pSKq;VXdJCaTrGr!2*9zAv78(Wbxk03zJ6tQ%>+UkxDM}5sgBoGQ%6;t*upGT zIQS_C7ujS%mIz-?A&*~HlnA%$)qI5ZK`VMzNEvcZe|Xr)K#}8e1qyx9hC7@V^dkUy z*+&2xMD{y(g3tGDJ$ZG>o^4!U4F_AsFXpawa=@w~POF;+fs|DxqCKRHzHaMJL=5-8 ztk67w6v3cWQ$P2(l<~KN(ITL^^!a768R=1hl3p(_Ep%g3858cX#})Hiz~1K9ZSx;j z2i>#Dc9_tr?-P1yM5Pt9r3Y@#o8#6`oIkwStxm- zv=u;lDAB?t!V@elgNcAPD;I$sen2TuQQ{E+;RRv0MP(`gwrsi=4&-(_qMD#~Q)qe; zP#N-4g`m{gm@f?bovI7Kci{$=Pat(R*bsvD@!@D5k4J!dU514du#ComtX0@)Agd7C z7FP*DpM?0kA|PVpkz&7ta760TpB8)Af{aHH98Zu%pSFF`_@)bmv-N6f=3ZUrgf(UJ zWfKw-#Fa+i*dXmbMw#m`_C-6z_vxSip#yP#tgVg|%VKts`STGrc}b-D|L6Rcsl+x{L`mHMOwC z`+*6^X9(9Q*BS|{^Ep_OWQ4S|!`MriXNaC!!KGlNfBp)(kC>w9mMohF{@J^;li2KF zBM0(C6dveFJ}?>o>^~0ltRBokc%xX^;Fh?49{%?_D28~Iynq+SZS{3R6;N0)nVOoK z4**;cG3eg(M9en`btM<3 ztYMoZo_)6=Luk->0y1XtyDzsuB*7YJZ1Lr{#a53dHAJ(-t)cPp@?f7l`uYWq6qVj1fD~q#RssbadQ)BlgH$HG&T@d?N`8;LOF4A zq!fsTSBOm=Krj+uu06#8Dqo-ke#QA=jJ`cF;;aHy~#!9a}0hvzO{0e1dgl=KF z%bKl^9b2x+7;`G(+>MRh4FQD}b3O@#c+DUPw+D3*B}qw>khQGe0s+>cuBx9g%>i+u@jNCj}dHpo)}yd5UZvqc*i9DFqB z6~2m6FmwW+a>&|R){k4;K~))n>Kp-h*ALLL864HoL=5n-cF6E&gwMj3+zU|Tw7F6u z5&G7xgd++xqjKT!d4MeVelMsULquu}bdh=*_kW<{NdZ&j4Xp1J=*jO7!*-cA zi^S9P{7lhQm4oY zbgpKB(QW`&_1_h%R-2)dEWUYjBFLI~bD;5cz?_xX`!5e42Cc)3--^CTLh>|R+t)UL zb|8&W{6j1VvKsmx-(8Y zBUL5Sdz_Arn)gloe2K)D^hjI?$eFgU{IfC|Qs{3jQY=##%|tEWhg?`Rmo+ zcA~p^a^6!3aO0!F95>YU=_xwNdxOeQR#p~_PxOyrETZn-7O`7EZ`-D1WIOq&IP~oj zX=z_{5*jW8{PhT+C)<$XKP%Z4Ek1HBjc)IhPYFHX@KV>GA%x1cc-*fqo$Y~(Sg+v5 zR0Lfx6Iz;|s;#Vy?jE%VdIZ4jqXu316+_3va-;sTF@1Gu2;&fX-#TlL<8M3FA2NFl zr~NrzK88kpB|okqqmBnlaFZcZGg4Rtnm=@BvH*KIHk3QoujWJ=Z_6OUMHXqF)OX2Q zlY6sZ23vFeTHo9uBENMN%yF>j)9i8fwsPymX?8*R(W*7)Bn#KynIf%xrF?` u-%wk>2A$wq|LYt4-MpOr{7-tF`SG4L|CevM$DAuR*uA?fOmlWPUid$X`7LY! diff --git a/src/primaite/notebooks/_package_data/uc2_network.png b/src/primaite/notebooks/_package_data/uc2_network.png index 20fa43c996bb7c3bce61778a7aa233ee4b8852f6..10989201b9c56d7bbfdbae05ef78e9df712964f5 100644 GIT binary patch literal 70945 zcmeFZdpwkF*Ec@esZ>%5Wt%oscFBHQDN0Bx$u^QCG#GnEdxaUPNVZWZB4lsS&L|8L zBik|<3?>F+?_KAhyX*Zu?|=96Jn!%QJagaI)zzHmoab?@<5+8b*IM6o+&`tSy?*Vs zwI~#7{c#j5G?> zZ-qiBKSiNzhdVIw(Q zCim1!X}&~-XWT_C;za_dUn_L@DYTWmd9%I!8fTB#${(vfZg)&r=X~k#*_0!X&-{2S zbpPdr2YTJwM{IO;H}BbXhc49Asx;{p7CrjN%u8K}QxNa#l5XZ@9_PB@0x6Y@`A*9y zo%0^GHm(dB?#9kuX!WZpT?yl5eG-q1(PMdpW-Ej~k7jLlEy12)Zqgw>KZ2&-&GioTL?28X66MLK5{W*UucfQSJ z)#PuiyN_T}y`Au4ic7`ylU$>)= zHJbc+CUL};jyKXz4zjYTyYJ=L;fM`vW5T&dx*A9>DL zEiJSB31$`v*_6mfGxovmlYt$oj2JF1=f|4<6HS{WmsbT zUA=$YH=+Kyy797cG&dKQHoc`(gSLP1H>h>>btX$9Mp;cf)?B)W>E z7^um@XQ$?x6^s8V%yvR}@jp>(PAyv!mQKK?)r((iF8iNHqfq}N5M(?2U%5lLZ3Py$ zs?|Xix89tly!q}l8|h_9`jOF?6=GsbDiB~X(>t8hG#YcasUb^+CKeoD-&2Lt$oE7S zW2Ww#Xy3SKw6yr6pcpajm69bZ%T|PR8HoS5aqJ&{pE@HQIIp zYwlL1@+`dkC)q;DQ#yUV_03vgO0CECInlXcOZiIz%kLgtQnsJu5*H?!qG{^WC{t09 zJE+pBjaRURJ5Da$A-uz)Y0O%6yemA-CNxvZe==4PCX{AV7WzkdwLYbXx0z^LJ!VZ) z^dDEk-?U`m%Eao589&ngzM{#a66dE*TP?lwkFwVuHHBetS?TGwUjOgd2oJJ0EWSOoP)M!)G3Itx^z{!f6bm|EfPhjG zmW-bChmwhIpDbKxv@Esy?6Mn9{Orq@e)ykg4g(U>mR|E>XnvaEfQ-OqcZoQNz5mbZ z1;2e|wA)UAjhFquVN|T$CZe<_T3TE`T~VCIEB*Sffg!ssO>`#}7XFE5{C48|rM3Rg z^cL}Aw58X)n!ZOfwnaoUu(Xt@{|5i{_lSzJhf+T?Rj zQP;HuVwSAA*v^$3QRVyoJQdxcAb1AFX4QIn(ccNLVGRPGhn0b!hDQD|Wtt+bjIj_e ze2lS@!fPzERdLoa`LckDv$J~7)29dfCemznb~7U+Ym}F19ui-Km(o1~%5GvAHda?V zu=ZwC)4HA2&g#2q6~W6aOQ%WEG!w8<6nNv=1V${DP;ki$Fncl&45El+^_&H*=lc5to{QxVtz7eJp3QnTTAQs zadaaW?=Jmkla2q|mB}_?4;wu>Nqvvp}wt2SXJ2ZKW%_k43 z+zgf_KH(N+X)0M zQ+6Z`D0+<_M}MozD{s+P+qL`T+k4Aw;{Los3Kyv;LwaV(2Lq{6UaM(wCh|WJ)qexF z`*&#SUwG_CN@^-?rm(nJb*rS=Q_KsQDY~ntDQ}WxS2tbvBnIRC25j8XB5Ri1BkG9TSc5UK z!Pqhev+izuK|ui{@JWzV$@w+6cPegJOicx#sjre_zbY@|j$T?p6or&mGWI4-RP(*k z(ruf*x3>#wO#i4!c=bwre5h?ZHO9jt+JCCg@XWzt{AyEE)5MoAcRhRd%zAo!P{xiF zosyOooRyU&CLvKxqqelPz+Ch56{&X0%F3N}+Cm{QDjvsdZEd4{N4^-EnYB{Q^z`(G ze}ClE*7`Iv$)LQe*RR}ngxrybp*3E)di6nIuUQQ^b4su*De%FA2N&N^e=};Eo13YW zH*eky$IVa14Sy7xd&{uc@NP)-CCx!>hc!sX@WKV<-fIE^0>iDYHJ^m7>xysZV9* zId=$FS636}aOoZNXL$t$O7hO_tzOPkK7@5lKiN$iI>^;g3e-zqnPX(KLs4R)sG#T1 z8%=r6mRefh8yhcYl?zKK3#GEhk_Xn|XCVA*c)8qYsaCSF-Q_!f!z>#L%V$ zjE#&od$fDm#o2~ZV|^qq*TlAY5qy|Ku;?LUG%`_Ro}r;_V`Gz{k$3UqUBQ*NhiZry zQ;jol4XMT+tk=d^dd#+|xDOy}n*Z}Ahs#jS-SykO2}0}Fug`Sr`^MIxpgl6_nms5O{#rySzN&9UhA&JGPxW5;8Mown|+1 zKB+LljtJX^M>T*{LxZpYZ{Ij%CP%e`MQfA31U)&IZwt#x#@VTQzs==%>+a;J=kroJNxg z1hvKtOQC(|(t5K|o463|_qXIYl7ps{vvF|47&BeEzn*`0#n6Ht1+O??TW)#xWD}RVQPfOeHRM?l~+4WExxvj3LiC3Y`&FJxyCm5Vb zo|CzmM;m52sSQNd(9i(8bQAo6JjU0u|IxX@oVV zn{P)%GS+^r>8%J%)TLbN!n+jm4jmsKAN;pdU^DP!Yxp;nA|WAx$eWainQ{f&HNF9-Nui=STFhw(0XQuO5e}fmj%h1@EZ1NTPvRHxt zqSU}uFTeMbx)zq#=fcPfyV7cZ;Y1ea_+(9o|4>gFMS#oE(J@0?i>d+EFQas-Kb3$p zhgp-F0zC;yNxL(YJm~FhwCExax<3I{!p;|LD;^EQQuAkmI(rBEfKj&ui_tHWTUb~~ zUdQafVoCYa^iu43X3DfL+00chgHo8Doad8|Xq^MBdOv1;M#HGbTmZ^2?h_N_Tt9?*Kzrt zw?n1O-?Zm>WF{?xbKbAM#lhumMs~X!R^zO(F>|WOeW0n%=9=?l(L4z(mwz13vBqCR z_m=#XFq=m!TAMV+ES;Yf+q<_PGw}Lss`r3$UDS4`{O82zmoHxuaIlht=!4qKIJzDN zgGsxP^P0#Edhnn#t5T9SJPT8{kd5{m|Di(`w|aZ_h9C}87!27cC!@kWe~ zk4MMYq!;u&vj8h|?#+cKvK{>msSM>!-Qz9J_&Xdt<5NxA8dG)AwCSOu3@c{e?{qnZ zc265_9v-H0(lo*0+O+|uA7hq|)u8%-f*8La5p1lj4Q0%MQsA6@DccDpbNlx0?SzLI zk|cM<&CgI7g$f=Wejaf(9NKr6obS$`&T~D^%BAU`tMKtBVmmj-@m?pT3-wTC?d%NSEZ2rx|<%WCi;ir22)4mdz?NP3? z5AnX2$~xvV@qYWMQ#aTJdp&$jOicD$6zA6vVL@CT?3(2FImnBhXJR|(6m$HIKDwOG z`J+dV>i_C(cF2&p(jFKXdNVPpWpdl-zvGeJsOk8-^;prv^eH;7)Am1Ar zj+&dB!`|AP%s<+jhRyg!v z(?iQ2dv`F^TE+-wV&*~8FcGh|Qf4t#O^IBi#xyIX6qPc+9Gj!BTL5fUXZkIgdP{B{ zbDmQE+W8iyMT*nV$k^XT%*o9ipDtR(xajZYU@le)X3)d_6_CVHS`y=4XoilcpVhmP`)?As&&)H0IxCKgpl?{cK5%2a|ct z!GZQR_TEt8){7@}b$uS6^_I2&_Ttbi?#2UKe!eAOC|$mORAKa9XW5PAN@Ur~RTVU9 zA)cmgV0>#|m&as&t&`1Kp1$>ts*73mzakxH&TT(f@K%||%Z4!(#>O;0CHSH$?b z^YDo;-y#brNcDx+9)s9u*S7X{i?^F}m|+HfetvN(-Rs`v&f#t=59L$VhDH2B^Dj!K z_K1;DePE4IMa+f~#Kk?8wbM^C%^M2j(cG~rkMZUF!S6oJV>!~N9`5{{b%-f&Bo;e2 zd9zxr_TkAFy9Tiz7vzgN^3BigW*SxeHUc&tY}w~hVtn@OmblpgI~9%FaZ0Y2?FL(VWPbe5NZvmdAT;1} z5D(QX<9ucUnKKRvIQqW5^r|*7GQx5U|%zp&kV}x%HEF zMMCwNPHlyP42+wb7VL|vSTz-ZzSh;xPg(0FZn|4Fh`hkY)8|ve`ekQlXMSH&lyZgw;7d9q%Xe}ln)=eg@LrwXpS3!5=FDS;i1VlI zLh3@8?}mn^rrUFJa?sxmwU5HKvU@KQXR~Jr99=YLrE!Kun-Xm9M9c9RijO40Fulm!j2sYB_-{5^Ep?~TDYD&b4CloHqt%fJ*ruz^-)ssJ*v~SYr$ZAEU4Q_!@K>eBQpT8iP+t{ z8>rwOa2^oywdQxP7(Rfk@1IRQcNC3A0Bch%+6T@`#k$nt*M~cq06(Kg4u9HS zgCU!X>}VIO^_`#Yprg~iUIeI@32-%J>7bW)~cmf4|Uy)y8gpmz%BN0+EjUuNUs zSCCc!6&1|LD`|(_D3=M&TCnIf_i2zeV62W_!>zQbVmFCyt>R$|u~D3QaN3`82I@}s z)rQv-gpO%K{$x)S<@r6q{+UyG0J0@ZXGT10eA?^1+^V)1o7GLa0@UbrCBXzH$`wku zI8}dZ`u6EsevMS<3^`oZTHs1~urECjz}9F~24o3bBLZ)z|CR(B{=?8Zo^zlm{dwLP z_m(Z69~r3g!OYO0ugnYA^1H)LUDQV{K*&`9+`ud1n%>zXfz@a5{BcbBKw#CiFRg5gwZ>t zVp3k|?BH-Rb}D(cWNTsRhOd>C>}J~P%)#r1$e!F|3j3Z!=j~@eT4WnQSv-;?cz`&C z^Yp0qC8Vj-XU=dNTl!(5A*e?KWI{3~4HU2Yo{2M=pkn?WD88=_Q4c-7a}-^$u)Hu$ zU3D>Y>REDG?&~SD$xuq+~e@@%L2=JyZ>uw z0RK}Ifwlj)RD1tKEvWwocyWIdeqI3M$$zeuW0*m;2y83DKE&{^1(Tll>9}miwmsFp z4v!ym+X=9*`om@t=DfFour&(%`{P7^sh=!Oj9(;zKqqAWY%As4yj$_ot;|D9uGk8d z!LQ0Yf1OdfKwkQ_dJ6f8{5;WSco;v6=9fiVH=?aH`%f&Y=<{yzOro~bw3pF*INQ~;t+>>`n;MO`dAWKcuQS_ih!nRcXMU7JGgw!8G0fjANdHJ7m zdVoJdzy54eb>gYU;pK_jK6CgNBh#B--jwJ`k0AZ|(CY zf{(|$ckezmzA?3dazb5WV>l)aod}LvU#a`CV&98LkeXLbIvE*;*~!{60fvb$UMPl> zEy6MDghfsW?{uKI6Nc|d`HBE8&B7gU%6a!rOIKI-;K74hTAv`BPdZ=dW`8nTQHFM9 zanF4*TqbfHuQBKOolLe9xaAb<*?GUI#LoxGearYwjiyl+Z`|44o3>o@c^f3$A&qe! zk}bYNu5m$cO!F=aEHbZ!#bF5D!$cvvs`u@>L7d7^Af&~?;4eHv7-0C$#5ZrMHwfEQ z@s#|&11!H&jWUbR4~t)Mc+&;}W$yV=gZampb*o-MVP&X@UXo_BG=f7=z!~T$)nHBT z{YJ%-MgJQw{a91y11Ux&8&@Vh&{l*LixxgMv71&OpO~z{#$s07{q>p0vPyZ8BwY41 z8QvG`)f1T}D!mR#V>B-Dn#+p;I%$f)N_44>$IyN4ka~}awthQg0Uk3M-#Y4;nBq>*i5HHBu%UZNXkzJk{7eyqvsp|3GJ`cwn!4|@(N=Nx(h?0VLRSMZPb z{r8vX|3&_>Qr#=AB(FYkLpK}uF#5rW36Q6%{*VJM{d{_VGbL^Ei3t(4sq?>FQU9tt zQ|g`{$@p9{D`f{l+_j5XF0*+LY?=dqSSq2AnzF>zn3YP>c)v{kWS0!d?lDI0_%E_A ze~a{)vaQR^$FbBb9NW%M|25X*kBLR;{EKykDo;Fe&msr}LsCBGa^eIo1j^%8%BwCN zUUEOe%csXn`TrxMw^UjE&t*3MZS>BZcDd&JdwB$6^tNFd~N<10OyI`I}H-cf522_d|Bnz83Fir6PJz2PuJl);hCfZBt#6VCk_lNs? zVxNMVU;O%5{I6?>X*j+o<}qA3bbrxcAbkTmHz``>@+yn;Ez~+FQ?V>3Lvn59#*aLQM`RQdMN<3!Q z4@GBtE%&O!7r%rkF!c2XsuS`ivoqi|kh2EOmhJCfq zk7Q!!e{av%jgUmD+T}IaIfm!(XPzyG5Bw&1l*mFN)~2T_5)CW1waY68M0+TUkY6O1 zJ2%>w=4^O5=}F11Bz+|j8m~;>1ckz)%dz;RnjhvU_wdBjj1q+D$BRB89DHJ$NypER zmChMA%7_I(u{z)c?+++Zkn9AV=9cB+j412-Jo-*Mj~dU!lVEba+R5^6wy)@^4Lp(lk(YU&OxETZ&k;I^Ysh0V~&^4+gHc7i@np$vZ{ z##SgVaAlR`wgWQl-FhoxrpN%+HjV(GzQ$(Ji`2l(CS-PScjrZ08m{aFFBZDuX zolg0~z=1)n3xZ-t;)OQjq5FZSR5 ziK|Vu<*WvXVCNTo;j(;>772w*0wSAh)S15Ta^?@zuS2++C3%w?)*U6YULmE~Lza{p zb^o(Tk;wQ0G95q1&7n^|m4;P9N$+^98G!Dld2~c#ZW~fQ*}pW=4(w3!I5hJ`2rK=} zLw))L{3gPrsq2({!ius1HgirxoFIOxpOF@^n7z8;O`@iduL7;b<4NIGBzV|t+ z$bk|_C-Ad>OgRWC|B0Z|i6B4>e2O0LmCzqU^2R4U86;q(vP2hbxNFzv#ixEqIe78I zZA9Z6WdhbAQ}p+#D`JhlMrN8>%KvD$Kpf@_G8z<$_bf!LWht)XCiDU3uq9#S1n@3d zSFFARL&>VjYrKF8%>oObL>#ivBp6&y2eG9Y8;Q|cxWJ3~kAe38KLgdp@SyBrt{uI^ z6Yb$JGgH80%^U*S$ESpqcEm4R_Sg!nM`pgz>JiXIaQSVeI@(4jKGHL_F|Hi4ARcmI4}yjGZt z%QV-iQdyGU5CSf60dOrofJLX*{I<%qxYEYJ>3(-kgslMIvDR3^bf+vigI27b{z6e- z?d6Wk5j78t^d<3|lb?X0o*P0s$AaZpVbV@|epqL*$8viI2YgsqXPW58z`{W+7E-3r z%z}*EbKr|aOotDBCH3=4YFz3%WLMe*YJ7&#g%%qbysGdS7_uDrqAVGlV!S~x*j%Cr zCR5EWgxj+%uS*8;c+wYGfGo85M|bv!;Pebb!i0E-Yix#Nva~$!T3a|vBvu<)+eW87 zn9tS=-}+fLTmL!^3101*oBiUpx53`!U=1gD!G;Yx)aZ;2~E(T@rH$1^OB zA0Lh{&&$cN18G!QJzG4!CPw7_00{`EbAf1Z%X=QNST;UD@aeq-%39f`}0)6JC7X$(RFPrzkDW&Dpqh zE19FR)o_zc;{Jc!ve z9*yYd@#6zt_NOSiCmz6~{~A|0ME}p2O%kSgwgNnBuS$PTf~J~kWl6Y2dIf811aIG? zzpvV7puXmy9dZjbyN0Uf?mn58MnN{g5mqx|S-uSBYgaK*@QMQ-*1t;m05?cnPxkU- zcr4t-yAY$7KS5}VJ_E68QpoE%YoC)>VfZ0~c(Pjgz+bQ2XS=5dyR*tl{ouX2YaMeD z>~bX#{n0Amma>ftYnT4G=GntP15W^l=Y>~uZEsy1)uIeegP);i0=F?5WEI$`=ED$F z@!3d6;vXwF7U9y$EK;>^;_s2KYpHRcQKe`8Yw$Z3R{F`B%{UwoHb(?Y?P_#QwC`SQ z$@SB2w@D`0UTs8!kqi)F5$>&8mBt=FC?*bF{SgCOB3#Xfx&Epz!V-P>_f6t)a-#Ii z1$|yu8B}D-5cKIOE-)b?G3H%XZY2PnoZx$|-6qaINr{#(y{uw}gyh6JTl-22(U`r?<+brC=Be_0IsS!aHn`0&mL%euu*$p-~H@8nv z3_)_n@=mQ!G0|NI<>ZihITEAt+U-86sGJm`SLrju3RU>-4nQuZOU=F6H0kofa<1C7 zk+;m;qYVloX@QV4&F@VJLU@BT9UBWsTmy+pJ;@+W5Yo{I&nZom4JExH#{0z4cAF>< zmY#$`ef&^}l(;x)Vd2+73L~J-34(g4w5v~FJ9F;bWdR4B!l8WN>IgzPR@J(6iElU2 zN>%4Us*_Xa0E#T^1BAkAS1lBU-C*V0G8?4!_h$GAn_+oA@NjJ4@d&F6A;3X(%uc|* z0(pUvk*n4TVc;0CGU8CQ%d${Mp}Kgh(9#P#hc}EG=lsQI0NR8WSLX{_I`kftK|Uiy zD{{KI;h7gIPcnd|0aTpRC^nSUnP_20J3P|m`l9x1a2vQy;2^WwXj2xL3zD~ zB(w72O;BCh47D?)I+B(MiboQFd>Oy->1gHzE9yD0t{DtF41lF5EZsbLLbxn2_0_BU z+Jas?E&RqBkh;#9bIQ5`sDQd(-;MT;2)3vO3T&}D=n0+!B+aKKj&#)X#zZ^3f7%Ef zLIc%dE#K*u4jkQ&?vrU>pM-G73S6n>kUQXqiqzy=yt6%22Z7&Fz?drE?i>uY5;iQ7 z%NO+h{T?bCD%QZj0qJdqQcB(*?bIpQpTeGgv&=L{#4j%e=pr;reA)1j2CP=9%Wa4<1V2)j&gn z9<#|^ecCr#&hmL=rcF(3#K>RDvQ~4=ZJAW0NGxf-4Qg>p9nMh8f+7;axOK=N$UBwJ zj$3eSnrtp`wS(-BhlM5ZcV@#iw{6?j`8qZI0a6mCli>o%$lP4Oa=Mv3wO^ZG(u;q* z4)1>u;+$9pVaVYP(0pKtqp!7Pdo+Ce7RG1-QJMYT6gUE^m)JI%W$%Uen~DSq*oXK5 z(0yhTxH>TRAa?EmiWTo8gzOfyRifVz)t3$_AEb19zSMt~>q|pz?Z(9mP9Rf+U3BQHs_fm&JakUVJM$c8Kyu8$n=xW!G|&k7phpDuEk+yuEm2pea)nSa!8GF-TGK?%lg2nyzHQPJ|gY zX3X?gVHci`FJYBC1^8q)e0T0O6mFe`8A>@ z@MG7Z?Xb~|lVeYFA)QMZnbj(BA*@DDzEz`N-`_&Vzfq$P@46*+1cAbd5iy#)MhJZ{ zTGR>|fqKxoCr_S$Ce(Yi0a+3$ZiUhAZg;Bqf|(x7W10*g#>e%mxUo5=A%NlB^!9np z>dpofI)?1Ghjn8`~)2y;eF|_oobC(hz<*pA{Q6>$8`I4R%gTTb7RCzGn?$4BNx(@@tWWm zWebHOPn(!TVqOFW0>AugfkmEETRgQ1Ok99=3rtrux${LNH1bhbjmcWh<~f%ak^GYd z4)J0TwSu|?$!EqY=oH*o){B8gHLDcJ^5(OWH#V&tMHay{4G&s8hw#0&`D$#9_nO*| z;4J@!eh7)5H#-)R6&D4bpB95WjuIKEo&(cr-BsL5j;)snfW&wskcN|0IW~tq@EQfX z=6?IptG)R)a&Z~VcJ(!7Q(sQhZPxReuVVQggwelN2aFw(Zcrja8lzJAw3b%YQyEd} z)Hhuz0XgjkxMg;`KG35(0lF8v{EipBSiu@EwlLAZRzm?n0(A%uS?0pO&gPh*p_5%7 z(Dkd~L8JSV56X4VE2JX`c;H7-hBRZqOPux+91`S3eCKkd*7w1(LvaD3fb-SwjWc$ob=K(~p) zjDa7F)2zc6r0$`w0FZ|@M>et)0&&GqDmSEeOPJ+L15?NqXVs?%+X!(jfDK%^`2F{4|-y1wqA<0!Ow=IMIbwp8~T5F zvk{N*Wlvl%&IpI-fq^OQboa)OG&?V_Z5#rVW3lPl9}lQ@tsY z29+H=bb1gG=G{bf$4s^2^guRyG{r_}`n;L%@MkVAfTij`2^Dq%_sju(q1(NvpFeVH z@JA^)56w)1a%&+l*sQ?id`;!el^mV@sTq=2E`z#@KiVT;8S0gLt!*o$e);FLq;=rz zh7w^0_v?*_R4(a)dS|?QcMwS`0-@&C+z%kXvf(wq*i~s3jEEm~%ensR!=X2JZ}~WR zE)`O*Iynt7&&T+7XSY9ZHwLM`Sd3Ec5*`ten2o?){^~GceC^plL^@duo$Tc~5VP}+ z^CN-6lUwZB7HcV|L~uKlAycsXmfB-%x7E zNuoj(0d%yN@D9{U>kI?{h zZYOe#h)gWbD>80y!s}2^BtY%OwFjpHw^Q0*C7)U6gZpdKUuqc__!vupEQjLd&QJfm z+v3lJvOo?1J$(-Kh-+abJqvtpD`!7#w9KEY2!Za0Q@A`ob{OV4uyq`334Llh*sh zCdUCUPCR%sq!LU+y^@fqa$)v5tU||*PJs1y)rdE8#}Q1Q7V$yxH=Izg_J9ErTK?nC z1mDKsn^oCJlqz-S)0Wp)+>pQ$(Yo^6V0ocVFFJfq_dqQmtlT;WinkB-A0X%)U{68o>^yA4w~W3A$VLRFZ; zWEbiyx7PVVon^|E+h(D0e&)j8>v$hp^>Y+!8`THANC=>U?N;{G6>66GRr(t#-)!kV z&PWd=jW;x6fj(At<&PW$oMZg08!JT)L0QQx)ax=&Zw_R9DAffV9b2G8dZBf^v~;0a z@*bBwY*ODYy!u8KA7E|Q0zJ{+apVOBj2#@cYG4m zqi^B^cP!Xi)hOv-y-$^Gv&?q{7#_u}fo)Z&2pNcrH3)7+POYK642=)xG37XbpVvXs zl=iGVMS$d2yK1otRiE)<*qb`Fa9|B=FMA+WBftb@b(=K;O`Zh}zK(x#ef`Zf$U0n%E?hAYl5lGc77eUR~H!P4_W!5$u1`x(yEAwrn+F_zkA z_Y5@Y{^X>q6rvr;Pc9R<|9YnaKyjG?r48?Li2K+%;o7Dw$YX#V`7Ztleg}6-A7*XD z_^1&RS@<5wD8XGLpb-C=x8+Apvim;iLWacE%{)&HMac+O3R->h=&eD`g^umlUU5=OnB|93OAVxVdA z8e*_)T^8jR$}(7^|4`T!+}6gChXbN5H$--qPkdRgz; z1sleTLlU^Txt6$!R$04~LgVbe!AsZ}>pGl_o=Dp7#JL8^1+R>x=8!Fd0O4;@wOqvb z+7kdAQ7FPfUhU-caClrfPpPj#qs$a$mZ~0ySrJ$|Su`ekfX=RQ-JYd=-H3JnQ~kdF z>7aswhrc=aXaF-N+RHW}hyDcTt#A~X}u2s+sBk#jSx3B6XQ zZs13@N@+U;r$`^Bz#5&3|I^`V3kRqK9GbAI7AiNP`)W7J;KYd2Fp#n)B$rv1Vrb@j zjbC;c7)N6H1gSwd`~qGVkcd~`i)7bmyer9#863c~3=!D~Q)mO1T z+l2KHB##{Q)F^YT$}~BSmx1I|SE0<2)k&KQ0wRJ~z6x#LKy~~RJ{!k|3T5TAkHx&v zXp6<%z6qyrxI*E$Y_<1Aa?tQ2$C^t5Hd?TegY|1lYd0AjRDXgAzG$SlqP(!DrzbG* zDfB@A6?Mpl46*SQi02_uBL+-W??^&_Z*O2A?|31kQxXXTLcD{(E2^z(`n}9AsQ{x4 z?I?MIUzHCZ0!2S1MiesMBqemE5(b+1sgk`pOnk@Q8>q?Ve6$VHc^E@p?BY}>W|$Qm z0Ok(as|^|fAw=}UWk9#;pH5S?a_DR*w}j*Rh#n=37|14C9YUN2dW~{6@ZeL)V`e^sIm6$L48{s#fxxeb@P%)tC8wvSpEB6DMn#zh`!p2a zF%hC*C-B?=iEe;IqtjH>A*Yc(b4qXFfH5{JZk7I&#LfxIi7Z@QYTXP~$!b86Ud(Vw zkMn*#FYu6+GoalQ^3U%WX0~})QprfypI`@w_+*s41R_%hKBTP`8b?_X21GROLysTg zVVzk`enY??K$2PzqaQtTuz`)845vM*OZ z05Z>5Q7o+NdT+JC!9hYg`v{_~Whu-dk7_&vJDmp;O|FzIIK{OhfNL1}@(z8jIxsM0 z(wT0wf-T+{iv8#xyLfQT^D$zO@NSMn9WFa@%25c{M+&JzNqXyUmH3jt&@7Dq$1h5Qcs)AIcH z*|kGlGI~MF1cU(@?T`rq&NJ~0Ld~}^4uc;zfqgIo+PxvPg4who!5pRpLcOPIKJFyO zX}Hacvna!3DFf%|C?HJ(5&@;r)Om0?T3ajHm87IZj+<_!j$D`rn)4&qSx8rDP`x1@ zgO|wdp#3QFaBjW+aXlg^gtBAxeXpdtJT7t~>1I_9^X2SvoYGWpBI4vZMy8va{rT7} z{p9GwM*2_Bgmz4SeQ%sdn2qoXQTxX9NZEj$$Qes+(H`(vXFPdawlhmtGB~4sI>aJx z?aF}Xp0v@;Dv*6>n8{-voh_X+Eq+V;oJL_w)OtR*{~|Tt^6%y zbq*}#(*{P-ln{k+<)QcNLIX2}ka|PRIa7eT-(i{M77ttN_KOmJA z$iHR;diHwEgwzwd-h#g750`IGgxDBaNp+(|N* zgOPR4+tEoM6JOKk=1W84?;py#BE0(P#aC;?R-Ms{)hl&&2;UG}6DG3y{>En2d}ebeuI*JQO{9;lk_Q-YZA7 zw5~v&zg(%Ry820*6H{eZRi#wvwbu<^uc6(0ui9ws5sZ8+aPu2zo&lO05vI8ajVLmGX?FRo$K(F70*WF-6NQG|m%enWVNUZITMx8`BOd(^?Tb ztt2Ub>j&G0EIJ(Bnc#KeBNJYU4@cJg1kzKF2(LLxfkO z?8f}o*N@$$9aN_f4=G*NOBcQAMUbCnGL~(KKaYNMZZnB(UmGBr6lSxy1calLx7L7dYCvJpxvEbP%EOmD4-UX*znUMRIb~GGWp+`6#euX(fKs+cbE{jN&VGJB*Au zoGE%bI_3G>485of2T}=z@EQT`NTbCF@1m~D!Zw(34 zP8n1uqzIpbCuv%J7@v9A3k-JvcgWtb5<=Ju^ z5(-8aFFtS1b)=zVz=81LjZU8~US+{wjuUXWt@(n(T*)`aqwhmaIoH$cPokr*x!r#F zLZQRAo;nECJJ|?K_=#fgi+xk~}8Nw(%b9HQtW2Rj7mN%bf|IxlwDh%GE>xxbS^fes<74VOmz3 zog0NtNu?>ELOwngHwq@UOI{ENF{O;fB7$^(|)Kd z$%Rm%pfIcw3mRST8O@}JFz6zujBabf{5QIksC%FEipp#7EV}5iVQqi)+f>uMZ;s2q zT`e}B3&)cCCfQK8lT*`!ITTTyq3D{`<`~=PhL_7x`+v9hkL{i7*9plh(x{)W&eN2$ z|E86wz2Oy__vJ*Y=+sQ*tudEa)a^LPK!5Zs1{;${j!AY$pA@33J_v5Uux+Lz&pFQi zhf*kXvt9EZgy!rq(>RBK_d?XSN8_jWOGvEmgVgh(mII-A|n(A1L_Kw`tg zVTp2Y6FIa`2JL${GMiIQ(|dNXu3Dij1&V13jtbUS3fn6z625+w%~AG0bLLWr^QYqp zavG?g34RR6$ezoDtNS?#Hu1`qOOQ}$8liXDM;i& zow|H%)xNh6LqktZ$9yk?T{Q;z>ZrmOAmG;kfliz}`5nH*5CuF^u=8_C4Ej~5Cnt47 zeZ>5XT1isk!>;UA#7W)b#|;l1y0O!~F5#$PavwT&?QR*Fx;MH(G|#E=!R?P3gL}{4 z&^xH_hM0KGisqRw8wlA0em0ZYQX6=&<9;QU96S<_+Wec}>A8(|7ize^1Lbh3BVwxF z41RUec%xA|#aUhL-7reLfkKj90aIP8Zw^e)e9-%vfftlDSJ*Nzc~Uo`$bc`@O>g`9 zUUKp|;AWc26kGoh6;ydtF$O;nJ_P$g7PFR%z8h)wV>W}Ks86#+q)1g zswOAbaGt0%DiWC-h(=@tDR+}MG|UD=&(3DF=ti8TuK9z&z}nKeF{wPOV7KRwj-ERg z4hAa;D!&fk2?4c55N97pM6kb%{dkYl$4^2=Mkd}*6MFaTm<$TKhMF*X)G$;0t5#l{1X4`umMOeU+MU1n*?tAn~&Ds6c(o z_sGwd0(Da%QEE;{P%AX0)!+<_atfS@!HY*UD%Stw2dbFMF~wkvp+DL~=6 z9;!y~T)IzW{JIT;2l8t#zzw%3$(QQu_!Xt&$IP%L=|&%53w=I#Hi*W3;C&ZOwZVhT zb6aSt0+uNB?0MgccfNfus~@*^E624&i|sSMIDFAOm|hcGU+OU=uSQmX^`?_z?dO!| zK&LAEj7RqsWMw^ieLqOa>0+HojF^HDaV&bWwIsI2+0hqf)qXy|iQ@3&iQQwyxJ%wE z)39{=#CAdqLDsG=AK%L2B%BT#Ki2 zsT|>0F>jQ{4>)#12cO|L{+aD_dG2*uE4n4%pn%kD?lH2gMEaE>bpOes*>~hnB3T>X z$B^ROgksxxkfgwfkT6X;{%H5{10Ivt3BTeVK71Y>&EMVAv&|?-@!ql1r%$&gaia-* zmOS&g0)3^;fx*GS@#J5+T3Wa7ZV-6={K&npARs6*Oo4HKhdB8w3GwloaKuI)oOPs@ z$f0}fTAID(ojZ4uPU-t0n(xoKlPP`?P6@iCs&8PxDHeRO+Y{xy`UbKnIpWw<`EF;q za0$2T?i04r*R* z@k;3z971CL`RX-;QCIB!yk8g3sDM0f1mNnZJwxDv!4pibYuX5G5LCcfvy3%Py}oan zD?L`fcsud?sHJYSm`b1f>}DO_M0v7gnA>=(Ql8%mmsT|&jf{q|XUT=LcQmiM`a4vb zQV$<*&cY-&TeLL~KHEcy<(guo&i^iWbL7mK7YB$qH=lN}3JO^fo&=?-wpVL)N$df? z5%;a?z5%1?G1=Q%kZm9J>*Kxc3iHgtmgKIkOGZ~d6%+*?0qdn#378^h=-ba`wjI1i zr0>b#*F-lS0FrlNXZaRp!ZX0)T`473xPYn!Q z4N4kWK0HCe`?R)uT@^X5?9#PbWaDlAB;?qpi{07+j${5+=OpZFc-Fda9gypJ?=C+; zI#gJQ*;m?PiFMS*Gfe2yzSxkW`%sS~rw8M+3s;~tm%Gl*c+EO|zM4l83ve5-wH;jEfP0Y)%VxEdI*=6T0>3z^wSmd6tjvneD|!&}&m)kjTg_*D z#k&~MeBt9{ORV3i8e7Mz7!|VPGhXWTkkni3oEI)+IJ_mw>%=4Ps>yq_}h)W9mcks5-h-mpnd#5)w*RkK77f+FySn64Q*i{mc=KPY^t^& zr74O@NNc4zM3}q3YJe=)ws?RogolJrgOk0zl^Evl_gBBYxKDN@=*2{W2FJy0XNylq zO8w;&IKSx%l@wDxQt@!eJSe(I4qDJpf{2e@(K_J4gYIoejj;EEW`4GYo^7MQU&hCm z(WfBFPU_Z=QC>;cno>%4pl&ke;^O*BBAv6gP8c@x_b(Z4%F!(cp82-g%t7}ez(F_F zlVIhH0xD!y1~bk`8zr3_-iEu@lKaMf5;~PRJykXRL?F^feK>JP?RP=Ut!_AeD9^iT zLW5cpNB+v$Sm>86p*-c%7N^uqVSfgv3T%@(*f!_|T0fU#E$4a;P>qp=el0g@qwkWH zqCNs6#Wp+T*;-Ei9(fYKrfe^CGL0}8W54bRdHYtI96NUR1&2{MLS zAuniWYx~71FDmIK+Q02A*qzJO&uq8Vq?;El+hzdIK8Wc*Q%YgG8iY8i08SzvYx4!o z5yaUrEmNxDLki1WWop~a2O(F_xbK3?3KUef%Wm-L>KyQ~!&)_ke1u>(+%MB4R;MRHO)~ zC^ozoWsHuUFDdTStdeEaK@?OMoPH z@%yBoNqf8^T$P8hfHMA@M=fsLI4CKJRd3Fnnm>5(V9VSj(I;<$>*7dnRYliQoYb9Q z&Xe^PY}WxmD`f}>3ZA#^EP5;=7M~_4A8}=S zZ#|^JfM91jZG6US(vo++);T-o1_nV>?xTip`h>bp#=9M#tMUph`ZyG1%xe>xMQ{8- z<>&jkWj$9<3y^SVSvCq!^yBQFKRGsoLk0rfgN-0yN^C`#X;&z9Fj|Q3GwVKCH|^aj zW{a=T6SrNu5rn{liXhgR(}~odI^~}%SOPP7A%ylxteExADG3nfm>`6u)A!2>`&QVT|rZ-Tesgt_wFBFE=ddMARo6Z#dkql2p`-7 zY%OYy-@U6ZTU@sM8XqtXcA7|glzH-t<{WK#YaIa@Lq>s(8-=~?A`%kDF^5@6TlR!f z_h&*>XdHH}u;b~ft2C{Ex2_g_OaV-qc9bysX<}kN)CTOLT;Ql<1iWi(OYO=%=WFTH5AhM#$~C`0o#fXrm9; z>auR~KNlLwLs*{0+i&M4mYZs#7o_HEgZQj)L$=t_R&%?2AHNx+c=UPiAN}VIX1_F8 z_jb#CMe8*bcWDsl^SLS%al7`2Nk=<^(!RTCN9bc_Sg|qlFp_`f$*m96eH*krLmsDF znRu&3e9t^{0Z@2f6BiU#Y62Co^)zrP#~B!VL7D68#Ajb+&u_QZPlLa;0YWqFkg~LX znL{#={^aq%r9`mm2my8Sq!!}2qYGcX@@^kKG8g{rS?1TTR}h{lEpJ!#r$o7$rg{nw zgQF|RN-`BynMwn!I+-x#=F6}<%sFc7x9bQ8C3@P#+e3aW?M1uqdCNz>bN!WolG-RP zhb)@or)Wam4pMnLNTtHax?8~~Yd=f1+P9hXxvL1V=V_BDlAO&X+unM$lPLfFJjw4m zi`pifo!wqgM=~QO@{2uBqmItJVI@&yg0Ge)M)oYLiYmCxbeDVbWy4wX;^h2{kH}r2 zoKiDKOsR`a)>8Q#TFre<7VYctR3NSCtYz2ieAJp;!ioOF*(&GQJT_w7H{LxBl`FgA z#d*B%D8-e=idu!!W+(D%1J=_=@sD5?fqLA>iKlzFK*t&|A~*;Zs8E~{gS9^01^i?c zqdX7H*J*&0BN2MWlloCEdy0--t7AkcDT^d?=v}t!-=+abbs7Xmt>Aw#lGO-_RR#pY z+KuC*_mW3wr0;GR3`O^WmE)ZUHIjAS?I`~wr~uI}WM1a0GUCPb*LS{SS_y*o z1gwv~9fCqJX2HUE#UX^-$9f|=VFDuG%qsiVE)_lGspp zok-ll$CnSd6Eoi3F;OJGWy6LI^}g^-E(7nk?%*(q&dNJLj2E|0fTA|7>`4d*VthG- zhotFT8bB0+jk1yE^{YAKFNe@9djwDXJkb%}nfd;_;BBxRNx}pZ8%!pO%O4m7%GitS z?&^;AA@W%k;1`fFi62=kp6+4I5#E7fGiU1cR0#>-6l1v!b7XkDKR^wgZ#V9dB3V>Z zZ`;$D@?4ct_0r*G^?b=EGL8|&yrwTcOS7q~F#^aM+*IlGc=8()zBYh(6#7K-2^iJ( zXD9n8IJDP79tSsfimp&@`Lj^$6JQWhVrRwBeNSL9cHBJGakJ!MZ+k3Y>-~a)>${ax zs|5rEV1=H{pu5Dqc{t**EjZEi*v!_8#5Fv^FR>7>Ntf^Q@oy%W_)=M*4$2UHz@Obxg;V|e36y}=WSVkrI_sj#GE#ocBrjKOm9RxcZuS~YLW!yfB- zM&Tn+n-XL!sG4g~dj=>e4MoH8J7|D)RGY(iCC=VC*MHLXU>$Bj&v=t%RkGXWS1>hE zWFD~8@)yWaon0i#xpzU+a#PESY>e{qh<8np38&7q>e%sFZmFd1q>gSi=*xlZsR`Qv z7|d?0(HfK{_t=xpOqe4`;4(4~#ldHv@K%`s;cSMv?|)vxUZm}O?5Xud<3i%IMOdgk z{dK(u_pSDT7fz&j7g-hhPBwCO(mObGqc-se{vyOKl|JRh=h@l_>CYsLP?7T@kylls zA2gh2*!P52xX0^xy=|E`STC=x^VOzU2F34f1hBzZ`losbJgt=5SX(tHY0a8MaRwCImc7yG7k`5m}d>Oc}fa<>B;LT1<>`Cu=nYv>uEPx-U8LM%)ElAHa~8|(Nyj?m}DN zqn_Nl2LE`S#6+14VItDl+J0&|Sax7$s^Gg3{>4KU%hL?})xBYdk2Es^HIB0EhAH=b#@GqSatJZd z6S?~7^oEe*xqLtdDuwYphe6Js<-eER8Mvlp?rV6b)%$M>x=>;%U&Rmc3$vBR!u8wZ zUY6l)W++SlJiDHYRzBS%+9GY(5uE+3JMkzob|*JOM7@v2HS-ndv-OjzK~RZ8jrvK>PckcL+o@vO*&p;s;YqiVC0kg z!uz$H^~CS_p_YT|ZPsoSXg|Pwd7*7L1l&Gf_@G2kPAEQg*a5|@4yPA6ZK5HU}M$;De9zf65-t}Pt zPTd_fiKNN2nCzOGvb^rEAe-k*Yz+htztmC5ZbY@n!)iM0JbIy&1i^); z*T`X9NHNf-xR0|0Y;G(Nr_}QDincRr{2y)Kh?}^h$WrTi98wtY5iRfsA%j{wT(g7L zqL>}?v@9eCko&ma4NPv>?$o22-OPMG%fQwByMuSW2uH0kpnRGQ=$A9AgvPJx*7HDNonTEhc%MVg$&(1 zyUl@JpT-4Wx>!cOh?@ckN(mX=zedfJ=}JFfAkXi;k7D>9-P}oqF8_}nUgeH&K0QHBxnSNEcD^?g5*me`mMBF=adAzKrnq@yiY)E) zw{J>yI(k`Cn0Wj=RvobMFV2#@XAMg63cr?xrxUO{RaGt#qcgZPQc8*lVewf?TdFd9 zb`aCtv`ShG21}{6u)~YFU zInQbW4PpRB4KqA9Y0qFV@pZCQQo>hX|LQG*JRzGRNj|ZeeWMt@^s3v~j~$+jC{a7H zXOg%lkft!GK1^6)X2c->aGRgmYR3-Oss%Flsv)^5xeJ*3nB|{+aMJc_AJHG}+Ql|q zN|2ODi;mu@8`V49BW7ONwx549lF}h?8$t%`i>VY5EQ!sOpcF%=pN>uhV%r;d=QBrE z7@+2iWXRcG4&!^EboOipv}~@f8WFB48pV+HAYf#deZe05ggim=mu7&uA*t|VYjsp59_kgrhO`F+TlqP8f7q|X`Pu_X#0H5SOJxkUV zV$Tug5H^y#H~;QQVXp877Nf-2iX*bZy4kWh_MlS-O0>RRwc~L~%j>z!>)|FQ;WauN%iK0{%VXH>LYxX2c9LfoO=OxZY)ccXhc)XFrv44p18XZEGcuoQ)0xry zxktv=@)_7dKu)Q;3T%PtP;X^D_o%C_(xYgo$?F}(b8t|wc(zakKS;C56)yh*uyQIg zXj6`?@4>dM8^uu6{(%n@(_aqsCAWR@I)8|ma7j1n0h{iBk_>t26_g3dkSVXqKPeFo zKJJv664z?VQOQ*H!6Cmsw9RY-wTwfmJ%*<=XEe3T(VpC1Twg5Ngho3s%1dMBA6biI z(!>o*#p`rNuLi^KwsJ-a9t5Y-^DXx6>%7pufIGDhs#}Y*RVEq$7r40N(Y_yhi z!7x}KZD@Em$D*`-C!cG@_=cw|Negh*G?rU87u>VE#5B~(c}w#eJg?Eu*d;%fSL82- zs*W-ydF%_9OF=V)UHJ{~>JPq*@LJy%qZ&W*0s1$KZ*+Lk<^!JD^*_0^Z@i^mEpa#p z7vkQaZO^En86{jbeR+|9KTqxE8}dSvp4dv|3bUj9C07Q{DJwghI-VsaOBlgX4rDhn zDk?(Aj*&cfOEA~q0N#T+6@me-6Yl;<%GX_3_*6ZyNF*;Z|0BysBiwIdr0-@$ISI;} zpn$|=oD&n(%=Vtcg!k&jT_`(n62>(hA$RTjNG3GzMPO5NgtuNJLuNrY%AeoT@tm_+ z=x!^|L`!GP^Uk@B@#|?#(*uZG+vjzk*J~g>jkHgsLbbH7y_w>QJj`YK^ks-(D~+Ed zO1Y^AZQ;9AQL?m@2IQ0jdZn77j5gH84k_{D^GxTIjLk96UQy=oy8~_E=D6fXcODgA%Mmyond0lm~sxh+YmWG?|La%BL#5}O0h*R zsHOzVcqL;BBlil+7?}BBV+A-3W~M$@n~;Fp#lM^p_*jpSQWXjgH@t;`F?KAbMAk&^ z?Q%ZkQbK`tPO2Soc4p01K`GXEI8*r;B<6eEFuuWc-_LyPs?1gD0yLsalz6w839ZO8 z&&|X->mfgc;qx&fk(gPupb?Zn@WyOd)8Y;>FhaqG$VHt$r;px7Zb4?(O73D$rYIXG%tRG7wShQ z7Hbe-U(seOc|s#i&Eu{6LVevkcyA}N8_z#__qM12%P1U4rkJW@f*g9fZlPLL^?WMIL`#4kZJCO*_{IUAuO>abCq_U z_EcIDAC9|$t~6oX^Azk01XB18jvWj0?v1Yb`DjSD6_-n1I?`ui(}4jEAbC>-uzV=d zgHoGjGQg^S|1PYW=>Wlx&ZTR5lgXQe94Oxd-pN%>i({(#C6zDNMNC?E{5-s4CtvFb zE+Q*OnmB2wTU#)m5=L$7&xg+4%tn#0%k)%NN#6zT2w$(~O`(R~!v#Z&^O$^6X~vvp zm4?C;^@}M2B*7GA5{T`LYXE5;TgS!tvP;7TqJ^!fHQ$C<^_R0#U2Q{_2~tu9PDYS| zTTgWd))9v*TuuUfQcI*H`gydN>Q!Bx8=IPHdwC`t+!@6zC%ow9p!L|%uawD!$cT9D z$>cYf6T0@G%iqT@X@d;qp{qc!kk_ofOoWPtXE{+_UgAR1rgLOgKpY~axOCAiT?HU}VIPjbI&s6gqvI9}H0oHca*a3Stl_=-rOXc?OXo{E~6&YW<13#XQ|$ zeko`L>AsinTdLIcDQE($joK-7$@MZ)cewh6z+Yb|=+bDkE5#x`7%K-?no15LN$~e5 zMqI+*gMagC%@9yT=XSZ0_*o!Vx^hbR$Sx!-`g^c3t^7Z^Tf^@JKqv^lI55FGmV2wa1l>F;sPpY2K4cn}OLO8KmUHd37O`;gy- z@C$zrHm1ER;~*a{yL`Py*RQOWe8NBJnRNdico|$FWo5glWs^)0dvfK%zgK?tiOvT$ zmfzBD1ox1F%nwO~jX?zl+E>dYhl$%4LkMd*cZ@wlZud<>M!*;se0%jc+Te2R@1?;p z_^%Ys{Ql8@es38n5Pm{i)T2-yiRa(HUmt^#A96TX1eEM#LpqcUg#tjff@-}@wh9AV zogLXRWoWU#6qy-t>^VXY(aRA={w;rRALH!;%=fn&s^-2wgoI@Q_Nh|#^XH0EZsVeL z;_;H0Rw&{EXy#w#c79e@@f;mJ?7H?sWiD z>kdKi6Kk-&dv$TNwCR-a=jr55Ibp?pCj-NbM_|G z?c}nuujFl-n*`P4G`+nm;r;=47&xg`xK5mmq`#nE0(Q5}%nCE^Np!q^UFC(#*maCD zauTl0cPf2tc`_V%vElZ^Imc7RUtt9zA`8b4r* zA-DtUekf^!=l!-k+F7MhI8=G{s7TY2;e|l?IGc-n_!@;f^58%+76z2H$_|v<45T{c0P{u5A3|{-=^09ADqOyI;G0Wp_}cSrVcuH-KM@)8$T0E=_uKZ8grQ` zm28!@h{559N7cN1dD{;x?C`bEL6a5A2RME-zbGoglbzP^-w-%ZqD@O$s&~4PN)qM| zooPsJrJa7tBP5ZwEbbUX_!0Ex{;LxyZ#F0(Ra8m+7pjvB8ycEmNQP&f3k8mgJrpq; z6cnS@q)}ei?#)J;Fy$YyF({-!!I%XGws|6-sOyEQWb5rsYfwgyT8mZWwQ?66myXn% zD)KL10`y~wN;O>{tF*a7D`<;f?mLyJSmRhR(mJXNg?$@t9C@`r>_25xEEuo%@%WZ> zTt?+gT+9pEoF=q=42>Or+kXpL`SNSE;9RZ5bZU7AS?gQdT+o3S27cDXn{i4ZyUc0r zeFF`=?Y%yU6jnda4|6456D0F5WJH3Hd7p{*hv_f9+PVo}@_iS%p+z3&o@Wtecld}% z`aa~$yUN~@k%_8#5@7O6prH`VwjDL{wKi>ig9WzOC)Bc2`w+Uq@@;`s2+4_H*YYLY zRF`$dPCOa~%9p%NvAw?Xa~kSB@d;mBH_0NqO6UW*6;8>k74WzqR5^QS|*4bpc(mn zjfWbKHsmI`Dqj730)R9aSbSa3WZkcP|GrR_v3R-h`uU12KU}MbA*G8_GvHq#~fGNNy)V4_e&`_i>2ZMsby&`6!prTXDT7JVda@)U*)Bb03@Xdo#B zd~V#*?2B^WW)O-&aqUEW41bP}j-JgJHUwxT6E*bSIO^yZwZ&E%ln0c?%KCf#eAc0K zq3cKsy&XQvSd?&#Nq}P zbw16ML_J$0dP-;v{QvGHnoYd4$3=Lrb4GHIH<0miIeEt#?%uzC*z&{is-FaL)!Geu z@MBz+>3I9+D$iZl?rEhTRwe8cv8ehswcMY-KxGj!#&%zsar<20+M|S{ez_~gQL>18 z=y%*2Q?2Vd*UCN1XcBE=qiv&w6IRRYQ73_l&*{dJMBD!pb8m zlhnC^L_*h!4%pqBL-xytrYHEOy*?Mhi;y|KsITVbbk{MLor61NuIw~UvN{J{^Q9=R zbe=ttB2|+ho8cF~Zf90jHrSGcc6MAM)pN}KdCn&yCPie^&g#Lb%7lYQro7NP984^u z=?#L*NJM~=kABV$e=!^S&EZtiSbwyxrvkeZ8_``{h|Ffl-M<#)_z4y)#Lx11FZySi zK&D@xwwA+pc!l}4PP=n`;&*WH{iKBmlUUD7xfaYX9?-w38f>oarx-pxB-eLJR$vrn zG2PcG%u2=r0<@Uw4Fp(QYwgsunt6Ms-$ku2@*bq8vGNG2delvn&d!X|jiyYNvQszp zG4Z&P#*j5&+Mq(Ee%XfGek6aAw7|mvyf`|dav6UQ{rxp zc$!vzB zxas~7#ON}sfbCc7zuO=vmjw>@M5Li$$>oa#VXl*8;YG(KlVH|w>Q5swug)5SpMIX2 z>YC=WbEnVX@lW8eY`w4U;^VOJE+{AB(8g!@DRLiA@`mklP4An8%mkNh_|z%C>%qBT zDQ)Nj5UPq^`^aAJ1Y<5bJKs^i-ZylT@AQ=$F&~FUo;e5e2MdNe4|=NaHdU(_G}lrY z_N!jZe>D23e_^Z&CYuWBI|4;=3a)8YTY2>I4VA6qhT$F=-+BgFD)q%pt!CMxzTH(1 zz>*^ioq+?aLnh>#8y&u&w)bp{An)t$HIYTaK-X?t%hRDiV{8UH8gmpV2UQ( zR#M4IHEzLlKQ=75A(U09>e<5AH=UDKw^BDHZwvyk5dkiJa;y+U6^?+hD;<0Ikaf|* z1i-*T)N&TPnHgB!j_u(N89CCX8#C~2+Q;<$eB0c4V0btBpQe1RHRmG164-tE%7C*m zImq`^6Y(pQ?pjMu*pJad_HZ)HBPNMgBjHCu+aL`N04>UA(W+Mq_Clx zw6FKi6k{pFv!b?H%RW|l3Et)khR_T2n(&lvgSu#n|mh^{eCNGW>&3PlVwta_b`* z%V|Ee?8MtpdZb#{(^yWP_VW@3)W*WqqVX_Fm;fFAI$-d2R?hu=tw3}Hx71wl?j6!W zK|9rz@%W5S$%4;xKfT!J{4GxxjmH8V8i_OI%XmY3E?ut;tM2YJ5G?uW-ZL=F^u289NBh<< zlgkqcGT3Lg_j$z1O@8fhfI_X{hFdNgmAhcFhzj+c`k|XoQG$_9Pl(Q9bs{KZ9X=Ct zDfdm|s(bSx##bYK?(zUrpJuP|l#*}1pat9jAbIi?%G*fKxuwM6k8@W5i7~jY#@hHh zy;Yvkkzk+{!GN{9%f;u{w7YwQZ56$959v(HWjA~liXQDZL^bsXE373S@c!0qtq)a0 zYSeOH%3EDew-2!~)K4N}lxHTi1ttS;Bcl-e>WD~REkR=CE-(Sqhk^;mBRMK(E?-y& zwG-otBR-6%x;y(S`yF5f5cy&<02+w(>eUp<))PZKVtb6;5;|X)7{Df5@luYbPivGP zYD3SSC8;0WE%mjM=_(JflPB_?fm`{}5q|?_1YO2A2RHdq#>j>NrKo|SvUBL~wl=vd=q+q)EBNB5M36Yo0eI?%QPq@Fr9~o+FOR3$zZ|2F zZo*NTv?@|r$rG-l&}HM)mF{dgP~u6J-^$0wQRE|my(4-ShRS*_>|}_&bafo{b|0(f z8dSS;!Zxti(s>GZVA0X$2doU@E2@3q% z>x7uy@Vr-Fc8+vP%KnBT_>e9sk8msC9Iw>1HbKH{aZE;5o-23rJMYGx2-C#cPCnF? zYYsG22&Y)xbT@nsK^i!WR_%C|Y@Zy=^%Hrw1ZLTSrZAT-UzSpaPJp~?a-?HbHa;xp zAVXyUK_CD7YMD!U`y2a8v74`0e)u^{9lP%YG3CO80;KlG9~%y!V&?*NzmUoNt(_w1 z@fpwYP8KCfKNuDDGfaI$jxkS`e6*;5=`Lbh-VtjjpViwm^|fCdrzJYPO9$B zH!rjjt9i^nd(>*DmzoaP_hyldqb+EWrG?;wLbxQfi48pzXM=(wg*V-ygGPk{pW~JS zVUlNzgl$GC)*%s!?OM9khZXTUUgX$!$|8d1V+UB~2zu&JL7VOfyzyR^M) z0ordYOtrD8=K|Nm8lm^6B+D*vFz`DkCiV`0Klxdb|;H$9a1%M4d->OVaq9& zGBGunj9Sx!QAn_y3k@HRXU5O!cij1Q1BI5g-goMN(#dp_==w&9q;PQg$JfQ0%2?nM z;;vym4x*zkzTVohFpfV|H92HhA6*KlaFSIG|6F{TTzZB8F`wx2U`=~R^qSY!NHYRJ zun&PzA_@ThLoEjEX&x;#ivwn6i=HRh1N9`GhGzU(l*PxH`E zVbKD({hZsja93^Kj_-BPW7(ED2Zyei_=_W*=jgKZI`^J=^QSk)H@-Sbxwf44O?c)LzjNp zj+)#a4ZtR?c4Te$lC6(DH7M;+>usUKhg+k$UP~~Bx0B5Cweq)*_EL7DJR~UV5T3}> z4m=43Sc~A<8L;0UJ?Oh@h|eJSghmzBkpLP+50@tT-l_-1uh~RU6Ur-7^ov`5Fxd&v zsnjMIDgYor{VassNOK#t?;sv37t$<^fC;8(Xbvh zr+dkh@$g}F$}4wZz|bq~D_1dq$7!d?^2J43*w5~EiZ^tS5;Zo>TnGnQG&^Zh z_xzjMFS|jqD?TeL@6;V7CYIrJ?9)dM5mw~|!_YH?*p{|(N<@uEc5}&GA9O0E7ZW1W z5rV-_9mOc0;tY=zrs1m{fLrgrAYPD#?Zme|2b!fhRGBrj84tr7JV;ooZV^3aaB%KT zqVWkKuZ{i}IPA2WJcpt@A#zY9LgYE)r4BKXRr|>r0Te^hrSg{DHv&h>(G|1hxg_bH zMwoL*>6@rn3dJkf`QY2*cY>WQQ-v!8FJA7-&OgO#4^+wo-2lQyu%Gq|8wN+dfOT(i zU*XZOrDtIc0Jt22uy@eDm+ZC(Q%Dt8&I2g5L>;KQFT3AvB|AXds#X*WX`KJvyCBu% zt)%%^l-pAao#E5%-~Hx|mSL44q2=e8soM$BOF5CkCL@?0wAaibZ9uy>Hu}4aUd4f3 zEx{f|6`TdMH_m$;Poa3t`q+^focc7zC%L1!kB@G_{e-e7JXW1#e~nO&fc)7X5Yv#M z$^7Q~Z(XX5HU!(Ja-oN;QAx?pMkj@gX?YDc{CZ)s-ZfSoTBo}uiXKB4i_lXqM|s#B z?*RZUYx;<;P|#^>Q?e9@?qLUBXL_#EMy&~bL3*Apx0DIq5{4)? zSJ@6MT0_;Z{mRUY(A6MdWzsy7;@9qJV(N}D_dZR_AIuKsw&-nl8{hH@f~K^V3sFsk zb!Iu`HsbEK=a*ZW>pA(7?7FJ80K0g~up;~e&S|+iQz5fCSvKuCywFo>9BjZNdiX`3 zSCx<0(p(ldXf{so*Hn&)F}v5ve**&tfVwQb)g) zKeY~JS?RktupQ0GXM^F)NJj9kc(a8*(5>57h_h>?A!8ZUM)ox6{;617KhH^#w7~&x zNiU;jiCz8Dd?n5etQJk;k-hFyLd3hz(oek?+sbXal=P(>{i+a|ZsJ)92@(ytXh*mx zGDkPozHN9X_MY1)5tboMA`E;sC=q{w8L?)0QXHsOQGwpxM4l~vi^WzFEhB0WBV-oF zteroA6^D8$+|RaL_O+c&hS4~7lCUAFrA0#HK4M&&mu70JmZZGDU41;lbs@a7*p}#s zKJQLEtN$!gn68oW#)HgZ(X?D5$(M4fhaH{XfEZ{BUP!K-WQX&ZL%>Q5Zy~G~c(G)? znxzn{3ScCf&k-zMe7rb8xG<Chd*}f zO59?p=ZFCVin{0c%NNZAsKdJ{9UAtqBE$4%-=(Y6V=P`XK6VD`e5bJW^dE%#0+IIK zYnk_H!>a5{}$r?DxaJJQI?@8ffaNN}|Lg zs4XmhxvvlW7cZ^jG_ms<7)~w#4KSB8&(CfTBWm#B`|L>>n#JIEG6SBAgEp^dJV=+= z=+Sz3VbI!>S6Cz!Q>C64QH9-dUy;QNxqu|OCck&4s2jC5A|8tlCjfMW_3fP5!S!QW zXw+Dyf?$2K$6j^IoH-gu4}-WxaU2b}dIWryqAtJ~88>+*s~d1h`8~9bsZ@fYIK#Zdlth2bineSAwY_uV@KFNHV%S?Z=W zkAP|R?8#66aX0@^1B8G*l3Qm+zX*NXqSNcn6(x??^iLxi(FszQbce)HKLMe(Wg((w zNy1+c>BPWX#6MR!-Pf(IQEJqyo}kvN!&;Z2{b+V^&gb<_w}Nu}q>%u5KcNA1MIbDK zHpv@XVS&P$)TyCS?YnRDB&5BvivM~M=ptnZQ}OBRyrf;1-N98nb!-l^Syadtho$mZ zyRUBlgSBOG0w8Wkb?bo!;ukIvYTO+S|4nP(>i#6a{1FZK8;1&kOzHtnT zA-%lF%hPJeEHd=gxa11P`4ZTf;6WsgH|m?mHgvfpgvu{WEI%^CGLZoT@7>qaaO|{? zKSG?nN%IaPsDTr=@gBO8uVta6q!iQZCB2(?y?*OWptC6hG7!3spP7*V~9ynTHP)YTXkQtMYX)BF^Jr9zzNpXbLwTh=)}tY{5;glU^$O8qHNR z%P1Lo6@gQLPw|xF2=QC9LW3v~6)!xutGIMUJTGQa><$Ym$q{lj&bbu_i%a*(0H2=~f?wm8OQi03F6~5Jnb`{=3NF!V#kBL}*GIWF`0v zxW!fpYeCyc+nb^|1is8yXr)^h)yTVdS&%6CVnQPCjkmWfK@iGv5C!QfI9k|NljY<) zlk2KMhRg(}Pfuy$b4 zkAPtnu$uokD`t*I8_&)L$yI3+v3=kL!sx*dL%sm~K^Cd`_Jt+h@2$O;{fJ6q4=IS%6TC^z&;_{>i`xe$-%+i`&J`4m%WI$PR*JhQH@Z#u`O^ zUNp@e^1=JqFkOugZ&H{r|Dx^9T7RaIFF_OHiu_;}Rv9pcT-+?z>r0C~77|K0=V{ZJFifezQFW#g>IcI?yC*hvB@|5Q$gUt8akJ z?$r$ln}o{XfJZ9gWAbMc`(<+!X7-i}bGF_|Itwt0%V*XhOe?$W=k~V_7hnRorLt;6 zSh;qKaS?FE>+-8oAy{+iGJz#yM@a`Z9I>njLff&!Z}51_Hb7T zM=t%?`?LE|mPSbUz_)B<4fjP#m*YIg%owus*Nd!^2ZHXSVa3nqr4ul7frGdl4yGfw zwbfogc>{DHXh#Ku?VlbG7gwWO$AA53TLm!_!^REu@9LvxI5;^o%dSlHSy#6_9w62K z1fWw-j0w&>MMAVl!F^P(V0HM{0mw6YZoYY?D!+47BwB3cx2V9Z|7GRNijLRnr{TZM zYmtcY?@@>P^%$!^@;|n^n0|kNfAhb8eXB0Ana=Mc)jOmN@l}SWrvw z_c28e2gUg7uQ&or@Zr2+%Cn}?cG14~?5 z2$V1WIphPNs_9xXbW0+?EhA@Tuhzo+`T&j)Lj)(dl!Kv$r|?0b!#J$r^Q7cN7eW@(l< z^F+$om1@@C7l5_~lswKzjPh%w!%ov$?ZEIs+}n7gNI(+Y!!uj60Vin zjk=Zj&;~JCF`iLz*8u&u*o-Zl`4+zbG)PhgK0GPzZH{;A%^dHkpg<+b;iE_KON;be zUV{`|uAw;er)EM!gEa^sX!_<&PEIOhj(vJIf%#c6kt2I9q`kVi1`n$rWP=PMqSnuN zIokBeJ_xS-1buD4pWXk_S!TvPBXA|!;)I_)yoB5FQf^FD+>J z#!?l(qVu%+b2fjZP6+iHDF#>b8Qp`BGXE%-@@GQILHGx{p^+n`nJ4@G`^FsMb-&Mv zvOI=>rK3o%Ds+0bLigoUq0D$B>-GC!*#(hSL&=* zKj+UB#>VWgG}!ZQ)4!|&@^1&b^09)~rGg)+v82Kr;eDeE(}F23TC+Fc>5n@8##q0u znlv-ac>B?IcJ>rxHvFnLTm7AU0F5 zbY{$G%wB;Hl_yUXhku6pN}J!covWYvzx^DD5Chqf$bxu8sBnbIS+Mz^KDIy}5C|7& z)Z_P| z^^>9(;RMrBgr#JpUkhBq+Lo85f{5p?fr_6H16S7)=(4=N@W)OAY)gi!m~TO#PS9QM zTJ(ZF>@cIxV(AWEvOso8S18Ft?LeO$T22KR4Di~8_H z@W}rC=RlLH1e^L0w(2`gbcEPGK1d#fS@w`UnKWh!aX`0qv|36hBT6H zo>3;;bWDPZx_UbZpJ#zIZ)R%h*>LU4m!FT|(rtPxJ#<-DBUWH;m3)_4G$Oucy#O_} zFBRGCkc38|6aX(Vv#og)BjG();N`j{idM<-S+29cDF)jN)c8x@Lt#{TWFv`W$b|YNj?j@R#x-RED|Qm zeH9gNpEz;i>60h>ln;Z<6SQ0I-8*OTyGUJA{HF5Q4HfJ3HH(*Xh1c!54c_Z+XhpVV z3+RWzjikXoYnF>ND)LoO%wpQYlYz(MlT`b|qoUHIj6FYGNdS2Ym?{e(KSvO@{aT=q zUro!}xN+mtgoHfbMOr3ohr%W8UeN6DW<@ae;AJf>kSq{@5`QZyxeb&vVqsUC@2A>9 zsv2*bm{%+X!ang<*htXgff{BZOSxFMh3Lq&5*8p)R%JC05aw0KO@(Y&!>h;ZC^rrf z2e0ui_Cea`DSfD`My&zp#$?l$rNedVW^~sSOktRfmtao$!iL>e58Mf)1BjgZK28^I|vNv*D%> z=b6I3HhMiXBL)@ew!81L^6}!?-b1l)8%SI$U%q*B^xRm$KY$5}5qguj;y}CraF&|B z{t|l>+z^+zUEIt>Uq0tabG(NwT%Fuin%_r71`Y&*U=Kz*i!mtop9c+g=S>mrofe>b zBfB__ljO7D2SFQap2MIOe}D2cY-ap-ngC4`u+m5&R*Xmhq69#r2$~=DK-?DaeL7tK zkc>=E(PyYNj);oVOUFb+Mq<~=gJw$+2o?coQwM>-m=k6XCJmq(yFQo`*%B|2E_CqV zv6UrdQ10|%m*TM7IB44)DHMSmNbdl1^j&PYWD4kOc~DqU$ai7{q`iN(!)-#rA!@?!K^Tm!#*k zWjr1PqOHkBk6}DOY*!1+OT9Db5<>`~1p<9yu;!IHZu5gG>xK+dC3r+D&+W0F(_nRC z)gDg4o?o2whoPamE-pn4i~9ux0`V?K!lZxRv+@8%C*7#O&>h9H5xJ+M21V~hg+78H zx7r#u3%CxLka{N=1~4d;)I8Ytu|u2S+$@8XPBnh|B)U$C*K7264c!FBgz#Dk42oJK zMywtSipsfQ5KTQ`i=qej_*0mE&r=ji3=kX3M@oK;owPb^*w&?B3Ue=QFxRlOT^TJ> zvjE#9f|jr>!Ed~r6x;x!Rk8uxBO@bRk}eqpLP|;sR2h+uojX$i3oYg31JlZt5aut2 zMX_wz!zOyLY0Y}V$QkLE9XA2$i!PP9erCQ4W2?pSNWaj0e(d@eu!*==AheCmD^(Y4 zVt~Hf3@G~E9<8jZstrzKr4;_xP0I(o_1gpv}0U)wYIz~oB z=n#FNhlL*Bc_0o{*ha=HCQhxB2Q|8{8Bh@Z=V0W<)h5+qfo}xF!jTMS_{`nS$JNG8 z+O!{qsLBA%_2;7~QA-yFg@9tv^^!~^?`H!UqqISIWJJ(Ttn)mg^YA7^9E91OH~GU9 zdqaNp_p3>@(6_;q0x@jb;Lo2<*(%`z#a6ibB57-U#-nFh2k@T~WV%Gr>PkvXYgUP} zmG3fdOAAm4*t+#v(@|ScI3t!5RLfD7l3VP??3kI?% z@>@dr{pH{Wkb(m-URi<&6cd9l4wlQ-&CQy^LPv2LtiEgwoeLit8*va_g$zzlyTC30 zUUs!eZsOPR`Wz z1Vqahwx_IW9~Am_+-v40?1rT!At3=9YQ`?Oif)L!wRSyM*d{P$h?!|P0{YPFrvBNqQ>W2q58|0jo|QjAhywxJgP;9%v0`0t=Cb z1()&sOUijlNtd0LqvK0Z;!g&xwrAGdGM+B+ceIRPy+=nj1aq1)o;jc%2h+PKX*cv{ z#rmp;<6oJnfP`nU5`BX!`p=D^*z%ZfCDkgctLuV9F%v8A&A>x&O`p5cr))b1cKR~| zei_o$gWrLL7Ihv{4eR<8Bc^iF{8L7$^DGFC9XWEO@rp2rkzrqK=987qxc0EM^>QhH zDA5~#wt(_?X#E}Dxg4{61X}G#(+(I-x{=7feP_V#Qg`lUF3NlR_AH19W*V>M2Lkb9 z-JMMoYV_Z`^rtBZvWZ@SodpaE%|pONZ{d~BfZac>-aXiXU7}IifNTJP$B*WP^W6=I z#Qv2lS5h$GWW6q7;OALhv*Vyd6x%mq0*f1nnQVX z-l4yr^Y4V{|Mlzbf4L6mOg_FMz5V+`|EEK(|5bA$wCm;PQgpp_dO7Dn58c{(j z3Sc2-K>H55ORgscD8|PEA-s<1P>87wRWa79-lw)UeU*Q>a?`X|kk&_VX7(37p%Ph> zU$bDFjPxzR;eSl=SNG#4AFt;|@LQmY3f^7(pSjQG4Kizg+6c*}%cR!H05pYqpq>i`{+nzRKuDE&%X zDFo)hKl!q6T-!P^!MgM5(niZ&|1_uasF+h#XBS)TbZ5EkTP8sY;qkuxhmXqM!00o- zno%CZr>Q+RFvh%y5f3ZD@+2w!%Xe8s?Rj&DKBu(Rx`xnR^xVWqpspraKrzpsLw*-{3dpf9yU=yJ%)6m257zm#MhC3EDWn&Jex>l4&HjsR;{VF0ib_MwQcqls?ST*?;{0z?$7$_W?{NaP%f|c^|N zu~>_$?$viQk?vLX%3!N>1xETry)D`xWZ6k@IRpGz$gB7er1irUvL~K= zb>`fJD;PZW%qS@>4PuaZ8g=nWA8Flf+qMOj)3BplLn#!gtI5y$%F~BOK8?z?4Jtgi zf4>rKngf0IL`&~Ob@(Dri6MQt{-v)ngPGFfcIq7C3s0bN(O3GtZ(SQs{hCLUBd^L~ z$3}B1-Mm3ikeO4)q!-(q5LZCIo)Crp2!z@4`QIa&)&asmi~D}}9(NR2UYX_M>x;zN zPAXMhT|Eqydy#5Wi(}>7lF_TjFur0ZqSWHL=mEpMdp`|$&-4r?^=KJN6Cb-4xaKw{ z1EC{SqDOz}ly@@oXYHDEs4m@F(MAKkP0swy zK3b!9;J_V_EglM#g(pY7V<<`hLR?S+>mJ^3f-HHrz^58}y*vRg`IXUuoi&WFd@?SN z@!10AFhpBWMv3DMbJ6tFvUPuw)z3Tu{v4y_KP^Y=q}3M|eNEr=6Ah|$IGuLo3L6qM zU8IC_OWc7(pd(4M1WU7LhaDRH3kME!$!SwrmNc|1ctu7bK@=`_}s~+3ba@$AeLLZ#t{3JLL1A*oa zlNGMnez>nCRf7Ni>NcbA&v#z&ie!;UzY`d!AAbi$b7N4c4n_U70pO6{1r6&L9*#qf zC!ruk8TUrL=DVn#Kq~8FdPv}#Dp^*A-b;p7w;ny(i=;_SOKV`z8^_ka?RxV@qw4K9 zV5wlJHBn^c2}+x!T)a3p$tZijzrTNxEHdqBG#0mLVaC`&9Uby3f_0^u0OT=jT!{J6 zE`w_E(T>5Sw7r2Puo#o=^GUlXYXasF;uLkhPke8X!EITMj}9d`vT{qlv2b{)T~WyN z`aDSYT@ehxl)5|{&xgBpQ2}_9EiqhjI9xY3=d@usvt{K{i&UE5w z6y>1l9>fu}raUk31yZ#=XouU{Rx%2$#Ok*! z4QAl4-&(U8?W9~-95n{6fOqd$BP(lwPDw89&zJz-W}YYm?yv@z0DEey!w9x>?y1#wE4ZGHxqE524JO)AM=+OjR7|{3< z)LF+3qMwo~41%QX2>6HG&aTwrmCg^!$?%J#M;YuIZS&y%P&>8^X0g)vJNIa`@EMJc zp%jxgDZoz3mmC~w?DAzSyA8G5=Q$dv+Rshb1eg!o+wem84M{kK=lgxX3H6Cojfg@==C4BOn^|6-{9X-S*wwKHB@omFWP6b1d?(iAS(Ccgx zYGJW77-+ZVuwbRk{1vkA(?g=8qx}+8OJ2RI0Ar(&FX`H$SM``mon6+rjnqZSb?XXP zsfAmpcb^_vV_9-l(4=^V&7W?UOB4;Way}{n)}_{KM2I}^k_FUNiPCC8_jGxTcgXvr zWfBU52ep8DsyU`Xu-?@27&(Tkb7Pn@3$_Rs*w3EzTH)$vYmKs-b9Qmev4m+8k6FX8 z<4?weX&wrLQ;RDW78E4?Zhex)K{33=sDEA7{6?Z+@ovjG3}-g0^?VVSX7rv2fDs=* z5DhVP#NG?!>?6jq086wP*L5Z8fti@~{4?#&I2H#2#vZoDtROBqXc zMrHY7<6_gj1^Sxhmj#j=!ED72bh9Ku@S9=(12b57Pa)3@Up|G@8y zYTz>HZZV|oYtcwXeJ2=}0|;B-x<9*y%2rtG3GMtDBj7#U&&xA{SrK`fPdJ<|@k>!O z#3TtV0Jh@ltNVFSzZ*9cA)EUK;2*b?-FpB|)l>=v#mr@HD4(}kB!HN%OA=v-$YutE zG19@pKA#})ldU!6POW0+*Ql_~YD_?n5o4WaM*(5kDN*a3zJF_AN4$7r@w_iZ4z{QuLp;nhn%=?$L_(yG z)z;2U`?+D@E#@2sg6aA7=J``j!NB)OgReT$6yh6?Mx@vdi{juuPN7R*YJ5oZB=jy# z8}9Ykf~gcE?f7Cz^m@!LfRsTD5>$K%)jIa&)n$_ztCOFvVwkDk0AD$MZs}k%*B3Nd zy~j`L=~ky(KN zQQN)SVP5KCOUqi-!~lTrDJgJ}1hh}sy9-ctD>^B?f3Fk5_=;$hb^UsFxB*OxB>l9V zuy<&hnw5V1qga;mZg_FbtcREy-vE+}2Q%uD)+#6%v&#Qa4l0gACxTw*wxG91tI)G$ z3(PhdseomhN$lEC7QTi>TTkz+_?*WMpI_Xl{XkrJjTd@J1TG@y1<8Ad_{KiMc)CYl zzPxzv`UgTqrz;?QntYw)KUM{8%{zS%j0>_{0cABhI_hV4u=ZM`o!dOW!Q9WLrqRVz z&%R^Y%B#V_Eh+f%#XKQH6P2%s=IxMVkR}w&>UxH&A&;J!sJd zxan#c$7Yp~5re-1PI;khBH`uS>)XaGxe(gLv zF^i-&C*HpO-gZw9cl>=Dh5R_l2i-W zIix<pzz z{~DeE(UwNJZDUR~iaIo2{4(yPLnAzZysO6FYg8$tblxC_<=g8m64GDlJ{Pw!Z;bz| zgNu55bnXl^@Gj_*EQ|SDKD)bQqm9nXv~oqSeHpXYM4*q@n!T8B-MPWePu8wk6MxV} zvHycb!jalNOEHhpk*Ws4wFGIR#NbC10OR;$co<~2jeHY~*xOhIL!v1&qMJb;J7gTI-k=#P|FLsOIfwu7kt0L%R}$hZy~C4T5q%TW zJ7nLU?~%DK>vA8N$|TQz{nRqpJ6KxnDRZ^Owr)#Y9}1aSLs?SvxV=||$SwpAfemXF z=QPWzcPZVRWrcqubU6|i#sE)E4LUpBA%YMzV6rF*WO5rtDEj1BBl2E{@CJh;aScpV zZ*zd~a03m6x2}eT5tRvXB0IB!&6cAj6mt;%UUDp$co`RA$7L&4bV|l{badPZW|$}J z+tx@Fhrd2soRU|pCJh}AJUEDkSls0@Ir8v;RqqcyW3T;YN+br6Ion~Yt)ZylLZ+3f zRC`ZWe&#N_+)l}=ZgR7Dq4bb1O&`}vN$DaEv9pN8kl>X3-(IA1l{IQIcY^!T#4${7j{XKqH^#Hc0R5%vuJt0Bi5&_zp2%OvyqVk;6PyPIJ-KhV1+ zvJL$9iIJx+?<3G1xb4lAh=}!=-3Im`&J$=czxcor=keT6A_(&1ZUzRt!@`Y^#e|^` z9Jo?nUd~?24CIy)`ou i^}Itz&;EppcZH&xzf%)8r+cUlM8x5E|8WzqK44jFns>5pnTt8sV4V$*zIIgJGi8H@Fk}dSH5Tc6WbywNY<(gYqRc|5-zjG_GKuDzbBfa ze6N=*+YNZ5t@=UOrAw&7Q=X~ACh>417PpvP;0?!MU`hb5V0tzGsxla$`;Qk)hXf0C-egOd@ z*|K9~BNL#H!@)If+x2`4Y*|QgU~uq4s}Y0*B=_rddvTw6HzJD;3FP_8a6Bx|8 zICw=#dATl>K*(Dp;0+tmSP;)2{_gYVZ;5dS5eu)yBztXZ`jcWMbr_M+T7xd%X|)PZ zfJ~tlS|82Nr!klA!b*~cn}KF8Q46v07>~rUtFd0+WjZ0;DGuSSMMe{L{rWKjd#J)Z zN14TaNJ;mBgO!-R2)d58+LSmv_R*ZnvDdC8ATQo*oTEcPxwVn~-@Y+PoC4seg~6~v zXk0YJg_#PmQF6UKwb61{AcB9N;ctn=aa>%wG2O0Fc8z=UHy)X7tOJd1akb7@FI~EU zHFFmmlF%%x;>tSph8J9THZBFv^r8^nF3EKl1%g*SN6p7{5YayUA1!zKtpe~M)!=ZN zTn32WES^1cNK?{pCugs$fO+a|jk3=<%gv({3Rv+zbQUHvgco9<$UWfs4;UGBFfE|CrHklfK5}J=1;#d#eMR%$DJ4YPGbH5Jet_Wl{pf#z)O4c@$P;3s3=0&B6 zwSxAYY7g28`s(+n??v4BLulIihlg_mIP{G9+CEa6h-T05FqGTJl&RNgXJ)=Q+h~`y z_V%GLpc0;u*JjO`iH&sD8m*@%_YUJI9tpw>9ORfpg69YZ39|6{v&`YTqyDp=tHX2j zAt3b9jg?C~nk=UX7C(`n++Uk2dLs{c`&f)GBU=RChu&)5z5&~C`}zz=`crdMh$B&+ zQ7~1fbgz~HzY!{{qnwWo3doaP0ee{$#}7x9*VboR+fHs{P~MAP;DSmpq$8S|nqhB` zrg^%P?Fo}Usqae;uM3J(?_;iBA!l;`;O5AbWh^iBU0gCyuC|6Do7K=z)h31~@w@S= zyu22j=1=9*0A8?7pC(V*H{f$u0LrjxAZc*^gtZ?@VWDtFW;@JQ6*ZUs7@> zh~k6clRT1@-xU6>tk4$qvj@_ao-9g5Djg+F=d#RsEN+=!J2BqL>*-d_71xe#QPlOCI5MbsSQClVi$Z3 z-#~VHKoffA#_JI9N|R&YH!E=J8hW211t^4I^IIg2D@|^U5F_VcS-?4!m_AZ|aP4o; zo(ztq+}su}g81Q&QTw=elPxPIrR%0L+TCV zbLbb3azix0-}F8-m`>`w&uQj_`T0r6a&9YZZtZT)co2TuyH8kc;*4xBA|0L7R zQpr%bn>>&{1iGb!bVWr`%s5U z;PAEBYRP~+78-GFi zW#KbtpQz~OC2Jh3MBYemQ@qkU)lxzzBWLZI3(#+(BKhRce@tY#QQx_Q4a9tgN^)n))`m+4Y!jvfQI?v|CNPLA3QmE+v|3=KnF^+g0SNtJV;cqF)QHLM(l$F9uP50!^-`~5R*&;pIsic)VKO*cWHpTL6{<(!?E_Lf1 zX8quKwB(PoPu1jw1*%lKSSNTph>C^5jrK|ovNxmbW!~bXplxOKA z{=yrSm*r7Auiy1*pJ^TbTs|nzY18-@|HMk68Ox?R+~de+)7=Mh4;C8=ODlDXMI`D5 zui%)zTKxF$^5mEPC`G>fpS#yuG@Yzv!>YwI279I)5jK{xR zn)Y)BSU%_}_j1PKIH&h+Z=5UQ0p+>JY$$?cBHS_F**X{^Bpo%m43yQbZT8weT9(ww zr1)kFckbW)Zla+(g|2&W)T! zS%MzgwwhqQeI|d+vi#6G;!1bx7r%|Vm6^#ue*6bD!FX=K*DC>0&DZ*A7mk#UO3C6E z&t6PIQ+`)B8}^?Y3^JuqMx)%Q;=^$~7pOz^p9h)#`|FN4UeB}XTS^{KubYHDdJdDX z#Npgv*itGF<_zcJ#Y!W^lt6X?-z%=Fn{JSamO;jj%)P@Alg1+YWSIgZT6*A=GgdIZ z_FGqwkcK}eKmW}&@GrAEZj7!}Yh{`4E#=F;}5e%y0e_WZC-ANT?VB z<&B+SQFppzP@X>epmt|JS`O;e1qB6JPi{Ymo;GSo#s%of4o`~ zf-HR39R*q0>#nN!$+P93{v{Zhn%9p$&6+)%ewM@J0-&fJ5Jc3qL}z$1goo_*61 z3a6uzeo#(Mjy6}K&IV|RcdHbn8{UY16QMNpT5C^sX4MgQcWFMpW5WHPUj=w}BR?Y__l9Vvp$Pj@0hP=^YZL9z1g2hOk??xpF|EH&9B$g+qpi zhxML%x}0!xE3T;6d$qJhCdlp6vy-nFJ*nn}7w;w|1tlaTSRFfd5l2k^(AD+eDK=*b zd3m0gn3z=0zBck3oJB3Ev9U4C;kgcm#N5tVG@oK`-xX*P?4z`0%Mw5_$#1gK(!}>o z42l-FDvkF92M4Q7VW$_4M@I5a)K{goXz#A*vl{gvC&IOPHk@{!8v2 z4N2X~4+X!WRphuhRQ^dsVcxwRxl+%*rGrB_wueX=yE)dsfxQwd2F$jEszV{a1YQ zr-gw8+mUS)+}B>KrBBS_&8Ds;R{*e`0 z_~Tlh(r~=e^(Et0_Vyv@nY>e_0A1hNaKW&yi-qQ2oU)0eOTgA~pHR33#0a4#r?RrL zbc5r(zGoK$l%!}0)sq2q!eM#JNwp9Yz-)U_DmVO0M-u2 zeYdrMXr7b9$l>|bo8cd^n@uPAU8t6#{_^;nH&-Wv0#~DAITZq2W*=z3d4A1T`RgK=cw4YVjycW?)wS$ zt+0(1wRd!AJmg`!oL97F%^Lsg?;ldvc zHm+QE`kY!5ugZN_k0ueuAH> zXf$4|3vI>O0f$C?IC-aMk9^Nv6IZS^6!rP@XVqz!Kt1niBkQfbHYMdR@8OXV3ly=a z3ZBQp^5**>D`iRC(DCwntST?;$66EgddgbUOZ{{vgB!nfwpLc_Q=JNRsl(OQy8P~7 z@Gu$I4F~ID>eOf2hV`GAcIiUg>=j_VFLT-h)e9Y7z{)Cc@afmKw(DX)DjFJiOb1_Z zu*GTebiLcf8&jJR*`>DJVs!vGR9V;r9bMgBnwoE|^&RQsBVAh2ZdO)SMdOep?Y;FB zyB%kySm*6p>2cesp`zlF-r2)>ZEbB|J3E6g&?T0$Sy##@Wo%Bzv=w-_l9Mw+?87@; zPdICP3DXSR^4K!F3)h)neCGwL2Qlk1rNV)Eb3aDWS`gct0=i3p~DTr{N`y`3-NJvz$CU8eYVUJrkbvrjKq*NC||y8E4rw1`6M?H2ByY zMH@YO%Lhuix1Qs`nby^TsM2Y9Vk9JRaGJHPZHz>OBa~L|of);}Cr)gM1roU<|N3IL zCJ)A~Xz-f+skaKSjN^@jv_85uA0(V-sdY-%Ic~eVJRAXquUec$pZ~Q%Vn}AsPh<1v5MEGIb!vh67yP49Dgnz#w@o{DcHebyrtb9tpM3qhef_1 z8=9Xr1Av3?SACG29DMTR$pT9Y;f`=&)0J=*so0Ax^E8i_x_KweI$W0i#5G~l*#32T zd*@|eFFbxdDvG_H<~`mM7x$Ppxad`*8|~@(3<2`-O&B3JPrP zkJ;ETdK(4?1`>@)fG9Harx&?)?b-)0H+*|V5~x%q=q=c?jf4)Sn5=mE^eGnZ9>dL5 zNk^Z*FFG#^_r2Hl?|;dvWF*rBqUTn!4jZ#))hl$yTQXzDj8s_nDbIs{Yko0L zSblSTAv9GE`xx@3^zGzvm2)N&qE}aoii%1CMz-Btgmz5PVht6zB5t%QxT>jXCH7bL zjC1n}k$B^Nt$F@N?f2Eyn>~ZDviuzi1r(Y71q1Y~Q$rt$6*Ir~1%ZqAR~mh(6bSRT zdG6V5@R6DZ9toHf!+q@>Yv*k7wYgPzLaclgz3n$-kPISpGAX#E|Mu#$7$DqX;Qt|TEJG|QD3*Mks_KvsaGLT9gW5k9qKL>BSxi`BXm$tUDy3pF%nywxM zt72tm7mQh541$?^WS~pS+jP@1I=q$a9p~s|+an2NCcZBtV)jjWZRs$bEPjQ1b#*z& zh(T~uVu|Eo@OiN!Z5toZ&AM zDI?ff+gd%5&4@L;BvY;nr6V!=iG#zd^)AduseR-D7|L}%d?N)M|+xhCdR)fB`PlZJ0pX7)%EX7 zQ>6~Bci z0zbmV+}SUPz%pS4*(0d_-T|ULwF&>ex2+FQNhL3tt~Y1R0#FbLyH4PlAsFG#qWOC9 z={aJ>3wdf<9E$#!`5Mpa)oe%`KDbVLi>Pw?vFiIOxjGx(O1H);crCLHW``3@v($no z_1RO)AB0`sEURot&X?DBs^_1a?$Kemm|Y;|-`Cde`Wb-L&@>uak9a#AHm~?p6*u1d zkdBCbV_SQ>1vbSaLRx{_XM(kDXYUlI43&zPS$e;UNA|UtGyChU0JgF7=gzgtA2f*d z?tU#}E9yiKk~$b1P-uRN4k7S&akuFi;MjdG^VGi1!<>PDW z&&*m^2SL!+bUH%i5Ae6A%B^#pgOgr}9R)*{)|7&Eem5m01kJisEG;ePv$6`iKV@MN zXb)n<%FzdCLjhEcToo4Sq{(%q_n3=7$Tx3RHi2CT6z*WSea~JnPvsox?~Lk`!U}~EP!c{V@0)Q=t}D-Z zUpuw+x(!>Wk&u*yh&kN;zD>(Eb}~<1yYRaF+i6xs=@w^$4PnjDk7@Z&bqp^a z=8CxUp?(@aQa&t#M`Go84w&auXa{w^zs3McS&t8SE5Z}tLN;|YpAHM&s9t^CrIAbZ zfdB&vUQn|voTy&Dw+Fclu7-ClkUTIrQd3c}2)pikZi^NxC7#{3+9}%z?xYA}v}UlH z?>>H9&SC29h=6~Wu5RsZzslF{U0$>(D;j9*un`f(3-22mWA)i8-vP@Rv|ua&OKbEFx3d4<67VjbWZ-^}ZnnA9?rTplBFkEccGo;sl@NCRk)1Or`$m zJ_%gh2w=HsgGE{zkUG;hqU!mrfX^}B< zYjpAVQ^V~T)pOfDS|r&9e%D} z_>Q%OBFD!!Yhp{x5pxpv+=S(EN1ESE;EM|vEMqp{0<W5NY+n7FToL}sbISABh}gbDK2_+24I1JsGJ z5>67W-Tv|F#}9{=C&|fnl>G?jmG*X zZ^6pO!a0ZL&kw?sZYAUN%!+CDOww_vJ*K!bi%cGIq}oCQiEscXVh+LPDQeUA^ft}$nAM!rBSvk;GnQ z&Yb844Hl2hHvRQ@Flw^!mBwex3-$Ec6w^3@JZ;(+HQsqn=DhMbQ+LsEdU!tD1XJBq zHTQU#gFR)W5J0NP1F74S;Qq5upUjSMM_jmoWg6RiP>zM=N0C%4_fQmI(OE2;WG8QR zl>{7{tel($F?r>fJ{AZatWmP%8C&i6XU$WqSfX7<1xOjmXSbk3zuw(bQ&aOH&9YoD z$npNpUAwe%fh})(h-Gszo<f{C zDU>NqRTRw~!x1s~O#u6zkudsfqVgZk<7kJUqB*)+8))tg0 z4PQC1mF3lz$s0F&*Vo@4+sz4k+G&lH{7fD*h*8uv@x7QnkU5&dtflm1i8}m<92A}% zyE}Iii{9Qz;0zawmVn1w7H;aP3JPc)^WDTcUdlvCsyp-R0A1yqfOUA$@=qs~M&p60 zNw52TP#8IgYGlfR|Ao5>Mz3xfiOmRA^!iObEHK;Cbm=z_z!E(*zPKLC|0@LCKl;o6 zP_N{@|L4vBhoUF5;D2T{_)TSvlP5e??wu*n$>x_9JxL~z?^yo7dBX(9aS!`Fxqp?d zik)3{;+HjJsnW~rSde<_redd<^o)mAcSDYp=9x3zwEq2*MTvMW*VGi@`gim+`PrM$ z5qRry%Hha?~r6!k^X}I{yrk;el>w__d_4ICa+GSTk5rKb*-A79ZN8haNX74{n=}u%y zQRH|gY4WvuLYFS+V+LnQ6%`FkTwkiJw|_G6Ave(a71#5p4fYM}QYj!@JI}=DkV+x{ znXVX!;3K8S??10x`%69S!1hOWcw?J4$ivCc{}A!|`=e(WsszXfE!3xmra61`SRDVU+<=ae5w-q z`mr}C81dC)n?@xoFO^O##%-R&L&4YV=K(ZpFe+I;Bxo={fkH_|H0wybnFgxwx)x;k_m~8U5hF&7KDZ z1{MI3Kp?yVR3N_(t+ti}yg`)2900IvBpn2pq9Ic@$iySxN=tLDsl%v~>j8s^PfWBd zadewvnh3zDowKmTNhX;(KK!+-E5%z@bMu< z>+S2?Arv0vEMj>RDJmG45a52AUX+oM5hRIg$Q5&Qy=pv?kq?8<619%CKsHQHKyq%6 znUc5_Bh>8+ZEHXQZx4OG2-s{aZ?&+uniYGjMs$lozGj!SRC>99X zEv4HSU=>LVQz3P?ZU+)!*xTEy!9}Xm1pzh|D9geo1Zn7yfgz3EPA(*wzi<}_n&l{c z7~rV{_*AlG%zb=_!Cq_E;fs85lR=QKsgFyur_`1$1sSy_+|cW8=TY>Q&L8hf5!f~t zY~Y0p5#7GPwJ~1m-q%W7*j7hjE6-!6d;p(-cL`%47)SNzSIo5ll1zY`?na;@oKe_Y zNPz%gD?O&c|NS(Bbbis?s~* z@Y0t%8t${Ou)kMT*{Y){gFRbv>|>eGBp;z}yD3r!GUTgVwFhCBb8~Xo#*lp{@ZhL* z!wcrmUpw5}0mVNtPylvx>x&A%a zVDDVwi*1DwCn#Fc(xUfh1zwWX-uUa~Cr#pS-Mp#D8Ppaa@8$6UtFY75t23h{BX00v z{%Ch;S{cRrsdn`vy`GQQw))j3<*B46utCANb=S_NcAW1tn|e;anIv^aAp$)eU9F~Q zvzO-<_$LX2S-Xm@7lWTp`}g|{G)lE(V*u%)aifQu(~x)WX_FEY+pn+6$7happ`E;* z&^uFf5+NW=u+`c|x7i5uP2lPESKA`K`4SQi!(=eYq;PS`c1kFKk(p%r07s5o9YY@3 z3}N~n?|wJRfSZ3maxC-0OW#@zwYAwXQUvPpc$)?69y1iPqX0lgJg6ct4e>C$e0+BK zL#-+kA2ZKHK3Vz(n=M_{CZAjNZ6eZkEFvJuckkW1_vZW^^ts#~s%mLBo$zdVlArHeln+Ld*yV$ZfLMr z$wbD^$|^$ghuBe!_&em()-=k*SXxbeQ%X!Vf-xGpt8ytE)A+&u~(Y6=7w{S(ur>AvBrH_xARvjEP-}-IteBd3o1j zOJ9^IeLEkFcxWt**+bBz)-7?&2iY(V(cRv9*MIxUnG)6No%f6|fy3yLCJ|b907wAO zXttLuG*5LJZ5qgTs~LyOb@m)mJnHW5zMtg__QOZ%8YjLq>e_`Qw`C9*wp^0L4wbwv zpDy+E;!~@fv%~3{-mb(_$&7u^X-`&9SHDvkI0kC5dpR|ZGdD%WfLcF35`^vA_4y+hj4dTdf9%+Y9 z&mMjb^rr!*S2=K?`XL2$zs9It=Bmnq>izup@7+`V%Q~T+?Wno*;Y>^BVTLorDa(r$ zA{nQ|<9^8*FI~*eJEQ3EN!iGoSx8tW0I}ldb{rKmy!1&28k2&HD*!q&i6V2~$Ha0Lcv~0XW z*SJztvgjuuH%W?`nVD6VteW&@08NM@#KV5Y7G0%rW{8$Yp#KBUDSoUV$qd_jP8hVl z`1pG%?=I{qS5oS915O?QfreTx&=G)l&DgIQga`#JPZ$*xp#F^c0gD~1Y;A)8-5jjL zV89P$&e3%`6rJo)Xhb+~ydHVT8ysOOff8N-Xb<9hckjk!0hi%mK3|7ygMYXK&Hxw< zW30BJA#YU=C-w1bhT;G;8X&K*$h2dfn=QdV2}s0!%uHDw^?R1T*EE{e47Ob}9kmFU zR@T;${R7T_iv%ha7cjLzHtWh`rzGLbAP`CMZmOyxrf;5Sa^VM|wc({P{mvSMCk6*( zjt3AQ zO0k8Fm|s3VY|akVdk*}14Wi=2vCe{Op`*@&KR$c(DMgMr7B1C&)6SDvg`cxOzkcC@ zy>Cxc7~OxP33^d%N(Z`Fps)gynrInpE3o-ewxvrq*(lPASI%`)_0XgA-@3JV_wkq& z6%~O#P?rG{R`W{eG{79&I4$6tH&DsAjO2l*T|QxSc;WW8)z}$ix&^`?;Nyr)z02zF z;N2EiX(`1Fc3@S;O73=o!#o&lDYpwjBLbB2d%>at&PoIX_lThr9!ub0NkkdwFSZAv z8hLx@+d1GxVfLefN3;yB(-{RQNtwRGsrOxVp2T6fvi`)XNwjy--wrMw7FFu&|PPeJn@`& z^m)3)HMKhglD`!jd1?bYXzwC~8yh>p2N7++RUd4-G?a;b;`2(ST)s!cT*5#5de>M) za?ROFP!XWvNXU+8mHBY%B@#+u{ARGq@0Ae(a{>*`^5Tu*rG!~n1%&V4B|>CN$Rt!84&Ps-3>Veh;)z(1XtPQjyI`{mEa9x{^HQD@kB&T}5>6xl zYgTovX{gMNzmC0J;pmi$lIoHzYW3VOU~!Wa4s|jD4?Kf+@LAy2I2cfsk^2vl)zW() z@EeB>eZzU?0|`Pw+opw^}p3vrdl#Y5p9&Z_MF)>wZ z%PLWIYwC?{x=R>X5J=EHtwEx3D=SM1PhpNbLH1NMS@Ei0QB8&G*hE%g*@7 z+0&_xC^e(4pG3DY9WmgO-oI&CJXCbRY1s|WH-Iq;HA&9O$S6c+^NU;}qQ2PJShYBW za!$Tbpyy@Ju-O(viON!lx2wP5`#BWMmxOsR2f0{FvSA$D;wwiCcAjb3!eCyUc z_5|W0tz*eO2*sl3ydna%?v_%hjf%l!nIri@#%6&DLo^GRcwzYq7A$ZCo4SXqXP)Wc zwp$AQZwT#$6@DZiwJ>%!&=3z166#dBmnhf&$vN5W{{k_lH8mHFe}#?vQvxM4W2p{V@*5+;ym3)G}=Hpqb2*_dfhQ*3dA zyJ2>F%feQ^tE^-Ld$#+bm1uq}A{T39Xo%b5TxVWjLYoD~UZaldHr|4)tn7wPI5+EJ zPA)FBGx9{Z1&VB+wzjx!+&5yGh2Yy~M9G01Bh8azsUA!p-XOTv+wDhd3a_NK6M&jj_4UWd#N4lu$0{MbR z5;n-cJ%8u$wdefx zYULk#=?lN&C6-@Wd9vpJkWq({#2Z1F_0WtxtiQ~i-+Va?Fr+;Jw`DtjS_u|upIGp9 zp~DzQW0Kr;cAxb4jn==4z%X1Y#$TI=(F5TF01X8ga05B=xYlUDrSyHV!% z4Ey))f%(t7{GWW6_ImGrl!om@{rIY6qCX`cKM&5K5naA!j*Taceq76fO6n$cxIO)M zEpxM#7YoDJG;^%Jq$yFT2Pw(NH{Lt3^fy1F zx1l73y3%x5%WLukS*GtfIr3UIJ9bp-#{X#CZe9E9CZ6$(eiL@}@fg_=q{MnsWwhs- zNL+)X=~YvTLMIoYa>*y+`GaB#r<@yw03H(`s3VL;uQsoc(mgoiqw9d{)}PTPi=X&= z^1CeW-ZE+Qn`EXmxf~g7ccZq)ZC_PgdaaN$s5HI`H#$lkJ90eN%M0qh(Jh_Ve_e%> z@hOX{{kI<(jDkd^KI*Q%$4B-_!12oFTYSzv-yEDoHMOSn(p*$V&QsQm7gF6V>YS(U z5V-Nir=FwdnK4Qe6Xp|f@{1|sv~e+5bN5n1^YU;-UVE8|ybBjzufCMZ>pE^fP(T}R zbZG9WG0o*KQ|iil()^eb+28Q%WMcL|>L(iAk(~!#rc&17ZC1tp z>Ph!ZKRz?_Xx*n`_X#C`Jq=$2j}Ioe`nnT4^iCW;bmEAth0_uI!6GIiwq-MR_KJz< zh;ES;6_phi-6$d=D+oM6 Uqh5Am^6jYZ*rSrX&Fs|w0^+#qi2wiq literal 70887 zcmeEvcT`i|wr{YZq5_JFQUw(hX(G~#f}o-zq9RDsh=?>1=_Ob}x`_0mBB0U)q&F2L z)I_C(9wbPBKq57C-rNEFzVD84|GVd&_ue=;;|w`_uf6wLbIm?~bDgK>&uMPnxO*cC zh1z`Tr1}LEYW;l_YHj}db#Ud7*f9e9!;e0B;VcT}x*vt|dW1sFz$LGK6v{yYg&H(P zq2wb_sGWCW3Uw6W#=7fTn(C-Uw z_cQHc5jpx?^zkgb5p_!a*hS~Ip-zl@r5@ez{irNnp@m%;8O)HJIAvQN}w#{X>4}DR< z9!N+&V%VPex~={E_B+H(0ua=5ypPl#W5MS_$u0~#A<&n<0+E|C!wdjJ8cxVN7$nJ z{z~c4YP-=>+cU|(!buCZN>evCycrSF{wk$n8+ZWAv$M>tOHSW>&GL9t)QN5TD;CF| z25G8Slyxqq$!PJL%+7eQv)fgF)p~vF%cE4OmAM3aH)y-LsjDlFjrGAWH#*hN$A@Ld zqH%NU%GaxOzdzbA5^Y{ABG6O6wJM0$O@MM{<>sc(%wY@1)YZACO%^9oaJJ{f74}dJ z|J?mDYx0LFD??D5;E0;H%U&XZi_ern+yc%@Ee+U9<_5Vf=@p_%QdYM)IZ!)u% z^19JWLL4?}d}=)KJUbAzHgQD+zWMQ~ba8N%wX-Q)bu{C7%zke$rgemQr)Xf_S(QB$ zVF{~0{BQIzCkGolu_%;l(;nx*vE^Y}doGr^P_9yGEAH-O=>D@OLgHsts^I5fmx*No zn`=v9-cm6UUv`P<2KS6{Ii^Bop>`$}{prIWVVi5yqb+C**;?Y|*X?R8w04`D8cBi? z!3S2vQN6-X!NeCE*Ae5e>F{J?M#nZ-P@Kai&aErL>*dqZnk7P6@4uC?%r*bGQ*370 z%+9$l%1JTY;cs_P{nxSf;x~Aho~Zv3_QG41FpX84p8id^eY3VK6H+29M$*>iP)5lS z3g@QJLJRI^o7qj(TmL-$Z(syfo#qvdbb(I7MrQ>E$B+V}IfO37L;!&<{ZV=vE#aki zwVT;P>??~4PV;{YAek<&3iYp4hv!2bmOtawIFG@2Iv_J>tjGhhYw-NpJ@%sxHNB~V zD3aH|VO9vY3GOB5llFf-wO!~Y&uWeBe@zVGw&xCydST;-@y>h=gEXf3up5G z2*Lg~&A*w3H&niKlSqd(rraO4{m*(z!Zq^C?8<$PnU&X{9EXvNiuVws2tUl47%;z1 z6q(Hqd=^Sm8ZGA~Doppv>0?)M9$pc=5PFJ9$!-;w+ggg_C1*=Kf3uD|r(Rnr;c{K9CpA~U@Q6jbk(_36#*eA?5 z;h#2id7f_09Gh%T2=qPmJg{C~yhpV%Hm8$&$v@el*2_7r=;f1 z73J`Rk|{JJe!tPPdn-&lBzai~KSkuxUd+!cm);nmnenirG-Cdm1f6&KT71yiIK>0| z&&nUA_@E?uR!qc61BeW2o!ZGTvxO)it|ydXi<-rNr4cB6Qv}(1<5xdzrW4<+t-g4So6O-+WZ2iPy1#E zeiF9psmn9_=bak!`YQQV2^AX}PD7xg_Q$ngW2eQ;PNoVg)~wV*%bOnvM;6WPZIJg#tj zduM+Wdn#Mof17!!XqRVAwdA!2N*tWCe?H-A*I4n?u5YeJ7ADfHgBpEZvyx)?ANP*_ zX6*l`9k2i9)qnYiRT@S{Mr6XTU%xW%{QQzC7$CQK^X5!6Hqhq|>n^#~CAg^QXmmr; z*;qnOPL96je!&%EBXQ0MGe7aYrG=B7U0Pat<0iE`WA!qc9v&X*`QH99_l?b?k6Qbh z)rL73i`@4&34FK|_;}gfrgjwQYocCy4gK*#)me5N>YuQze4?gs(|wM_m*>ZF>MxFc zoswR7Fuic_eJWaFO?`cRtY)g=z3(SZ^Y8s^2f0wj&tE}XY2sot`^)Y>JILu&IzMa+ ze7q62GA#Jz(@U$)LYuxc8iLNes65#XwS9w zle6zrH#0K}eevRig@uK&3bCt;m!19V*ROIa4d1`B2Px0o8fwWDJSZYUdh`68Z%p0WZ0efW63bP4giZIf#`gWkA z-%jzsu&^*?EYR@la2}CYE9}sKeVsJHupu%nDQW-U;Na_b?+z@G7w6(L?MRizV_j~v zl32bEd3kx*WR>}Wd`fzeYkijM#Nd^JyT82X{e_ftKXLb;Ec59?@or7+u7!p4E;n>* zmRT&pws=J2-o1MjIRxW&^6r|Nnx6a2ygu;nF(u8%KRPB^-)iK zZdqCRpYYq^u&{aIKC-$fY0KB|-@hlaW(f)y7#Q??VG;MYOfT3kFE2kDRhE+@y+ho% zgUtLRARxeQ-lg+Dl2*VtEqAb&&!g_v9 zl)iia-dEbPxp%&F*L!&B+xPExOJLM19`7{fv}{hTw(L?{#1+hq)-oH43u>(-ZMJ^* zpg)22Un%qSup*W!<=PT?=|l|1&|{)0HpsfG*yWR4MiI3zhG#3n0ZGv|zM8x)dy!!X zMHZ7DHuYdJ5rq$0(P!it1KWIMp`h=gW=!1`Wd352;z<}eLP+KM@mPCvFeO6E&5W*WuQ8P*1UifGO!bOEAw)4COGwUbsI=d8O9}r z*nEPq+i(DEB`-Q9CB?1&&f+|s>|eCFFzf9QRET$)Xwsw0r&C;Ai@Yg>3|c0#>&ArN zZI&4m`6}&sw&K0DZ?roJ?d|rXm0Y&Ph=7uK9SQRjO$5W~z|K++w;;DG$lL^j6n{`+ zWIFHZ;#wvs1XfvdTlAKJ1vl0bURO7dvXRQ)M46J3{mirSl(TfYE>I$)%yM>eSgCR{ ztZbHcNJNA}ce!?hXVA0d4;S9S@;LPxE_O4k_`H7o+8CQ|SdfKH-&Rua^9h%0oxXuV z8>I!bHm|1ry~XLSi+Xz4ninE&?qzS^3X7GD|KKHNq@+-!%k|A;U)Y0@!W3a6z-r`c zmX=b#hwRh3>sabM_%3;!p;qZpn~oOBZeL&D9^Lqvi#YmG>$Zy@i0$#|WqH6y%JT!- zK&7m#EbJCuqaZsATU&gqT;E_y26bXu>Bg^`7jk211~~^a1a)l6<|nV@-?^3%YfpLHHZ^?(-mu=9-%Z6sgDDw z3b)3SG7=7fa%}6v!Dn|9sq%iPpr? zSzP?9_8{KQXEJv>wJ2%RgtB8irFaZiOEE7jTSqOTmA*|&8@6}He0}vS3@>@w=+~^f z69x}vatpnB#cxD+sAAMG!jm*&yuA{-(!ba< zeKwyuc2By6wQ;OoA{cS@#*2fSUcaLd?|(wAB-zM>W%c$`7Y=dVBTDvyaT9LoSj|(d zZt!D4X4BGZB=&YX?XPl;JC2y;)GL_+WH&IsBL#Sm^f9$5n6u*!X_zWvO!cw6cKV@q zGh;ZXQYp%MkHY@fei}ErUQvssIN%vcCu+yB#ST@hWMXM;{gpq28vo{D{vegSE0wBQ z&oDV&ilbG6h@YlRZppR2y1$h*FB`91WNN>;?@D-jA$38r?eu|%lIL|^i$892NtWrX z-4$-^YLNFUdW)_Ux@wk*RDS;0L`_Id#pbpXmZw>zV)jriFKgVt@8ZFgMzQzzFJY`hDt{3s`*I#5Qe3$!gpFI^ofU7AqHEiF~fo|U%v@rH?}P3Sxz zuu-?0Gk`BN9CV?g-s+O0yLazKwm5cCabb!S8&n^!qGCQ&&Z?r=ORmI{zUWDS?>04--5?p*_$9v-|g-CQA3@& zWjp))P96vXNnm-0N9G}t;kc(Xoezw+rIt1@W0+=CluHcx^g_=#(tiFvtNY6_%43$D zeWCoy6YsPquEv2$3c@UqG1TJpckg!nj)?m#Pb(gCbVs`MIj^Q{rimz*F3?51Y>e%C3(O*8J08w z9~n`jL#^2fiHZ9zXBfwhIV%?#_+K)Q_i)4o?$Qbu8@9t3T)j#{yjQT0INBvWJ$ing zkro@`B_h6hVYE7MdR?THka_7dVbyWaTSv@Grm2C?f~Y4VSj3Az;03C{d0z?|z(W(f z`{)7~-Hv+=5L*~fYUwN$ zM**u1PWqsGI=Mc2Cn(F>)?-PjsfS7zCNk&|mOsBdmdi*B;F8feb?Owkq_i)}Ql3mH zw6AN@Gf@&)>r-mz*lH$6@Y^BkD;Ov%5QwZLC55ZG!(?GJ+`dc3i0EfTlrduA2McBg z9!X$}tIA|%&@XPTruMPJ9y8++N39KTI%k7GbzSo?=Hy+;`=6}1q-WHGru+K%%*@Qh zusVPrfd3pVr^xy(c{wQr8fCLpTzKh zL(##6>KO9`en-#AY7!Mi>d4eEU+Te&Y-f4Xita6pKFF`FUOYUm2Gzeh>2eh&5nI%*r|p z@q}A(*NiK%5dzR$>vq?n_ZLo{IB{a6-qtf)Q$LjgtPr|wq|fcuH-#CAtiMuw95}G} zB)TV!-qROt8|cmJ`gTmM6P&>Op{&}T5}JoC`MYGeiKh!0FM8#}OS~K`fD}!fyQ%p5 z)9R65;utyeE*}{dCVMh202~a(utWMkY)eN+M{!01>t;R`<>$+qYd*cN{DD^Zq^nu? z;zbT^<>?#9$|eXVM9a*dpdhNJzqtXCCCza$`J_OF>kuL7T)w=69P4D)5*Izseee4w zPaFw!s)%10h@YG_=wFZ*78mEc!!zBHr*;Bi{TM5-baG|rw|GrR%gjw-RRTeKi6Fkj zwG!KP;Kpm|=;#d@qXvwtSp2?jt^)4$LBOdO+JtDw}&=;%*_p)qP#3AQ3Bne**Tj8+PJN~UDHTx zG%x#C;uV8gtmAS$%PVyI&YeM3%v9L_Q|_iQUv_I4?D1nyD6q1Z%?fCo&1AFWtH*Di zR`~DWQh!RO5;Zxwxf&3NeYKHb83;b-THuTf)UUoWj=Vq)^!B@cpP(3JY( ztio(7)*OY}`{^A2vU`)z0jNAqU~!|RcQtFR-gt5)1~Ls55fu%9ASV@#N{AtuE29ugd$8M_R`Dg1!nVFJO_1fj;&=(>sx6jL)X-6eo-t(~X!mKs%MX z(q*79?V_`SN$kf4@!77xq{g9`Ou`1qnDMM$2Uv_HVfq%Ni5XW1Fx1-OEu1VX?i*?* z)1jSFSaIf&*zspS#;?)iO7t~NIYwMfKkBZ01oaQ#<6ndden5}K*o2;o1(4agcMkNp zspf~Q<~*x69$%5~mnfV(+W1WDVL3og{h%tjy-+>*$&n(GLE-+)f>DQUz6ShN_-y z1N;CQQefY~v%Q*Y^%PPSCm;dboLLzFhFW&I_c^Izo3=j~T_n_Rezx>JjewAlo-4*~ z7R{1gkA&|Brz*KkR_rXB`g+U4qFTNtzX{%kR4BfEV`FEhxe|)55Bx}!wCkxzO<3l+ zy_y>y?Nx#Z0(5-5!=^%)o~J_Bp_*a$b-P~0_&`H;;Br1MI8-ou3+k~Wt%QEi%m2Ey zwe<)TMwD}{T0)A=_{`?To9815G54Ns9CF}g#bgn^V9%_)BZ@_=dhtLf{XE1)jYxc2)e6S^!G8VT^ zt$S+O{(5~mziQ701;-&MES7(OG7Ug8mVW*&Q1$uQY93z?cYgoS5{F{s(jC(}U3dns zICci) zEzb?#H8nJ031F8PR*&ByjOw2xKP2e=6_R5e4eB5&qi|NYXPMQ$tdU!u)utel>GGfd z^XBg_nU&yu5z5WOH`Bcjxc=OU>C`1wTmJIlJIe)2u*VzhxR>_`^}hwA{u{z;q`&(! z0Zl8_%K{>Xgd~vOKHZib$Q1dfqF42)%Oj)0Ji`Q$8KeeW5jiG;jJWf^Gwr{V^eia2 zg`YDofN!^67y91@m;XbNe14dL1;X6m5tQ4vPZO)N4M8^^{uAp4-!GXpeu_6PI|>Xq z{f$}tDFZII`tZ;HZIq*_;=u!Q^2+0%wU8b59o5&@^r{oSFG00x&Duv_6wfG}Dtlpb{da%+Fc1=Za=zYAcSYBb7>h%_Kk2gMSlp1T(eq8t59ZXPilA zB10UOYwuo?+MwRJF>@oxopZ83AN}nqMnPBr9sLJ+`2-n!Ci3t;D1(C5deYtFCyoQe z6yvA56|}SlsJ+n8&>K7tk0J9aCM(gZ<(DB5s#_60d3@o*g{`&W;z3t!3ieTidtbeI zv-h^8CHl*w4f>i4-Qy)m)+=+Bo-7$X-7Fd6FxPlMxg1rw7j)YNh(AFSc`8nIKh)Q} zhJx4R*|RXd9%<1vVmQ~W;>i&m-?m%(s5MT03;I*F5WZS|)h92Q84gO3e919)Pj|SG zW1-d(=D=NeKt#k(W%{{_4vO*N?hgtV=URRbV}eTD=w>|3sEy}fy_YUttT37TYQiuZ zNOG^)ZYpOCucG2fcft@REf}BYzKmeZ7stg-d~K~hJeNgT=&rhUQ69AQP`k|BI_i%^ zEr0;@zSw?0hu#}WkGGMhFhfN%IiDHNHpT)I&biGV z0C1_|_O^w)WoV#9G(Bfc|UUuNJ z$B+;R6GY?e@nyj)iigty+;2_?WO9%l1~R7#uDs??CK&bq4=`v;UZ>k+rB;yvsg-Qv zZ&c*T&d%<)BW63JPvHn-qn5B*A@srft4F@kJ=s8qJq2i)E6*4b%FN&wP9O-cW;;X> zO;0j{my=y4CKz349pSsvp?!4m5H1G5%#02(&Je^Ig`eO+(`x3?g8(bA>dKO)r(aeaHf{W3oeaE^sze!6|Vqn@f2@d8i&cBGE%v%p5h+a~rkc^Wj z&4V1zMUHn0JdCh}|DFX7R*=%sb8-a&w9F#UOa)55#A{=vJN%BhL%Z*E^$Jsz~ zRv?SuZHW`f77c-ThC3>;9yA1`G;G^ObjNsSTb>7&@moJUApZgh+7POlvMo{7_|&P9 zmq>lxoM)FtovH6R2C_aMnF>E+(Gp?^lCBHwG=JTx+3uS9rrho$5k~i5fY%VCYX=4# znXM?smi|7jg$yXpDziRMVlMJS_9S=-VO{wcfy&V`+Y!+-6 z?a01;U-dN+eYfru63}|tkt!l>Yu$$WL_uw{td)+q-mq(|?aR~} za=<(a8InUP*g49pP=r_= z{t_8#0AEN}a->tkk$2hs?xsAH-*OXn5*GZoVW(R+O!IA!ieRn3cBH+C_TpH8IHJuC z!w`uzjP3Uq`IUXMSIH3wsgHg908z%o45GmI#`DF%;po2K*6oo}C+CLp)!M z#qn0SAr^_#h`}bDdi%BIkJ2({+uKMWV_AWl$|uK$@qJ2;mHt^vDq4+^dH9rn_}Z6e zk|kCKT_h)(ryQk9!s=?CHQsILBPn^AVggGPi`6}sUgb7~3p0YbU282dr@gEU59I(p zM83N+XjETnl()#GwmFsbKVP|uZ?)Pb^WH7P(Er- zySfvgi<#HTpQMuZCPR$~YN<#J`bk#m&pPQ;w|%hA8;Dw=Wi|xHM=ePh%+3@Aqk1I?inZ~X}Gw!95T%R{b8O8pgMplF!zkb zFS=WG`fxz}d6)*ACoApj${z97}OS8q!0pto1j<)Ck>rv)QhD+K&U;PW?F*+$7!&U1iPDUribLwnx9?!mdn~BobcB^ zz@&QL0bqo({uJnPha*RiRziIQu)07hCnRJ&bIPuZw7dbg{_6%rgsOV618Qg+lzmPX zPqtZR-u+ecHuYW@w5ueP9}v>(Rmyc?Q#knP)^9LG4|kg!_PKy$FC^sU2oEIFAoY%T zmQzqJ-$qS1%yg2E=c- z1EfzyWdK(Kz!i5Ky23U_#s?v#a-t!nIn56$-6zy50FTuJWFM>>ogxKclg>uJE-fv! zPeUp&hXBd)X~+lx96&v--DQ3x2&1?|%<#%LG8v+gKma-ji%s%vWkPYr_Qg5g?o!x? zAeCk9OOH#TvD8lSzi%)Hv>Bi;FJTIgKsw`YDG}heP~n1H4%3d6m?M3s=UO8k^sz`S0 zBp3h=4hr^it%c^a_fQZeX9B6-cJE{R1x8S#Xuzxi@+hFQLFSLOg4X>T!w93WpC$IE z>1XfXA!^W=fcjwsp`anaK@dqP4@r6TYR~eGPM|3o7bEw*qeHPfCR)ZePztY@jX$hO zkSn4N0l#mu`Y)Wd$2L08s@`qLe;2?YkOuONsv90jngg^j6CjN-v`(m`B5-5~>FfYY zrU_Ld90|aBJ|M5`^-X~7g*tBiv@u-oz%0&Zf&d`kN(R0z6Uj|Y_4qNz(7)O6vD;Gl zvjD)^0wA(ffP7-*Wo$Y)0R&8KT zGRxAGXe0X`3ah~epc2AkRXKHmgVJLJO_3QW`)zvh;spZ0a_NLbBq%_^5XuuSo2qJu z+2K68{!sql`;@yi2lR|xetC^$0Oj!0L*4MIaw+;p@L`I;q=K1rv9O#q`>Zg3W(HFd-o9PTyG-YXwa)Xl9}X5t&woAU z1Ln>2PM}iKURowt#vt&OHBME3XXO7iy_GlLBt-que;knWKx@zw43RVj0Y8!Fr?F>< zSXPFmgBG2ckA%GNFT*kbCwTB$J`ZxI0b_sDWjJ&gNN>YA4Fj{po#aFAPR%2)^t4qP zV(2XvSnZ{R<#~QcOYZ6nI`a`M{qMd=hSt(RHP7pD-&48 zYt@BS*Xkmmf{B{|?0I$|2Rpme&AO_B(%uj)jd~qv@W&UQ=mJ8z1)P8kUfJ%GoQE0n zji$OPwl zz~3IpikRdN*Q(95#w$)_L;89mH5^`oVr=7SP#t6f=#2od(4N@5+}uibP44g0_$QmW zHpaaJphQj00*x3`xG+Ouu7jPtk@2Rt|3RHpN2j`JzM~@D9f^H@;1_1gRebP<^yyn% zJ$NLHPVfUJsS(ZV2(kBoa_fY`0su{_3zR9T*&2HZ0G#idc0i*^UIhXIfaYNXc(h#V z<7eq+FY2RZ_;QJ$f)aFI1>R{zq6@8SG3|xcvmZP?F<0H@h6duzOAwFqB`Oqz87xmk zHdht+Dz9I?dPT?tou*^5h{3C+0~i}>x$MyK;3&3e2kf#8h&46PAuEZCh;rwmw ztaxHR0^eZ(jE2U#z>9LWg98Z4<4B0>X1tEn@%-%7CrX+WsuE+{QNqyCQW*n~19}Vz zaNj3DI|Gp5guT5zV8-_+X~**dBBW-Q6OAWG;Dyne43~gt^wwWfnWz<>i5ohI*|`_vbT~R8jS`(cPc(!A6yndy^Vh8 zEYkKIeZCB{nr_Z8QR$(BjA%rIcf0X09xOGsJt;nT;x4E6B-JgEf2_Jq7XtP61$0YQSU4%z1|t>@q{;f1y>97rlR0B%M=pwTvk10NR7feT%K zaO3!Ar#+6>y%@=uV|{Wn{lqHQ1Mpf^rWP=NDF8Eyb04j~1^pvwSMw^JO2=ZS2DCA| z!HKb(Y&F*eG|fdoK`UUFLE{g1H_V@W(REh|e4E$;UdW`je6-L-z;`~xxq701HclQe zY|0BGLD&;JjX>xCMZUVr&%N2M3Sc(d0jFW3IQP4p(K8Zn^1<{NfE+5Jp+hQ_YzDG! z*K|p5`w=OyGNIMa14XM8!Da_Q_IC?Lfb$IxzT|wc%f?GS$Yx|EuK5z72MI-(DWhX# z(dxvBuU?ujd!=r^G@K31WV(lRo1x72f`qx!V-G>}}5ACER@ zR*q*`ly_AxG2ceg#aG5|j}NLN#;f%%qn1Ew10QjdTObnMgMrf*e{A=bO_QBR_X0O$ z2DFhBQt&EBqs89D#UM^DUo$sgei*E~F44W&|3O{08@+ZUcR=Vy33uf)(DXZQ#2x2& z1=*LV*A*(q(I6J1{Ng8MlJI|CP4_EA+`0M!orRE ztGPEzI4UE}<1v~1W6&TMwmghc9X2(LhJ!gCxUXM8yeybBuWHYb>@1s*mJ}BG|$FF{ByKOymSKp*ihH}T|`U3*%{(hijRd=5ppGX zFU76Kkj{fOgFBB5X_@frgf&qul3}(m>ZR9m=|n$Tai9gpK~Tr2kkww}1}87}T%_FviXJtSHce_XNs{jDQKsu?aQ)X7dLwNy$veisx7Ja8Zo>J@eJ z>&BN_P6V!xzCp7b#v^$Hd}{E39E?fR=xs$Gzs zBB}8e@1=0w=!btR;uAQxG3e7x_{XXnk+RW3?&LK*a#V=EIGCNv7?8NiM8^l5;bI<( zvpvuUsgLz4Uap^O_^ohix^w&Zwk+c<8`{8Y1v3TYfSfa@EpkkR?EFVv5|SBXG)&6+ ze*m|?Cjm458OeV%>+st$X!_goxe(9QV3(t|t%J7fB?aKn7QJAXn{~pqh>nuD$*8W) zJ?Ha7d<`gH9cWq9HXna09KRi&j>AzNk~17!+fT9<-dp18f)bPp*512g{^+6KE0LnMwf zNdMCBk5a3L`Sm3lJ(kot=0VL&0unQz4 zm-PYrQlD&R%-y?RKaiJQ7_eF=tJ00ifTfXs*?+v#<{z70h1Ry=o#al&MaE7b$s#ZI zCD2GGsIK5jdk zbiSw^?xPa)md@ZYl|@u$TbrkxfIU7&(xZj%6Ad&@tY{kGDXwPaEA8!uRPfFlaR5?Q za-_9h{q4S7mF4X#LJZhAr%np;88YO*fKVc$%@pv;k|3V+FIiRW-snsJr z#Hr!PDHe|y)Q3MlU2Ev*=$Li@_#(&7o#s3nkisY&?VvnDw#*7dCH#QYWaVg^7L>$M zs+G{f-SC#!v@z>C_5lA0q&aNP6}rD5oe=G?mw(S+=S!fxe*PWzLo$U=q1OFWlg4nG zmr`Q&v9hV$G-M)Z$c+IZH0(x@m<0~^N$8#|E*3|8e$`chilb0Vg8~nl!|-amIO&sR z27r=K{2P!R^xut{TA=TnmPwf(w(vTlw9wn6 z>DBTYpF8yzF+G_=o&YpLdF@5=(Gr7J)2+Fwf*chVw(-xQ!lLmFIWQ{T#$!K)Q}FbL zHKzAme|`;fem(LIs&3I=_DD2?@rMKK09_67*d4y2+`K$Z$j^j^g&}=Q&9y_90FtR* z@qy3@Q9jbUVMIBpBu=tZ@;__54`waTRXTsZnH>Q)c^#LVj0kZy!(4?zBcd!S`wWtVuyBNcnJ0{N z(mZ7V&uh;M2y@>8dvfKka1*54b=UX|1ol0-?KTuiCxBzeKvN%rZq~4{82DK-P}U5Y zCkU6#f}}Oo5g0J?n0c(jW#eh2SrX}XrC}Cl5m0grc1xNAcvvHuK_02ZFKUP*O(4Lk zk(NRL;UJ$Gtre%$=|i>*d@8w&hmc$|%fL!v-Pj5b|)0_vrlQ8W74c9?f83Sle^b_D;f6VYGTyj|pS#oC>QY`_XG=EKl9{nc+L zy~+hz$$lBKj?4EKPO2f@C}nfCWkU{i5QtnKZp)cZH;49%hsH<~EA*VWFR^L~xpRzE zOk7BVg%14i6Ff11Kx#nIYJ9=n}*g6D=>^ zz{3!q@adU&I6&X^#1%R;@w&!lK<@1(J}!2w7=}4?l~XC5`F3ceVFAF&%yos3h(LNm z+!V>|?2NvL39`!Kba68V>4P!Ebg$;CD(Hg#5O_ME$`!B~+hjWKs3C+x4w2x@@%0M= z*Qqp--+tJ|h3JJ|(IXG$N^^5fD(@4FYu)BTN&C0=(o0>tb8gu#ni->6>GM4A)%ZNE zK_Rfu@|0il?D5YZ?_ue&qkU@%HoKNw)h_W?S~UwWh2c~32j;l`BVK< z)w(KN&xktfy%%JLy%$HEn{~V-hoHUC2be~^B3i+t0}dlC33U7YnOUJs3=kI}J1syd zNZ*aS?$w0A6*qzOZH~1-M0%IkWGO&AOm;(#6mTBt!Ld@2Y++uPt0jU#pHCn7rn9f9 zsR=>#O9`1kWQzp8WIBdwAFpso^MT?_e}WvrJ5X5e4M?#~@kk|f=LaIYJZMNqLb7q$ z8l(*d>1>fQv89xZRB|D0uED3mvU!Fxt9kw9~W&lu-SJjCokvn!BZ!KA9$_TQ9ZflmDe?UrjYd~ z`A)l+?9Rrk*>zJ z2E}$-FV`k9wTL!8v-=nuDt&NvcM`>caLce+TkURLl%eLMQCI6ezP>d)s+m0oY5NUV z^M^EwruoT#EUQ76Xo;0xG1IJ_s7^}Tlkxe$o;}9Zq8Yzkm3oKHA7@*mYF%({19U(q zjgH=JsCN+}yfXqBOM~+>PKSVqBs4Z&44PnJVF|#3txxhe7c1ZNF+C6Evo|p*Y4Fqi z)lrAdeA$Pl=4lu{i8TT)tZcGSQ8*iqSt$7aZm+nwHjv36mU)$7azk}p-I2AbhOJgd zMGf=Md(2Ksdx}xXE?^Ln(sz_rMu&#Jf%a<=85zPg{LJ~-VyB7ubg$-WBkPrApB-9LQz@LV9JY;m@% zWSwdZWIs;n^4veZ=~XxP!{-@d2SY-iJ$u#Da~-;anVY)dA>h+_yv0K{r39S0{g5RtJDPu;JSLRW5z zCo!!ms2W5ZKa^8f8<9EWdvbDmNg=w*av-cZQ6oq8A&|k)$8;6{IejOhg?sjH)VHZk z#nbzBQF}Sz%Dae&{@~{CZ%%FN3|Bh%7Uctdw`EMXxc8rpx}n&WW#)&ar0W*>b4q@> zC+Gp0Azq3C_ZD>FmH~Id`egC4xgQ^?5t8Ut&4dDb^9C^VW!!hTY(H~gX6L#evL3%H zHeX#+bC{P*CRtxU*kwK9Il=2+C(D2;A>tK+$P+mC69vo~)hWezFx1j!&Y zB@!^R?Zn+dW22*UfnOaCMgZO>a!^!UTqb}F=-7?2aV;%Z8k`O3G(QS$&w1TPs<3=E z$5J_W^|f4ijm|~a@>n+x)PE4Un)gF=_2JvaMn97#K(R1XuVzG!!9j<&U*{b99&VNM})~(xAi(G(chVN zl78Zmnf##W=mTJngOZaLyCD@VVzuX{fx9b24EDda`H zw3BzD#_SMvF-g$aq*HT=EOfthvQ?~MqUvjX)H@R**LIKUeUZ9>$In5 zsX?yQyWZX#ASxQj9?Z~bw(^TqM_QM)0Waj9*EW%C<#(U*w7e5n2B}BLmYxcMvS_zM z6ffbODHsE#!jJhu$pGr!BS)kj-`=85QfHuQAEgDv+cXquUEWsC%c%~P_Njy zxR7Ki+69hw0CK!DIxHm>z%>BBO8N9xtksGq{4K-TPt8CFyrofwrOy{GdLY;5JL?Pv zk(Phxam2W+LBAP`J#_c(-6POaD{b3l_*_W4Igm%8n3@dje0wLj6`jA4Evu`mj}4Om zqb|)$?(KEH_ZX~R(mMmgHQPSd z+|lC^(&iMUw1K z*V}I4lXSRrZ}59sN@1cy!AtZ^lzjF$Ma`hdGl5_Y&#g~{F2O42V(a#!q)bYqSbBqP zixvE7*OkGxQ1NOFosZ+~%6OzADl zNO@%T41F>-iQJa_@zV9<#l&I9p@rknJSeTCr(M`8-uA#=w{2jJY^nhND4k8j3W)FU~oo;Ocjv8#I9W1s{Lm^<7%aP&q zbc+Jw*^UibE%>_8bwva_;6!^4xo zAwu6~Cwx0;U=W236`;zHC_O|;`T_F?&tAOPJ}N)=)YtdNw^wS?_xjJb4m(0V{~VwD z@B1KMrW?<4Y~MZrZQ4J;FxlR__B1F+W;i83zlEdbopxqsW+m-@uiU;mNb>t*BO@b^ zJ(f)6X676SKhsT-0)NIal3(I^Q2WOh9BFtU9D!vQsdWcLLV|N zynwwueZT85x6`~(bWW3~;)QJ!9-fx9dMGvd#rr+pmMn^;Ps70I9TX`ekWHv8gPa~V zLsV(hg%5UqM6b;tV<^;>uk%seg*d&Gk~wcx;05E~ITe|{Bwm8b=SPya?S}Wa@g05}=HvQEWf*2o^&5v43xli38^)1FJxrIhW z-QF0G@3_-$u(K%7qM*3AxY=lThl1*(>ot)x99&$MYmNghJOR+UB0@q~XC)OCqHI}6 zW1?RHrIFfMB)a*mKOD6^rBqd2{T*7vLDlW^F2A-Li+D^9ZiIJ__YS~bJyK5_pxbK@@mc-B@@{eLPnh7NP?{P;mSra*je7i z^n$Kprza|gYJy!!5A0~yf3{8Z9DflW9v$X1+_t^Z5!d}-uaVnu&K{-k97%A^gDkEa zmP=FNlnh&o66$AOa;OAVw|o=}|?RR`9+#W94@!HXiTVGW<|6PJDhQxXUD`dGHz&YWF*kab=UD-VnbuF;>(! zlpOb#lE@->s-;ECf$*DQ;qlfUXJaGjxC>>mTud+1I~?1sD@lkOis<4|ANDvSKf@wv zKtZT)@h)E%wvLCq3&8$s4a?rVsrcCOPB-~mY!wRYKr`$S z1LqLJ|I_Er9gPz?bm$0gkyEwva1P;td~T;rc=^xu(-Ck$&lx5GGKWOE?q|fMG?&eS zSU2exl(NM7M^F=w5moFXd}*?q?6~CxpXb!)!D}Q8aMQV3m8mLHv^8RkONVrfRlfN@ zecBwWCZ!aY*RDNiQhQm_TpIQ1@QW8~&N881ISn>iAiA71y=;z*^G0MG_BWtceeks; z2Y&f-++*sex3P&mfDQHy%!TfQbj9H~r_F2gPoF-0IQQ9`frez};z$!?<8)}pe-Gq| zQgq4E^eT_eBYL*eC}6jxMBP{l9{uR8e%=IrWR5M-7|a(_IVa-d?fdFc6FNOc<_hxj z^}Trhe76$@Y)Y)W!-edd^`Wt`Ja^`yp4)iK9r61{WQBw@;i)6r{V5bGQ0NG_xDC#Z zLoMu$&PV_SQo+p4dj2ZI;hU3n9lxp-c-7GLWmnKNsPZs>=|=E*(mK{^DaHjYSuDx5 z$KXBpOeTec94m3tsg#~}kcvmg-Q8JIv`}oIaaUn#Kb9sBPc5jwxRk60xr)UT_kDdy zVF5B%h}_DrSqZLPUWf$)uR38sE(y1WE&Vzg{ePJI3aBW%uI&L7F%V1?BvceJKn0{> zKtz#LLM2s1x`d$_FhM002_*zYI;9z6Oi+=Ip%D>*85)Kd>fZ;{$M^mI^?iS@|6c2P zaO#e8_Stdmy{`izx_@9^i~r&F1*3%6swheKjxv$R(H~FL!CCX$*)t&q&o|!elTpP9 z!jlN45Guf#H`>3eyYj>REuM@F>(;Fkw*RhREPm*aGH6Pin~g#BX^32*0IAQ}b6Vr< z^l*ptR9`FqwaNZk{B72g_ck1p-v?z37WMJ|^GO53(n(24@ovz+zq`l&#*Hje`qA`w zwJ3{L{OGs0_}e_`Jz()`!4VyDUJ_x>#SFi+Ag%|fns&~0-rdvd<=G#~)~ zo{KAX4n0U2mQ<}y&~H>MamUq{spltwr%te(xtuX^NzmYNwMP^74vd}Ur|Cim5}Sm> zi2@#jxnQTWXMEQWj47XG>A%Qqd*bd%&iiZav*L2H=k@R02LF7tRdEUKqjln=VMbe| z%uvymOVI2urx>%Tt-uw0zoDTC>WI45YFx>=Eh8DalxafJw5|_Al-#}$>irANl z{qub~pvz~9gkdc_X#ldLxP$~tm`fVOLn`&bXX<9gdMNY7Q^Os4 zFhTJ=Wx&26eEsW=4$wa2y10iMIVE0!A~1(oE8gK*WMpb*N#zn7GQptjMVtmND)pV@ zSS+KklbbWI+%6A3{$dnJ*vx<#qiiM;4`CBydsCYW6io=5*YWnu&v2-pKOfY!KoV}C zFW8G<4R$|nFR6qC(~_8AC+m{V0^0YK1+IGUhNkYUSQ=HcI@#D~>&FiY5r!#=V(E1Z zYp6YeFQNrLblI!J((;^BKDl@$o_B%2&x=f*Z;AtF=-k_~BV1hM8Vp~$f zk9L>0f}IyAEO=IKDH|!SXG+?{?J@JZ$z%S0xajl-CsL#R(9{zu9#5%jrE=h0uih%p ztp!~fGFP>^Bho&Wzckvpb0^MOehK)mf`KgyG+P?-tGw!Ru#Ll%8o1j`?N*0TlkN)=u%~Pzk)VdbNb?+ko=i z3MRYVOpudcj}#=fCeSU^SrYdOpOIa3QTrIThKwTw3z=5a=C|9t;v_wo&f|k)m*i1B~KT=E;>$|Y#0yWz9hX1 zvwd!?+=0ranAxWOh0A4s_#;V&DXWseMmXyQP-+Ri+#WxUsb04ljAM6nAS@P+N=uJ_ zDp*gBUF~8D6g);9M&B##XlE0uXI7*Mv9brNvK0y zSaV5#=6SkR$65O$0-6x}KdolRu%175tkz=twwc&4W#g)@i$ygnQP8zfjArI0jQ3?Q zXS#*!L;GLMWvf&>4?cQ1*(ur|_3_Z;MCxP2h4oZo%atQbu~fM+28N z&qF&A?2)tWh44 zt>Ihy2-4Za?ZoMujdAptJzw8M*e2T_jg6Ok1XXDrA|9DZltzE)j0-+f7hF@_%~kx& zOGNtw8vvxZ=oyZ|T=@oO07Y2fcGpkNtCqO;e-rhgE+*$f5=RIGFcU|cWT9D=r+ZUA zmX{}KD_h#c#}4!E+t=9t(^Hi+y&wEAP~RsahHm9k_nkPQ12$96d2^B=fDB)%sx}VD zMD>lzOGMuJ-nZj{=bd^NOv1xd0f-oEZ7Ozso2Fhcbcexd)CpS~b5jgQ&Kd8#lnl}J zv*#los(c0=NhP7_pG;*eG1%r$p#325;Ib=u`qWI?rAwNt{`o!wdptpCT6UNMlqXH( z7xb8SpTLruy)CMk>nZ5GpiH#pFiSr%7>zDMoYR_tIv{>nixtwk=z%aY^3112bbI zxo{!sryqR5b5q}A@*s{}KjXR=gL%x8JXHi0J~n;?%cs|?T^mOvAYh*Spjmw;NT5Vm z@pag^>L)vm#UW>|YTRJ+XL44}8RmXcxtW)2j1l({Sl}`(IohJs;VZf$343iw5Vxba zoLo{SsTi0|6Q)W>0l|T+cg^Ud1v!cYND6G8rHuez{tl6j`;~@ka z1F{xW5TRwDcFha6W6?0EYYt%&PQrinFF* zZvN^;#c#BC>e~8lH9fP*$xr0Cy-s#tA8IuozS^_`D7d^W7vz`L=$T;Mhzkzp8l7>y zXfHsnyUQpXNyq9naymgRYNHY)DryRV9qvO<$q#_b9s6ojN{DX$!yJ;WIe26hK!O1_ zBzE5B8MfHMi0v^}HctRg@{$2~+0eR@-eB&Hej7G|t%z z+BB(DM)>TvV*5Iqre2jKR8r#Op+A~bx{xkEB5)}wI-sCqQum9tbwkkwQR{ifh{ltc zTT=~Ni02^*oF>4)lmy@>B(jW{y><7155{C^Z+~R6T}XYWW@(JQl8SGA zPM6BY&!seTH#g}Vh6%aluO~%6wbRorIcwbAbFo(@#fNScda@+#3f`0o zjJyCp|9x{kjLXMYu5bgScwAPHUTzLy1BO{aMI|Sz+;7yS(?&MQd3(muj*D{uR~<2$ zeOyjJcv>_^k)HHcNcVIfT`&}DV6|Y|jIpP|S5P>6QRbXUzMB}v`)HpGE?yJ4pu5We z%bjSIvj8*ziJ1<|>d_R4N3MvP0FHq32LdVYOda4yU8{*t`SN9SHhzQmD2iA+q;DjW zoXXZO<-W7DT=)g584_EZetgO|Oi;#nm!-}Z8S8|xZWp<87VbI^pxj)FsM{*6u6|Ry zu(qFLX-Xs5tTRD69+_n>ojX^3Cd{eD3o~G5dM(LcVUW%1e-0VuUjn7MAZ7U~D7~P4 zavl^Qz(y});*T+;F5AxW)`+tu!@9YaZShl*t1vNiG0<4blX}F(nIBLrIr}Qpp~V-k zc+t}rT=`iUU)ffI&mKZrZCe=MMQLHom0J-UiR%;8mtsV?y*4}A?Hzbf(Ks8fMh0UQ zNJ7~W+bSb7>LW0$ho6Eliv#B0d8t?)Q0@AOUvL7-d^Db9n5T(osxn~MH!YhX+z+$D z8(oH0xSbew-_8<(Yg9Gia?f^S8A~JNBzhgu>m9op&+A`mnF!8C8W)$h1b8?3;O_h| zbB)?Xe7UFKjb$dK5yy$!d2g)`J6SKW$Z2N1G(CnrKU^hhpc1f?T*p(T<>)Rf8hm%J zl4EU3P9AjY%@u`Zgzv$9iTxH`A9(sR!2TjhWSDbN3OE}HXOiMTxsBo+k9VpqBa=Z2VmmfYNF-C;OIMD+yJTh8}sX`|0 z4Vo)Kv|wp#hB0pIy93g88VEChywiAL6)3pG$v1B$#7iwXVQG!KbD^jiA}C7EJ#630 zbW0{aJ`TI)yVF_UIK_3-*KkK|ZXh^sDAD$;cc+9$J8CT}5zgEXxGnNW_V3qSJ7#Zq za&$4i>Z?H}Z@+ghawQ=7BRn`I7vHCuZV?MJGh4g}xa3Ax-zU@Tz+S9qG<2v5+}Sg=0Ig94gw{UYO$X15O>l}HKD!^vbF6%qn zxJ4*UU@fygti(gdb{is8f2Am& z^T=E`rFZpjqF*bF`w3Wqg=6Y(JY8dd|v9rnKK$0}1u3cbSTPYq*PII5vZ#40M^pT9dUW^{&wWrV33zPl9^mkBvLO~bS zCT==uv2B&yHH+4qq@&UeZbq;z)zo-qf|=4Y|ih zle1fcL4!Mhd&!X7c9dJUvefid)-abjE{Vw)qDYkrHlucxB!l#^qRQ6~&Tb2Hb9zea z{@UVQKG*&A(t`V(TD`;lF?$PJ%RXXN5QBt{`Qmg($rQFd(EWvR+GqPi2Ls%9P2aOI zu+&ePXm4C?ius@y#**ZuX-dlir!%)CE4Li)vx2sa6O2sFe#qbE@s@>7wbz~V(c-ig z&%pk{OI=r^`>V&VQ2D=4_GD=5q(7K=e@-x zbL`LR1q^J8!7bg3x#j&YiHpk$khln>JipvB#3!9M1Yp-2)=xC`6YlyG4Z(Bv6t~pA zeA#cC*w^k~bw0wl>5e<*m7Xpe&de)h%ncx^AU#(njkM1V&pJ`w>|tne#?p*Q-szcZ zKce!@7w)O!%6b)x9g;?f@jr1EE6@n1GleScgdx;uJUAj}}DA zl|48-j?Y{MaI;zeH$<=U?ifu1{L4uL&!g0y7$M4FXWmCE7#TOT*ad^M)bqoia3*r>p zwpz8H?-BpV04dfl2AQuE+nR^?a)0)yqZ_*NATV%ZHk>H18^az5Q7}Td#`@}>?nqIV z=WLxH8NwR{mfjhaB$svX?bQkMX1xer10*~S^i$QJOKDTUZqK5uUO`^W@`W~MVPwAz zPrY3%^2O&F=Qr-7$7m#=)KASfVOuUs+*YjcD3`zC z(<30};S#OH4U?hmSEsg08h5@NzCJlz7Xoo*rR9Z3*DGH(B;8gG(t;Sv&2;R+BbC#) zKLt1C3SD8r;DK-0a`cF(yUOXGmBC6ZI|I8Si5gw_3s-QnOs_%N!~HWtyC^>4;T;_E z6iz93l0cRDP5TEnZL{cFJRgXI%7r~jIH$C%60>}89zve7j)+{V`=0gF!oei#)dU@D z{4F_yvukUhz}oxFX;_OG;R8ie9!VSo9Celk_v)DX-Q`T+EI9u7wubE<=d%O0y*)T{ z?O~VssnQt>uTO!6aApJ=J4eyxHC)|mZ8(oCp<)=YTWXXE!Rf}f*9P0dc4^q(pBC5) zqm-`3X@x3<&a@o_4~WlJbdvtFyD9R)?g8w^SbT3SZcRy4Q7Kb_nt?uXSBe)U}PUGS+0 zAltRnKH?hr+LBEh`YJmB4$3Hv#%44$-Ks6P^|`A}x`fl2b1Ev1C4SmLp@)s)hE5fg zJnw=`%0X$nOtms{1wei53a0*$HaNZ-djGJ}%T;#l!3d6qrBfzK0>}DNU%3>5Aa|9w z*-v}CCS6Q_Xq#D>{e7OXC*u8St#dnDHwwPEG2UPfcj*0S{nk~#w7QBM;uXjS*;cmSZhu#{^e?^&5ruD;|a;WHmH%Zec*tfvO zVtag)GMdNZi$2ij;+?sL^!+$!2MmuDlwX}>WT^=cl-&&@tN}Tpt)$dvji|wcXTv5$ zhly==aUM7eQO^tXE~^t-Cfm4EUqguG{So3@QpWSA=HPh4b&Z$PUaTjeI3=qsEq%G@ z3*Ps>H?SuKQak?~*rDZ9|9Qg~qw;GLRXaMA0C{i2eQo)a@C`-@a0CB26fo$ux5V^B zw0{m^70=;)ydN$>UB@^2;v&bm(2V&S@^Op;D*%A$5g2ZO??Hg@4$U<5 z2G6zMH;LsflsaY@##g3?AOVaw(;IN*|2j5#gF^l1z^+=Bij_C+CI5H%C4G=FV4DPdEDxG~H{^CA>37hn&kNKbKYNgwfsEzK8b3Esq_IlJOY4R<6XCu~pWHYkDnPc8*OzsOiATh8RBJ$sdStdNd zrlgn{EbREf&E$RO85Ek4Vf&BUkoQd$06G4B?AaKFWwi&c{KeA%C+8A2%xGw9$3Yc# zHT$uvccAPsRRFbb_^Y_i=F@JB;Nc?gIw2$_ho?v`<3R9X;%>O#0{~#}#nfR@MjbsW zXsX6I_%LSEEc~owp4^2d%w3c^a0=;pmXxtF{ER*1x#6w^uu!tuxbM$N zFjxg&a65Rm0v`cLaIW28^Raypbb-*`xeJM>9OFJN<3b}vZb*&k8HDQeW{7a{@#Smj zk_2?Z(9Om?)-9Nk9%{xd68NcvDt77djVa3KPxacD%+wkA^%|+pue$Ef<9P>(**I5~QITFGL^ zZq`0(5QS|sZz+1(8jVf#ath7)w$bR?8@Z$sLF1sc$s^AxAo+4h6#pPVTWd%4HjkWk zau((yFD*!UYt)BI)UyxcFiAG*625z(9XEKn-fPi9>I&4?cr!v~V2?Z`Noy!hlqUZ- za?*&#zeY9;>W@<@R(oeYj%+{w>^6;~qPDiP$Xl$9+eewwW0z|;=I(Z7I0Y6N+!)Ut z*i&PW`Z@)fWtMCQ-eV#UMZF@zQbxS9)d zQyiQE>~#%?sc-c#yvKIS-DaGu$3?qx;i7v5up|2SFOIoM`&b)7fwe%}6%ebML2 zH=gJzeEW>yM`C(YLPS1!EJTwv`pH#Em8AQ%Q>a< zH*P3LlT?REayQc%JPbx=faSVMV&6n=7-ca^^X-}Q_Mb`d3wdfb+eW1ZXXll*wOjTz zF0%K5Yp!dQc`hi&vYL&MJ|RMGej9&CkB>yw9h5(8nDx#j^68@(Ifl7egQd>sj4n9JDS_VnrwjM+%Q$W8av4T9jJRAA2Rg%Yud(zMpU%eA3NI=YPg5 zhL1I5cycA=OnR3|i*@Fxdrj<%23$2Z+sphgaP6AMO%KFTS97RxXCC>w5TVPLe;%}7 zJT(fY)W)r|ur$A?w`3x{ytrD8W&Ubv*6^aazl?W#K2bsFy0mTNUg!D*85^aP%eF&) zXnHTRKeHz`|9Wr=+tf~D>wV+A&o z<)BHrq$S&WiUsuNz2;J@E!j@va`Cdp$(u$8n%{0e+FU0X8#q>QdOF7~2{9k$zDL%Y z=jyhh_O!>w!}Wfbs^>O%L;d+y!LOd$#>1W(+d`J6({kuUO?Lz?csz`0bbsv^OB- zM{6r?m*5B!RZ_ZOHg-#NtvB2RMlO8uE4PTmTo_%3D_S$<2{LK?>MBC%IMiS9^wDMA z^60>(bkQShR`U%bwDb(qjbtO!4o1$##_;)QqKN8|f#Hj%?vf~qUOy|n+D2TZb4Rz8Wm8L|3vqF4Jn)G>Y29Y! z8}q!ocpnBnte;E@R1WH-^+}fb+<_(SI8r@)BCRjktk0%iv|GX{)BH{4PI60ZMCrUm zbf|!lIql6DXuUP_Q=RG3MmrqjrUz?P-`fzi(Qg5jEfv>~XZu-dkvd1~f)e|0dh~gE z8@Ht-#2~4HzLhy;mECs(IIRc{%|m3oXEMr}p`anJ+UaRJ2O3M)iLNHWaueks#ef|a z`l4wI1#etB4Zv;%dY@W6O__JncT5|~`+AeE{7GwWcD%Z${u<(Jnd6b^H-cr!Lm?O3 zI?6z(&}f*;PjVA1}ZXtB^^W31SOB@7b zS{njNnq8%v=ILNqr=((H=`I?#9aAtMG0S#2=H9X^=BstiwSB9Ni`Dn{$fg<>;%*#@ z!wtQx-8)jV-eKvg2gVsD;gqEFp)^ajxK}wsL)isCEHkb!f(VY?ymD3k*4W#h@VGM@ zZlaA#>um0Fj<}4%H(smS(Gnlg%HN5-S7IO2Kko%zsgnBh{y#Ep>|- zO%EyRq{xl0o@KdJ-}fC7wi%QLZL0-lLWrIX88gsjh9`%Ch(d@9E?HS{Z+u&xEC^4| zE|uvIVCfqq3PV8sAG?d4UtRAtJ!qwg`5?Vj*6jx;8i%wmMwY7v1GTlx#_D=y7zFEP zGgZLG1Uit5w_B5@&hEt$ZN2W>bF$kYNg5LamqaIw0a=4(KXucd|a!`VedeRaUUNX^VQLmb=~qa0ybgO{)$b zj87ssmX_rVi~3X)8!8v54UL2RvA=!DDLL)3;OtpmC0*8YnZRf03-&(VX7BRZSdDAm z;LCEKPqfdjzj>z4p;~3aRn4aZpOWZO+2%=I_50Q_ha8=TSum>cu1ip8`lF`4}8 ztiBAl!nMpeAcy)HMhUM~s$ADm2 zPx*cseLlHe7_-DuNu4BO>8v_Ar&wNir(6u=c- z{I$BCgRO-{Wpe`_LycuU#@n_fCrS;!4uB$m_~Ekv19Gn(CcWEFj)B(rdB#BRi1PWR z(g8flHmd#X$PqVXq35*R>rzxpVi#6fhgX7bb6We zQA2Q4(#J|^CX>gwG6b9Z#|Utdh2ylR0Yk0#-+#t^QB4{q@DZHk!NUC%!t?}pCWP5n z3WL+trJ-=tKHM-~z_@wTsrz;CHX%~RJl zl1Jy`tv|Cu^<38%y-V?|Z@L#g~)uw?c5L>Mq+wc4tVGUgKOh&xOWR2S~m4S@vmI0^Y! z8Bv+!Cv$VUsvykLWe&T$ZaDX~nFG`_``TCoB}(h5iA7pgsK62ymMzsqdMOrH=A(xk zw|g5(KT?GG7UtqPx^C&t+26#A3$(dZQrmQs43-3U9W%qq9?>sunAsjVaZ(6cD5|+S z*ndQRIU-QB3>6td?fOd?QQ^?_GPz8?YWpPqFe|5g{9q3@9@P>7@or_9+Pq%$1|h1FIYY!C0&XnsaAQ3p6(R? zYWb<%m67MvSIgulx)ni|{>TyJ1ky8B>z!KFZT z&(@1bo2!x_jD)5$7KoCsT!z0Q0`9>uocLN4Whmr;v*Bj--RpqY>>g?Glj|c%>4r;l z9{yE}aoYv()jc947QPPR5Jkc#hr*l)D5tTM)57+4AIm0=Km!O2Q8@+0stZu9o{wRR z5J9;(Kn1(%Vs#fIay)HU1?+M#@{eaqw1s%4WcL{Uya z4{apb(oEsF^M)klkn*%4HUsF8ia!n{sM<66|yxo7dFe7=6niMad5DWe$1Q%s%o)`_aWk8&YoSWto zU~%im^owp;%~|=pCI&Vw8Zmvqbq;D2O_8IYh8h&XHZ1Hu5PZ|$+qSE*k=#RKhpQrw&Cf=^b-hQm&~GSTR!6~*ru_kiGDI=#1;rC6Twl_ zG<#*dMlZ@q-La%9JDeh7<_Y0*I{a94LH1inWM4>A?!(3eh85G>uMh(04J)IXRtxq0 zGaRQw<@erSyB6oxlUg!#u>J;wX*f4240dMLt0&=%#FBL<#EY)X z4EUVuB?D#Zuz-c<3ZKQ}B*7QA`-xewHcTJb|7|WsCbiynE%Z`yXfhPs!yc!#_dAe7 zUPcv3)AMxNTxTv?&{??R6fo9_2XEAMq?p4_%E4$h1)!7zFuM!azr11;PvZbvP9-FL zGzl93nPATVZ)&ul%oiGo;IdNkoM0>poGt}_eenp~nw9bUQFSGsG_b=!@_}-Zm7(Yu#Gh=u(wC;qxS0^3#f=a^kKcBXfTOAsE7a^TL836z{gJ zHtneZP#{Cll{eEBEp{#;mjNT+17?aVVkVQ(bmJ&H#$M|}1VPtv@bUzccLA-pzzT%{ zuje5kiHJY5evOygm`QzBhOH0`o@JRPv5A4W!;(EPmLTy>dxC zg+)r9&n=6#d(L&FMyy@Zd%n6NdpUGeU_uZv&v!60I0&&(cD}o;l8&TKY5gc1RRN2J zi^BV%d?&ZB_(?SUE|je9R?b%t_VWvWmoRcNu!}O%3+gnS5hPN$L6@V*6Gbb4$uxbB ztME)OGA41+Vcr=*oH0yrNUSC~kIcSvoah7ifzythDe2R3UDvSm1}=@_xUk*H8tlPU zHx@4lX6V{lkf&!p3wXpfUxx?m7_EZI*I;O#^kN;iPH;P84G%W<7&6sb^}>>IEO z_2K{=^V~Yh3CO9hShP)NGCYg*;Wgp)1>lgHbD5E_z9}e-=MGr399Y+LQpku>n+%ag$LD)4X#gef|Rjt1P?%VF9>iaBP>0%bC{m*}j zHk{LOYRDNJ`WaaSy6UXTnWLQy&V#M75V<94N2PbY6A<7p`LKnG{>4aPU z(X>d5GU_f2mUrfT^bM7p4_z~esgSCVrWCDNN?%ku9f$+0<#;Dwp3R}-s~0>LiU#pf zy<5Kw-=Da_W(LwVrYBWI?iop=?&W}+ zIs)o}(PM-G)9}?x`WWQZP?Ex;zsNX**5M$Iu>VynI6&d+3oP~Hd!KMk&sFBJ8POh^ zi$g$VNOE9SEU5AsY8`tYLh+9i?Q&~_%B(cz8>%15pUIjzT89lH(^K!C+@%)XTbn+E z@T(AF)8y{USLNkzm%K$XNs@(tdVeDIowY6lCXv}_L|tuo=>(%>iBjOOIZnsnXqUot z9HgmrOS5n{G}>m@Ik@$-nykmLOOW_qgMR5BFkS4Q=;}tXK6pnB>DC+>6p8m7&`Xiy z@_Fg5gnk0vzL#d3LOEtpFJ0^D_7zQIB%#;TxywX?g*cnh^(?;JQR_kqcv%ZSiFni2 zVUS+Wx717KwZzcn$j%XhW)DN|SsIEUM-`FHW3>lSY&71&eZtdQ3;>*Ue{SL9E zvi9}{_nT++Mj=BkbS zNqe7E<)_I)=!jfUSWie91=VHv+b$M{Ptz)1%w1{7hL2Jk-N2?*<$uE{_<`$-H-f%D z=o~w?yZctaYn~vTTprzNb9%$d#>~cx$H?IN(;&0ty7Hem zI-k%2;;aGlV^|(7p6GSE#Kw-Z`t^f#J*I|R0C>w7I{~3kqO2oI32ib5(>ZyzYJ#h} z&nrg_@V^Z3v&kZu<0I5CPrTU9#+)RQ?fSV0@`twkOn2&_VajV9QAVUc0*5TxmEkTG z3O9t^0Q!Ng0aNC-uWweu+YY|dzxn#H{am#&5 z4io{@AV9dKx>6Xo${x)C$#VaI2j)juPnPE}NzdhdSFkY@Lvckhxli?Pcl0@ERP0|7 zW)x7-1S%uqxunjyZXwLLk-A!vwzW<-%DTkZqq0Pd??t#d<*5%S=Bf?&g0my_DT-;fdVKlG zwKN1YHtL9hmU38}bXQuMwifW}4gwulpHGdP5L*K*yLofWPP<+x>RrV!J*PD6hr)0n zRleH~`I^3>A|-e`!=BZ3 zfFap;K+a*KSODnY_~|_GB5!ed$$RMgce&r=`GV(?4Un z$xE{td7WsPSUVVP0gwzzU|h$w-gpKF<2i%I3Y-T3>bx^8i7CK>&B7%jq_bw*zl z1}EkU;TI&YA*g1gZK;q zMSHcJNQQsxg=|8$`7FRxYuc~pw>2Coai<0anb1JFCDsd&GOhp!7T#lWqquaASJeNekdByLXsE#0*I! zO^6mlcza<{6T?Tj@m+fX(QX1G5yp$MW3YfyU`13@*Pp;AL*_?*Ar?6H>>!xY=3?;_ zI|jZvtUMhSKzK0Jl_WU%6wD%f)I!Ch5mAsn6tbYt*oU`?52Cq>YG z6(5Xd`1l9m`i3K0xLq}13fu-Ae)zsXDr#R;^v3Jji@Jf|J^)HbjaZ^ZO$`YiyQ}Ci z0Kky_AfPb-&gj7kcHs;ZTD3OPsz9rkPkl~&4EwB!!SLD8-K>Vb2MsZ!A3xxOeWNwss25CuKsii3KU4ht1P;ejgL{ArrqRnc+ zQ{_g5L)83Jf?+G{(7bT$XmrvO6X7h4AfQBH%G3K%xFjp)fxIki3+`gL2;P|Tvx%*rl z^};}}Vwj*~-*?2fJ6JrC2wg~t!=wa8&SJA)iV|jOqHRbPmdt)7!Cz* z0Gzc(Ysc4jmV(S8;7utgmZ00)+jSN;xKA&L5!eaR(qD4Q_SqDg^l~?(#dW0q5}L`CUu1^5K73l=7d{x4;?xw}1X}xnJ$qjbhki zp=PYpfKUgMtkyMYNWNV31qybQ|D`x({k+}J)I5A%1(F2FcZXW(T%fyn8|Blb-KQ=Z z$j55v=v;@Ifn>03BG??aL8?+aj-ukfJqdVN}crv+{&)4Kxup8j_Fj1xAC(7|Vo>ZD*X{yLbH z&jDC|rxm`NoNiEr8elcGwL=5>A0=>hkm{o>Di}T8r&nJ?*aCLK&IRus8KbHSK z{=ubWrd=4SKUn3^aOK)`=n`iuZFB6p<*mC)JIFUiiHd$ZUdVXT5@C0Hov>R+9Uxa2 zdRQ-MS|@d5!SE-^d%6aw&2i;m4TZBDhFVSv^rq)_Rs*wtI#7*Jpau1yfIt)MbCAZR z8(W{ICmQ$aRU%BN1u$Tp!KdH9e}8w<5cFL+&d-gc$(-_UdM;R6hk)L(x2Ka+Hw;gI z|5@s}6^&FJ1P;HBD*))cs~hVnr~1gwzXiaD9;AuIRWwu<*xlKRhd5))FL@|xaP(x2 z^&OLu@(Rlnbi1qKoI};TQu})=;>`Y26I*p2Uejlt&$g_FZ%6Zau zA7FRCk5#*uOT|X_%gCrb;r2uxU^Zdv?@ytg4VgG5Nz{$elU05|g0?wjnX7o^8|)j9 z-mHL@Q}cf7+m7}A(k9C0ra19GzsKHIU|NNZ6@@Z(ov@k%xhe$tFuwg~f^>61$?W>` zwV1b1x*c=Q)wWv-l?(npyd~s*>C5GJ4v%)cK8rFBDHXnAvP&&zI%=dF(W3rU$@u$c zm_%s_m`5Or9}p$Z`h%|e=cu_13-}`H9)Kl-T5A3r-i)^gF8_H2|Jd_iY0rPrCI7L| z?caaSYO-8!`RfujJ49^pN%+O=$569lzfwP#x#4epo%v=TtUV4w0!*Vf)4TuiYC*Wd z|F$+o!t&4Y9|{crU6-Shv40LTKh(1X)tqRepou35Y6fAE9{#u4geI0&T8jYs610%$ zW?eE^eVIA_Hu%kdj{i_S`0s=If7u|0>9?y$rxUA?6_J;ZgnWGHFTZ@{7Pp&qO6l}u zod)&z|N9jERZa@`{y!xk?5OY$88>fQFPrs@ALB$rZG2qFkC_|r~+F!-C*w|6( z+mpX56v=-Jw(vSsctLxoipt%vQGbo6w_*>pKsiFo*~&+M-Lki0Y}RKsxdNL z@>v|G$wGZ6GqyY+3OyYCRf!$K7Qj}7TK~pY{ORT2c;9=V#)HK;6&V1#cN6eAi(LJ{ zr26#eTXGP4DyO8A8I+#_5%LpY;X9%PMWb~S2WpEF1F@`Ru36*;9XR}d~gyuwr1 z8SM;ogz2LHmPW#4FDMPrg?M;kBbG`!$aSpyWjZr!p^!L62lQRymb-VMeh_v_`Z7X8 zya->uz@$9qMJI;`Id^Ve2$e*kBas8h0f4Bd-WxxFyb4fk*puZQ+QMcaBtR=(3!BX4 zI1T9nDH*@G_$7ul8@1fr+|I3OdDUOPs(8XSJ2;uD%h#@5YY@}Q zGOOLwV-Gj^kCZ@M#@LG*2P`ZQT`%SgRGtGZO$u<-LKvXKal%RiNLkGzV9}<;#Kfo} zQW01yuj1m&VRJjg$oSylL!buW?a2k2Nq;VBT_Cy|!2`{gALfjXPAC$7IM)%Q(stuW znBsW*0AOPV@1%_T0G*Pz zMf#d9m(X>;#(n>oX6$WwbWcF#byW3X7Wmsxloe8W9k`Z5U+XUuE{{tjQD6tCV~teU zr97OTCp{}WTlMDS)7+lI-xk9Vhh=6hsK5Ff2L7)FdVl@|wy3U)&t<(zU%vd5_}7J0 z!C|{S4%_PO2&}KGqj#!#DqqBm`P_x4(wLzyM zpsBj`yz?)c^No8qtMHEDj^n_fm?of9H#?xB-}~c7g9|t89YB|mcb^wj66|~$#*aDf zwK%VcgFUu>A3P0^^ki-_2{VA$wEzQOTwa1hh$neDeq;arO{j)8bA;i|h0<|}X!5=t zz=#vyyHn_TA|xC|T2H&V5gOkoiULXV7?Zb3#ED8Mg#U9Or~MXVEYSAE_kQ_uHbpsz zH`lzrEJy0bk58G;lJ~cNjTEmJHc->i*-rNtiovug{RA)vLqB>|ZXQXu8X*Yj5*!^8#lpu)Bk;iJ6}T^ydf>c&+gy zsA*0%FZ5bk#2H?IJzouhL&&=g9zYZ97nOa75)c@2jkM;b7zSJdomc8P$6f)H;j6HJ zMgtSG5eX`+%PV98Y*C_62~Te<4bApITz>$px{Cm)C?L3_>V{=_mjkiCh$qbvX;;{x zk*T?sSBnv>x)cy5o|lu8i{i>CE^gn7f$Qqv@iKppUm`>M{ZqFmn)Ga+(X+wk5u{%Z z32vm*0Gh*Jz~+wIpta*!UPX0vMm-_)@THdkK}3*sbIcwq`m@LB*%Y1v8A5#_O{tfR zldXY!8n<1|J}fvGmN}@AO&fk&5vRjN9X2#^rSrgTDB?Q)gMEmv;jb75`)Use0H276 zHfS|=ScIG65eGEKI3i>_joH%xtLd}>deNHsD+Wn{IAnV>bB6OhDGD9074bPto zL{$EC=H9F5rvY1*_&1ri0RSm_`8IS3UeAO~1HkT?3<^HFT-J@4&43<# z+xG1iurCF;edk9yOVAHMjn@aCVA}&6U&xt+UY{C~Sxpir+E4k{!E+wT3L8)!0|Pxc zfCnWdi^|#<*Iinbt(HiEb0wdfc@ZztYIOn}z(4}LVIiev1YBA4_w0V31OC2PE=vFB zjZNNaJeUnHp1$jqC&d5rHpU_lN=Fzp(H7so4($7n|6j+Ge|i3J$Ny8o3b+n`x6b{K z{QOtD9AqV2fjci2T3R&*Zl0uB7~O}u<6Hvoof?x$W7OeoK$^8^Io~<}z*CCyiimCc z4Uk^BJl$u+KN1xu0fa1stqqU2XggUiScL(;s2n6ru4)VEOh^bn5qIv;@*FTR%dFrM zZ?00-%QP+(KR%6l_4rbbV;pcN&8~C;0S3YJ3lTUx3E0OGU4&;|ek?e>ozm6jbnzFw zs?GLI6XI{Xbr{((6T-vDe3 zkVBPVF0pIBK+iu2^P4@eOaO+ub3bD$w6m`;RE~ADEeGuuqo5 z92uoq%k}-^E4N2x{`HLP-pgPIy}%nN@!$Nbi;#Nn|1K)?du)~;2=nK8S6=@0ge!0S z8LX9;|JkFKxE4Olp=KK(c*oGgCj57g-OLK%`mYN>AL#$#JNREkrFbTRDlPjr(AXe^ zkUcmQV(>dfla0#!e~YBI+lT+Rs^edQ{XK7grbY7)j4Pln$Ph1QC@=MX^cRKzz(D2= zsszA@HJ^Ur5j^yOg+nD>Hkyj%EYz={Jo*>P#d;Gm1?Tl-+n}p;Q{TtFc+}{N4sZ2e zeQ3+CpfvfA1pT{hL>49h-G99Wf8GQxx6`g=VwmTO+5Io73qLvd7@V;G-Y1TQQ+$p$ z&3?G)dqolS=e7VqMvr(>{v34eLbQ#LsD-BIOf2O`D8Y1+6DDERzLnj$gxz*K{*S&P(z7%En#<*%u-=JR6+BP$<7ixdhN63yvBDA}_7J8#lrr7k&-sKhtuT;O~1D-T}A>vKChWzxw?q#LRoasRitW$7-d196n(xv2TeLUMYiy zm)J89i=w>{?uh&?L>mAjL3uMseQ901nE2`yFBLd|0pQ4Lv0^Lk`UR*V|8!us`@PR0 z85u)hP1Vr~o^A^RhbjnI=ufzM8U}UIz+KFBqgtRaVk@tby7~pk^`d-Fb93{0cAzw< z0ea#bn+^%%^;@rjH!*9A`Q}ol!*+%Foq1-7Jth> zCORi>A?H7bZc|v!LD#Co5~QJchqRJESA0e3;FT-0<CqK`eL z6!bZAZmJm?McHwe#ylZmVNFaWpfhv0T(C))UF@8S@1FG-Qaw|fCJTUuxdZEZrLgb2 zDuOZ5;UQ5+wa-8$?Pmzn6BE@U~B>3-0EScYV|BNLg0Z-}U*G z@l|R6key~fMqIv34cJO@uuGVS*2dn`iCEGGL5^0(zsfooWZ{WXT)o+X2{L`fh4!cJ zwjHaJ2_ukUtCHF z$w_xZY9cDfezgh1F;9_m&YH9I&b^2T?(d-;?d|i2T>c1K5xA6XyP|!DT<3t6HfArn z(N*Deb856e4}a>kl*~*}KXxo~z4@Czo(^aN{U8P6A3ogh^psktuH>k_x%R z1T!46X!$8iylk^&JjWdgN+r(JY9@G&G81{X<#^G#gTd^tlq~F{#3f)uBpC24uUYA*b@SL&e)G*|Yj&^%KV9pFJbL zBDJ)QvH)}Wr-uQyD7Ud4?mbE2z^%PYE(1+^={hTE_*C^{Vsro{da-4#yc^}ZNdnlI zW^~r58_U?#Qs(Dy##H*hEx*u)DhZU}({BG7* zcO2I^x+S6gc8JPl4r7O<9gYCjWZlA1SbSMG{T!_|zhG1aQx+E_7#tS#dDMN6hJc5& zS(0P9x)!g1K=BSMq2fuB7ixXIcRqiBEDzv_uqf92PuD(GIwPC+ZZ zYE)I0`%C!kM6+bf*OQnq^5eZ?0GHOI^~cep(KbPM?^47%Dc`0XF2ZxM$cDoPg%pUP zxQ7SDYW}WF-vu?l{399n9o7j=Jfv1T#yp}&6-blqtGYlyOTqVzfi&ARy}i01WSFg2 zgEhOiC8s4>7L7A_G`^y!=0Wfm1wS5$n_iJ7uwTo-Ah?H+`z*qGy-8&YxLplS4NGwP zy2PxZiyo?Mu*fl!RRyL3J6Jw>_RN{{SRoHYjjp$sMNU>3ho)Lr9L{+kt^B zpLUQNBk#Dj76_uzspW(8+!SqsEj!cuF1@9o4*D>wnn=|c>3PxNXt3-bc77KRgnox5 z#+7{8*I9-raXD@JBE4p_%10Cq@`=?9)!mm#%HX3voo`!HhC}TI6;yLBNJ9Cr9xI!s zFjJ;Tpd$tg=6?{o_ti`#;n6~4mW7%qYb_eaQ)|At4-a zV7S%NPUhAkxx7^>#tUn0R$tt*@!o!aE-F0;rK8~{P$;?vo(Ui>{PBbIb0y(&4aNq4 z#cC$%MYo|#DzAYru0k6<1x?A;rEM06Skb+U%jw(gTmHuIH?s-{KB=qC(^)X|&p6_v zS6a)CoVNSgv(3c-9I|u1(|_}JeQn?Jl(x|MQJdj>SuMj{`qKEmNMF9PG2w!y5Z33Mv$IXO<35L{4oFmfjz4^H*mw!nY@kOI^zfARG_dN4RR=+S_3 z*uj2`(T<1(DZUB`vjp7L|9a!Fh)u#==Rg64eb1tN$0Ljj!-Ex8Qc*znJSQhmU`Se!xo%J7JWJrL3_Yn8;B>~FBva(HU6WII3fXq3$ajAik(dzrAd@RV$)*E-TzcQ)X zj5+Q2yrZ6Xlb~QJMb>5fAx%f||=!HwSQ zB#q@vjBnS!XrSut-2hm1}vx=e98*NGIuyDy+ z+Y`7=XWTOyICVH=A@*6uA$i2K=J}$NNAgfT9(_NSb?Y`BTS!Srzr5%FOAr<-R_hFM0qE&NJq z?~~7}&Za1Vlp(R~q%{|Wb4VnU@M zp%Hl+Qk`-(wE^P(%jP4sEVZ=6Qz=DjkGvMcQ*htKPT(f z7NF@<*i#cP7GDcSFR>xRfz#x?`ox#N{yDk^TA7Ykxpwjs-}mMuS|CA6cI}Bz9n;N0 zSbAm%UcVvPwLgnKrrVPBYR`+t!cN~D%MTq9?&hcZz(!|QQ zt7|MCuCT(jHn$I)doZr+q$J?gkJ{9m>+)RgrhlmR&Ods-iy=bZJJI>WJ%_R2LOTBHnu}*+=KJxFl+VueK{3sHRA~x+R zRWHX6kAPUfe|K;vbe#Y3ZJP3xi-?G@SbPdO8R2lje-)vt5$Od|qmKy=ZhC!jU)8%a z#>OGhe+GjxZ!z#_GOs->(M0-I#i8Njh1vw&TpKNIJopo$0|B=Wu3Fl_@-&P`^x4`L z66u!0rE7tM0>qH@&A}aI`i{XaBJd|E4SSRHp0g!Eq6HK<`}{$fJNNImS7hZKzj*O^ zp`*2p&0gv7CBgd_*%7wk*XQH=G>1P~5%oh?@CVo=*^b^HA|SrhJ!e@o#<-K%(xQ7g z$`mIX6)s=Kwmq*dVqAl>`3g^Jqc!#fe&CDHKz|OJ(vRMYci?{@dGSwD!5ahfe%IFR z)H2QK#;~f)Ws`JjgC<^vNPG`YW|+TNJ@>DMW#eFPTzv8yq|(1$Uw;oR6X78IrFHj; z>)spkBdFw{81>aFTF>IhXYQc zch#xbC8YC8Y<6$&MgJCUV_!_l=(25K=z(KI50Rj6tSJ~2B>FaXB`MVAEWwtilJDz_ z-mHY=VP`kG<%+rK^EypTWLj~T!(m?KDo2jASDx$Bihr#Y5Od#FiI6dTAYqY!)k$i( zUuenBvbsBRS51D=8->~Jo^wRuQQlTJ_Y4qG7dZD<8 zC~`vomPbdtZru1Gb+#Pdjr-FuPMi6VrV=Fb$2ZFE$$tX2yO355EWC2yN6^?=+%;Fz z&dgxQ4GP!t+)+7w8IvdeRLUXfBJhg!{QITzl`!{f?HXZSTx^+dpXr|a$3gIqnh1bJ z6LuLnC|oxcL}bL(Bpmv6S%+vNEb4=F2TA`fH$T!F2~D9vXol=r(busBnGAlZSMCDI zQLTvsG7OT)2=Mb4nUbiw0&7s#tr?H6mvQN0-HW#!j(Y`>jSx#Hd(J?xx zX#~v&)agA|;ZV$E_6duLRq@!aJ;bBrH|SgfXzbzZa(G5uSk8^_Is1xkJq@bE3=m5e z?S|DP+##zE_*^uy1Yjsc!(>$8Q(>0c6s?!#%*M$@`fELV@#4sxdw1@%CM7~<5wDkZ z3MSWiv@I~jv{r~`$E|>XD5O-TFc{I_-zJ+>5lz!2v~A!2;xtl1E^(CmJ4RLz0<$Or zR(#35d`L}AB3{C{j6-g?&dB-eHI`%-x;YOM=|t@|Ic8FHZ)^1WE;N`TctUIb%^}3{ z%6Cj6nz__wLNxW$rys#koeNDQGZh*m5(G`!=N*73&p6^zG|UG=VHJdjqF4Bu%WjJe zS`sHHOQcw*vYb-@-R-bwmK8je{GR9eZ7d+PyEbWjW5uN&=}1|+n$&BAQlgsp$+-)W zc-S`C)eOU%e{QBn+BGDpY5E0H1!=&`)?Wh=G(~52=8j z`%d@;vm+hl;wr6EGVDGeg9)Uw_PX0E_bU|&J z4z1)VYxAawpK{mh+X!}2^GHw4ps~+&e>$XXd-~s3Sj&xcu8l}k3!Zo}e=u(Ju-1lS za5>6**iS$qm>6hc;$dlty}J928F#=nlaFE~n1brY?v2y`cREUR-mXonn07z5y^T7b zSw$I)w6ePic3hSxv@h2M@HFUiQ$=w}0<%jwsYwBD=ORPaFg>r^y;DgDd zqi*MnFZ}d4yDMulTMrLc6)}3(k3%puU4vrb21*;Kz#RlN;P|WG2CS$hA*NKXU6`?wVey z#Q8dOZ0#Tu>O`QAbWM^GA1a#%y_sn%!TKi6g4-@__yYKcq6FQr z^swv5te;Z;9R@Sk$epbwM-w1l{uzubovms}O2k@2s@1-|cY^f4@uxV{P%R*n7L) z2k=Wo-(Projx37GTt3`7VT7T-F&%wOWX3po!1Psqq4J}-}2$yI`2e{?sJv1 ze0@510@(QO->|S=WctEnSE>&Chd&R>q;kv)j5ZgX&lkEoU(#Fc!M(k9`kSPyYurD- ze4R2m`aCLNGISu+m=T^OA^g|W4_T&A>EEcT$I`zk>Gyx@_y69;&s0-pta% zk?mjdypQ#Ejpxi$*xu*j>95g8<7JV^7o_H_N=Xqn37ltRD8o8)1&@=N*1e9d-C`>i zdW4?Mb{IK8{+uUJ=oh2AxMaGJ4*6m3Qa$&{h}N5oQU5BTt7|XO6c|>SQ)3EIy7Nm8 z{n~kCFn>YLWC!gStu48D!#?44`hHhirUsIWJx+pHo%xg)bbi#2(VcC~R6a}j7ubWd zlMC1LTF(E@@nY@o+);%RU{qKobXm-+x-4Y1?w=jFF~IOLexWe!~#z*9Imx7Atwv2y!6dy$`i&BXS1 zgnMh&->Joa>(@uZ?0?0S#*a1qIQ`3AU-MD7Z|_KTvr&KUWNcG^b{GHi-*Sil-utRH zAr<+^$8x7MJ+zIMP_6kycYk|kn_x$j?(8SOcI7f{n1`8Ur95@jByiRlFRH=xG5FW^ zzbW<&bAC_A|3hCtl&1bteU$A_%$ZWNn($OOl;Kkn8$TH(Ne{KD6q+gAyxRv>0h&->AzxHe=Wyz^b7|F%w6)R#*zZR<`mPJ8CAlr zqGk>4yY|H4{F$w;mf#a1z*Vay!3o@kL z&gKip5UJa(D>mg)d^MS`q^R3HmL`z`K1@$Ke<-)C>OXa9U;WuzcO&DAX0|6C09a??)iQ#_ zF~^B6^O*7B@<>PyryqC3mMC53m=75z^jF~ly?GB%A+hMPWzEa!K_qwb>+O<1 z|9toV2fx;cnJ(vzGvnIkrCyr#Sq;tc+9fyR zHTd)2zdsPwR96>de~=u>mYWkrj7obi`0xrOOD5b>){0Dj%p-CGTU%R=dX(%onJz*f zI;;8CZv%v2WM*D3F}Ji7(R2KIO)KTX@#N&>C%R=P<5E*4Hg2p*&^65`73n3HXp!=L zOhK;-3JO>`IpZe|vuqm~DOBL%F44KB^>D6Lzd&XldIW+R6#9)ZjyO13JIWnQDZPrpm0Pr*} zPA1CgP}DIWA0I_Um5g~sAACmdMofIWXDXw6-P4n2V`OynIX&_4WnZrzi(R`)A!n!Y zGvjcl_IqynLV*I8!vh2~_uHko`;w4aA~VpExyt2{R#G_U&l`tuhq+=z~jM$eiXV zib8*9=hMOdo*v~vMv-Rro%5+FDWyGAehf1Q2l@opt-Cv9Wo5hSYinxuo0*vneoMQUwRmC{pFQ`;dfo)0LYUB^gWT^%H%;-e!$pNv7#VP$6+2!HNae*~H#t$W37 zTG+L9Z%FV@P!0PuH8rD}T3Qq+S*-oV`cu}CmkK!@LhrK|l6NW};g5ms?CA-4spjtP ze)025Uh6a+JZ!0yf$ipQDmA{u?%BBhHs;4jih=gAG&MCTJ15|6-a1}G!&^1@F#cF! z?D4^`{=>t=S{e3cii)y7-t$znwCIl}Xl>gs6nGO2vaBkZSumv_@fYjjb#!rYX&XuF z2v>H^!Djx>?(Q4txS@7ZKTXH4CoxC<=)1b0Ks{%Z-sUWsRXa|mn(HoIx>OPEq$3$! z%{$v6?67(}k41&eIT+Oc;Wl=;(QkJHMA_NdTOpv2AB1M&_;cNiomM$qpyKMlYCP|7 z1kxi=7#|-WGwJ+oV03H$=VcO&D=~5*gE~WWdLbY@ZftbQGl@6dOzD3&^5qcjTOi@h z#tMtfva+(gqCZxBFqPfD{Z3F16j`m>En_ES0SdkL#x7L#?_)iTwT12@kCThvht}8Y z-yn{Gx_!gCDSc@SlyWlxOr*-%tZ6LnG!}1`Dx+CfX~O9@^9^OEMkqpJ>HigOq+eFqb$t(<2!7kFgkyJi~Nt< z{u*UjpadkXza22lj7v!GPSz_#^Kv0FaUMPT1A3dwZ$LG*8Y_&SocPA45ImG>XJ;2+ zru<{nmDj@(k}%b@u!BN@aV0}<7jTD)&&7_tJ8y)so2S+!iy2GRs=U6Tfv?mEdz|jh zmYzoy(rKPlM!mfYN zKIhT6E)7;V>4uCZ+!JMfNvIs2h;k)7_@_^wLhrAW04_F_=j3_)`ng`l$Gt0f>p_9* ze}7Qm^6j<~f8nxa%e;>tC;$@U*WBEUCI8gb9y*??Qw@7EFt)3ymoH!5dXRe0yw$cCuoWf zztIZ0BSe{ws;jTzp7^prfC{csF;?TyU@+@9R_^V=PotuESMiUI)#kVzsy)|wpWCLQ zxjB1FoR*GFskrwr_O8krF=&7I;PI=n=Y7b_Emeto^r^afA0TEhf_b>sol|T^XU=R& zGJFpi7N6PP9eNmqM%${x0rvRO+qZ9%V~7`WFF~v#dHBBXhr^j%~%hS5wjH>C&r? z1#jh;j?sjEzT)ogZdO*-1Fk!V|8g7s(N44w~qU+xZHS&-gNVrHe^<|c6Pk->s=Dy7dK+ET3GboS+Pr{ zaX9+*YkSr5OEAy%(*--!rx6T;52hw2-XL!_&z@&AHa>0y3C8W>HbZPHs%U6fA9buN zxq5rk1!fg9*QYSv7L{3S?CduTleA1)94WpxPc&dzK8}kMfX&%_skXM31tG;zeSL2D zI#EeU$^HQ2_HH<(n>#!1(!SkQi=Gb3zfbo>V>+`3eghEpPN3%7sW->>JX%SP*4AF1 zm6a6}ZNW8&=H{vypPs_1w70j{f$x$X?|qeoZGL(5d_z&p3LG;xR(_IQbPGjUamukBG(bR8|$_j8q3Fk&2w)mXIq zxlY>lXK`v%jC5{k+w*#p5zsEp3m$M4myl3`$=8YTN@Y%e<`iM zzuCEa&g~=G?$cLHoEj@S&dUu`L3)ZD?YMz2XjHNSx6|+X=M+^TO7Z#cg-9{h@%fBD@RrO z81+GwQqeAHWfy?DN_kiY9kynbb@H%(FVB&jPg_`0;qRPIxdtvq1Kq6k=fbuCld8I5h3CBSGOeU=g3z~{RqsFrMMP&%EGj* zxqKTX0Jc=P!=scLMYVxX4y|J5ffUlH?Tyy90{A$7D#9qXt71#25Yz>9Jj>2@Q`Mex ziJs9C&*9&4V#~%>GYc2($jQ#f{;D#n2$&%C5*`VSmg%VD-)01cK(uEPEBXe!KMFDI zv$eM``2PL-mD(jtmM@zf`_jbR7-2ah#mUNZJ*uWbB@f=Xq$CnN@l$=*OO%cLHH8b` z-NQ`GW*%II0Cj3=sVz+0>sTV)5MvQy6fIr|6 zEv@{L5bd+BLsP){objf9)nxa6x|>B&{r>&?Tf5^D<*Bvq(iBavEzCEQAOkq*j!1Tm zcIwo?>h}~))3kvy8J3hO(}c9m6^NI_=JQ}knDY&jD!I7Ndk0N~%8z`xsxBXcs04iP zSe{mlq3auB-0EKxyZQyK3v}+3Afo<@dzR8(s^Fwx`?m@QdSB?$_4V{!~-5`N|3e zpVOh9B-Sy!xSk0eaVNOvNThsN1Ofxkkp>wiv5wRG)!M>!5P#imk0|dz*iwiKMNc*r z!gC#r5Z|#Q@3^LJ%G06RPoFj=*uaUl6$dKeGKd-Rt}R=g{c|i3z*r5GN~p2hOxT>- zvUX!&@jeR+3l2%k4a+$sSB8a!S<&)Yl;Ba|a}F=w66QnCre}E%L-?bWwt{l?03w^a z;c@WJn9Nq7LRk^p4PNvuO~K7Ri=*yb$`pE%T~k+*>fpZfJ9VT>&HcW;H~}1XoYeF0u_4=`H*6pp_If+f=R23f8>f!I8DGFTpl-y2A7UUVL%Ncp2%s2D%8e@J8R;j5;X)<{vPj+9k zxcQYuFLZd^lTm#6JmY(GaxqrYUxzl3$9BSF!wMV9-0n(FO)bpL%}w6@r&omM&Tz+8 zZ|t+TFr4QfixEe779{@kh^8isaP~;+b8*N*B=lTq`3D<{eBkALhzdKYy3mnK8TdMQ zigA;)8k?gyK*^6Ic&PqxTVDp2R{tA{pGNx;U0qJ>VDU#xDivQuOgH(_cC8&do56U; za3a~v$3iw zPUK@xh4nE3mV1YySYiC3&E2n<2WFg`h^pX@emv2y^Y`(|BNGk>vlv2#M0=f@Wu`|7 zC~lK<9z5^L7;j1Ru|l+_y4Zi|iWQoEJ>A`#FDY!_zB=kyY-Kn#f2z)@utZ}(t7Wg} z@_mZH=Jn-!Mle$Y)@^~6rFip&OUCwOd<1MEJM>B23jDq#(qax7`)xuPZ+KxpnBnvF zAImfVVNePcrmV8<}Ahzu>NPKXoBBln7X2JwVND(xHv#%eUO7wh8f8^QmDb=d_nw4&k z3#G|4K_%m2*#<_Hwy+mT?Em!0{hTYec}c;Z4L5UQh45Z-*J55|E4oaM0o$C*l)E~m ztHnIXX5pEUYb+E6w=Mx1cg|oya&h}o`f#d#D1g*g&UO6 zC(mTzN=dt&;{;fgDK~N&xL`V_4!`{Bl^8L3qt7vYNa$d1f~jvspMnrP~%SLi+G7u1Gc9{KErWadgp!d*KB>zoK zt=$Q-VB6CcMw5Tx>#_#Q#6Y=n##$yx>V!67qYWxxh4|_nnw^Yw?5d9sEcWQd`k~@3 zUMObLhwirhEc+gMSRZ|H?I_6?*^eOmV4v+7^o@%`?8DSN64vg z1-48I)z>5tl!#64vETHr$W`BdMv9}l*A$l>CjPwN<#YdA`PlzET(3uzg`?veYaJwG zNKZ@xZBGSE$8WL8>({TZ5?b{8E+p*$xd%{?*>qB=hclj3bwlj#?dRur$OZnLdWY%a z&z9yO2|r>Cdi<%-b}Dnw;B0jfHuh2bS%Utbgb zXZu9$Ia#{L!j>Z$ER&-*K24BMsjUZ^h~%5uHUZ?&swB(umd~=>%=ZI!&AdX7D)2X3 zz&#lOB_nVZ9)@6E*k+2*z{t|lQk8LYw5PvYH?n2{lKln#g{v_sCS?514AqeqVp4xBdm9abIFt~ILt6j z>j7{|NA(;L3Z#tnB>Dp$C^DWiXHEsAq$c`S`RkTAd^LXKm!>0x{Uvvv@@@ouwO?Of zALN$Q0vqNcq^MwEu)Ve^nVt%E09j%ru86@j4(mWZY>2DeZI_A$--HvaM$R3qY48}g zg3VKMv1Qop!ds7xXdgoh04^Q$sjjXTZyQPjZKl@XS_82>ML~0$-0NR;TbVy;eR+r* zP*gNEHO=gh>-WZP0bXD}nxnEC`GpyUo*N7B#`7eNKypw}{L0JAixh>!X!ExW4G!iZ zEo4E_y`b0G*#EgtO|ATis4(h+)d4ncb=7`-(HjI_KF+7sed;1If7%Z>5HuB)l`o&t zDS%JaoBg5BmA9f6^pSipvf6Q|T9Yrq0YHO}3pK^+-7<+hWNDNheTMVKCoga^FR!ir zg&#j2!Fc5vV-#1tbb|g?phIBoV!*qtgfaLr8gjn%l?*|W32@UQ2UH}9)F&$pVPy_x z-F(ngew&qoW2|KQaQ(8!rio1=Gi@B0EVpkotkgF(?ddd4%-R{-KQ%eNX3{#X@tzTO zdWlt709#Za#)Tde0wh~K%mIvYGBDx_;Pa+MG(yI-*EQeluO-n>?+Wz9ocUPI1 z);u_Hz$J{NaaIkI(1SiM-`?L=Pv>6K6WZstEqTwQTax``H+Zi@Yo7N( zIvqUD-Lb=&#_FZykKR3URj!3}Q!}&pPcM-7RuDb%AgXZ3>5O}qcVlyvY|lhtt<3Rd zH^n0WeEVe&*PA{DHW>8dq@f9Gv0L48#P;N)Wr@_8hCOln?1sLTF4}pq?pECxVb;=o z^RKRu4?5rC&~iyVJPrdWKl(v!v%HM+VCNGzEQEl7O&d49#{TBE8>|1OlaPS7i!%WI z@Rnh|*;U4QZ~HU2uMn@@2tzi_q+r2S=~aP|(IHG9fg{yECq_qa4tE>t$}@JSJGcj) zU$K1o<`YYU&9eb<5SR&2PeiTj`1tr^fPWa*i_Mu*&*I`910Z2vzn*7fYkP==>R^PK za*((7+eZ5avzQ&^xx+uXB5lVaB1rOs2M>x|wbFmNRF8QMwYm<6*(6zQ!18XY2OAgu zsRP=5e;jyX3XDuNXFv9298O-rDZL}MJkQ3pTRq}|eJ1ePyb~M)i!LJ(EP!gApPD>p z!>U&;{uzDg0u@}`A0>6Or-l-djQ8{y_cqkku?pu*SZCr55bWI~ zSno4W(<*p_25)siabZgeIc5<9DWF>SD=?++I|c%Bf>oA!aVWUC9*Y3~2#^XcOU%y+ z1l^&|FHwPd52`X3ELdQKk{JQ^nCs7H-QvSG=`%vK%o2Nr%?V7R)stH1r}?%*K+RUz zj8Q({&=1O-z1MWqrML z*C?k#1kfZC)d9Ag2g)!GTgyG84*t^WX&q6|`V?M-39Ky@-gRwM1AqkZ48Eyz>f4wF z3i~hhyBJgT)z!sf0{e|%%K7H);&SOta;mnIE*WwIf~4t3@cOD#>WX>e&2;4T+ZJvQ zbdhS#Gwy(;3aaW)z{^q0(BLnvV}M$WppAR)4(WRRXYiYKB{2$A>}feQkXOXF#+XaAf2>s5D!`UKTWR z36=o}dg)jy=>EL^b4xZJ))71#iK(l2>s-3v>CM;oo^xSVvzLcBCCsYvu9J`?CgSu; zW@3sBDpu~qnetHkp97pvMOBrUk=B{bujVaMJuVce{Jit%mbIo@r?xHfxRr+qw85&y|9C#c5qNg=~bbV!1N{U^Lpj6o=ljlp1 z{siO*DLQ}t{LS%8w-awh)DuvI1kr1D+YC{-86)=!gWf!w;m6b(7oUTWKjy4ZXx8tg ztRaheU*(3Pve^)5#3iq1X9swZ#dLjzaO$3{_R!kHmxEb5SxlY{BYc-V2uhq?!@$3&sf7FH<|O!sSx7B1pj zpHK|}-V<#^)zlPvAH-I3-PUP(j94Lww`5vNkxhNw5Ir#&(mWWgIDU;q?|_~BKPNTW zu+s6V9}1H=4mF)vqY3^Et5y*X+sEhTe9U~ zegW{Ic!Br2ZLn*Gj*gD?Evh7+2h=HdIt3tjSgn*Q;js*(e7aBpg)d3^Ltf7g zdD|pxAUe5M#>ggIIp;3zgPPk*YuVSlCB%)5K8O00_=^cMT*gqI-t1!0jYaZXKF{39 zk~5Oemcy;hdAekp@bP#5+EQC9K`EwUy>6Plbk(YIknB&~GTcktGy+mC|w?rltaRVl=L(+b)EmCp_UR|6>C4W254C1T~9PTUQO8*TRQx}dv- z@M_18uS3XAqSbQtxlRbBjF&Ex`<^=|fI^s)VQPp~CZukJSup&`#uH*{tfa$D%@%x! zre9%S*U0xWVjS7r6|CgN%-qFlM=kCDekdL2tZW?|)JB^yTo7O-Rl=R$eLWcThVz5Q zBR`xS_mKoU1`hAQT&3v6votg{`ScR#h!vsDisqn)yYwA&g|kjA9cL#V9gwsD6;1?e zHtbV>Sih@rq#Ui$2%HabQ3DA^7ZYNv6`1Q$+=0IkJh1%t%PUG)2fKQERgx3ayPqk_ z5P_dNqH~|!UL*(wZKJ5z=Fa!P(Qgz2J>^d}xESwWGOv6up&AqKY~}T&V3BL{M7uFD z`6MA<0z6LM=**Imtia}S-c_vRH#(`%p!_svi#&YvPoEJgxUywUe_@>~U$Z~Z?C~e< z(%djf-X1D=@NN3pvJfs2CW|CLP*7sS-a2R&Dr##ryBa=zTnRRn@Zz|{*13KI4#?*=oa=K~^_VI*O2C?=X7tcjqu7GL}8N&IfS1t>)yS3=VnX77VI zQt|y`{3W5t5GPg*2baMzQ{dl;Qm@FEkUPoNg&&&zlT6cCVZd~->(wTG1tqabgKjio zrvvFNxqN&CJ= zHbQ45U%r}l0Y>n7v*_!|4|(#?ohDs)?}!kPC>>CYcQ05V>t=DpVle-a^LK4@%dXES z?umrM>P#T_1n}&#lu1I=yz^Y4NJCCe({vxwB&;r|{~O9eX=n7_-oW}rkm36;T59x3wQ%&a7?3_S zHopI$rudB#ai;}>GKkv@LxO`Zuqk-bqQ^?>-gqwOyg@1SSF*9OA=Oy!USM<2ig{+& zdT0Yhb5F3NLPF+`qP)pN#dzJW9WpK6_-!Gq|Hh52L_0CeUSd}Qdxacu6>Z>1K5=6w z`)fyBM9&H31e^J!$@;%4HSRqLQvudIQiDIN(pA#NH-7Ffeu3$hsMxh#9=$W7b21%w zUH`rRg6V&*mlpW|dxjt^?7qU6!QbZY{R<2*n6P{%`H)_>D0#{qM@uf7wE83HZ-RreF7e zMsRu<{vVzY->pRQ*pQ11k^YDOfBT z_`pfuH#$^XmGG_3^|zWj1yrIpQD@mppN7A$j#I+c7R_h#XD zR>eyFK1PYz?2K+*&4L^^dd8I0++$f6PfmC;C@WxK%NY&&1+Ozw3!&~)t4bi-UC777 zYPs9EOaRmLV7QK{Gh8%;wV96-$4il6&UV?-S2s2uFs#JTRZ4ark!aPQxG51W_p1eh z$E(V?LU-1d(E*0=Fyq4-S_gwFjN84En#nut+BHDCKTv+tD~Mw@+xY1YhP~PZwPhKf zyv1aSY3sy5*-ZhV@j#REWixlev|(U=&ZIu&{r4qzw{tj{rOC;E?^z$|{P|^(vHJC0 z1#59L*@F?oCsuFzc>vQBihfp4(VNk|++Gd|=Hb>#foyv>7)^cUc4LR^{2lA)xMn2Y zCROU!ScQsXhf(x0Hy=7 bool: diff --git a/src/primaite/simulator/file_system/file_system_item_abc.py b/src/primaite/simulator/file_system/file_system_item_abc.py index 48b95d20..db51924c 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -43,6 +43,9 @@ def convert_size(size_bytes: int) -> str: class FileSystemItemHealthStatus(Enum): """Status of the FileSystemItem.""" + NONE = 0 + """File system item health status is not known.""" + GOOD = 1 """File/Folder is OK.""" @@ -72,7 +75,7 @@ class FileSystemItemABC(SimComponent): health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.GOOD "Actual status of the current FileSystemItem" - visible_health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.GOOD + visible_health_status: FileSystemItemHealthStatus = FileSystemItemHealthStatus.NONE "Visible status of the current FileSystemItem" previous_hash: Optional[str] = None diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 78dba4e6..3dd5d1ce 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -46,7 +46,7 @@ class Folder(FileSystemItemABC): :param sys_log: The SysLog instance to us to create system logs. """ super().__init__(**kwargs) - + self._scanned_this_step: bool = False self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})") def _init_request_manager(self) -> RequestManager: @@ -83,6 +83,7 @@ class Folder(FileSystemItemABC): state = super().describe_state() state["files"] = {file.name: file.describe_state() for uuid, file in self.files.items()} state["deleted_files"] = {file.name: file.describe_state() for uuid, file in self.deleted_files.items()} + state["scanned_this_step"] = self._scanned_this_step return state def show(self, markdown: bool = False): @@ -135,7 +136,7 @@ class Folder(FileSystemItemABC): def pre_timestep(self, timestep: int) -> None: """Apply pre-timestep logic.""" super().pre_timestep(timestep) - + self._scanned_this_step = False for file in self.files.values(): file.pre_timestep(timestep) @@ -148,9 +149,17 @@ class Folder(FileSystemItemABC): for file_id in self.files: file = self.get_file_by_id(file_uuid=file_id) file.scan() - if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: - self.health_status = FileSystemItemHealthStatus.CORRUPT + # set folder health to worst file's health by generating a list of file healths. If no files, use 0 + self.health_status = FileSystemItemHealthStatus( + max( + [f.health_status.value for f in self.files.values()] + or [ + 0, + ] + ) + ) self.visible_health_status = self.health_status + self._scanned_this_step = True def _reveal_to_red_timestep(self) -> None: """Apply reveal to red timestep.""" diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 16cefdd6..82875b97 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -118,6 +118,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): session_id: Optional[str] = None, is_reattempt: Optional[bool] = False, ) -> bool: + self._active = True """ Connects the client to a given FTP server. @@ -174,6 +175,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. :type: is_reattempt: Optional[bool] """ + self._active = True # send a disconnect request payload to FTP server payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) software_manager: SoftwareManager = self.software_manager @@ -219,6 +221,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): :param: session_id: The id of the session :type: session_id: Optional[str] """ + self._active = True # check if the file to transfer exists on the client file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name) if not file_to_transfer: @@ -276,6 +279,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port["FTP"]. :type: dest_port: Optional[int] """ + self._active = True # check if FTP is currently connected to IP self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port) @@ -327,6 +331,7 @@ class FTPClient(FTPServiceABC, identifier="FTPClient"): This helps prevent an FTP request loop - FTP client and servers can exist on the same node. """ + self._active = True if not self._can_perform_action(): return False diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index 52f451e1..13acda70 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -3,9 +3,11 @@ from abc import ABC from ipaddress import IPv4Address from typing import Dict, Optional +from pydantic import StrictBool + from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode -from primaite.simulator.system.services.service import Service +from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.utils.validation.port import Port @@ -16,9 +18,22 @@ class FTPServiceABC(Service, ABC): Contains shared methods between both classes. """ + _active: StrictBool = False + """Flag that is True on timesteps where service transmits data and False when idle. Used for describe_state.""" + + def pre_timestep(self, timestep: int) -> None: + """When a new timestep begins, clear the _active attribute.""" + self._active = False + return super().pre_timestep(timestep) + def describe_state(self) -> Dict: """Returns a Dict of the FTPService state.""" - return super().describe_state() + state = super().describe_state() + + # override so that the service is shows as running only if actively transmitting data this timestep + if self.operating_state == ServiceOperatingState.RUNNING and not self._active: + state["operating_state"] = ServiceOperatingState.STOPPED.value + return state def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ @@ -29,6 +44,7 @@ class FTPServiceABC(Service, ABC): :param: session_id: session ID linked to the FTP Packet. Optional. :type: session_id: Optional[str] """ + self._active = True if payload.ftp_command is not None: self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.") @@ -51,6 +67,7 @@ class FTPServiceABC(Service, ABC): :param: payload: The FTP Packet that contains the file data :type: FTPPacket """ + self._active = True try: file_name = payload.ftp_command_args["dest_file_name"] folder_name = payload.ftp_command_args["dest_folder_name"] @@ -106,6 +123,7 @@ class FTPServiceABC(Service, ABC): :param: is_response: is true if the data being sent is in response to a request. Default False. :type: is_response: bool """ + self._active = True # send STOR request payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.STOR, @@ -135,6 +153,7 @@ class FTPServiceABC(Service, ABC): :param: payload: The FTP Packet that contains the file data :type: FTPPacket """ + self._active = True try: # find the file file_name = payload.ftp_command_args["src_file_name"] @@ -181,6 +200,7 @@ class FTPServiceABC(Service, ABC): :return: True if successful, False otherwise. """ + self._active = True self.sys_log.info(f"{self.name}: Sending FTP {payload.ftp_command.name} {payload.ftp_command_args}") return super().send( diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 9ab13036..3e57f579 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -69,8 +69,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 1cd0883c..9cf95a64 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -74,8 +74,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 10a92d7a..a39bf876 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -88,8 +88,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 328fe413..726c9ab0 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -160,8 +160,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index e277a881..41b7fce9 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -102,8 +102,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index 0ec0c91f..bff58ebd 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -161,8 +161,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 6b454a12..4b11dbcc 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -77,8 +77,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/fixing_duration_one_item.yaml b/tests/assets/configs/fixing_duration_one_item.yaml index 02aa8e4b..da5a9993 100644 --- a/tests/assets/configs/fixing_duration_one_item.yaml +++ b/tests/assets/configs/fixing_duration_one_item.yaml @@ -81,8 +81,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 3b746273..93baf4af 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -152,8 +152,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP @@ -666,8 +666,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 7ad5371d..d5615a72 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -151,8 +151,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/software_fixing_duration.yaml b/tests/assets/configs/software_fixing_duration.yaml index 073a5f83..f685b420 100644 --- a/tests/assets/configs/software_fixing_duration.yaml +++ b/tests/assets/configs/software_fixing_duration.yaml @@ -81,8 +81,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index cafcc72b..25bc38e6 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -155,8 +155,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index cd5d08d3..2d124981 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -103,8 +103,8 @@ agents: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index 0976abdc..cab80434 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -69,7 +69,7 @@ def test_file_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent file.corrupt() assert file.health_status == FileSystemItemHealthStatus.CORRUPT - assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file.visible_health_status == FileSystemItemHealthStatus.NONE action = ( "node_file_scan", diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index 9cd4bfcf..207f7d48 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -52,12 +52,12 @@ def test_folder_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge folder = client_1.file_system.get_folder(folder_name="downloads") assert folder.health_status == FileSystemItemHealthStatus.GOOD - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE folder.corrupt() assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE action = ( "node_folder_scan", diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index 0268cb95..19c0c4bc 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -32,11 +32,11 @@ def test_file_observation(simulation): assert dog_file_obs.space["health_status"] == spaces.Discrete(6) observation_state = dog_file_obs.observe(simulation.describe_state()) - assert observation_state.get("health_status") == 1 # good initial + assert observation_state.get("health_status") == 0 # initially unset file.corrupt() observation_state = dog_file_obs.observe(simulation.describe_state()) - assert observation_state.get("health_status") == 1 # scan file so this changes + assert observation_state.get("health_status") == 0 # still default unset value because no scan happened file.scan() file.apply_timestep(0) # apply time step @@ -63,11 +63,11 @@ def test_folder_observation(simulation): observation_state = root_folder_obs.observe(simulation.describe_state()) assert observation_state.get("FILES") is not None - assert observation_state.get("health_status") == 1 + assert observation_state.get("health_status") == 0 # initially unset file.corrupt() # corrupt just the file observation_state = root_folder_obs.observe(simulation.describe_state()) - assert observation_state.get("health_status") == 1 # scan folder to change this + assert observation_state.get("health_status") == 0 # still unset as no scan occurred yet folder.scan() for i in range(folder.scan_duration + 1): diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 800549bc..5a308cf8 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -275,7 +275,7 @@ def test_node_file_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAge client_1 = game.simulation.network.get_node_by_hostname("client_1") file = client_1.file_system.get_file("downloads", "cat.png") assert file.health_status == FileSystemItemHealthStatus.GOOD - assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file.visible_health_status == FileSystemItemHealthStatus.NONE # 2: perform a scan and make sure nothing has changed action = ( diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index 23364f13..5afad296 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -17,12 +17,7 @@ def test_file_observation(): dog_file_obs = FileObservation( where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"], include_num_access=False, - file_system_requires_scan=True, + file_system_requires_scan=False, ) assert dog_file_obs.observe(state) == {"health_status": 1} assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)}) - - -# TODO: -# def test_file_num_access(): -# ... diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 31732f77..bb25f8c8 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -163,7 +163,7 @@ def test_restore_backup_without_updating_scan(uc2_network): db_service.db_file.corrupt() # corrupt the db assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT # db file is actually corrupt - assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.GOOD # not scanned yet + assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.NONE # not scanned yet db_service.db_file.scan() # scan the db file @@ -190,7 +190,7 @@ def test_restore_backup_after_deleting_file_without_updating_scan(uc2_network): db_service.db_file.corrupt() # corrupt the db assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT # db file is actually corrupt - assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.GOOD # not scanned yet + assert db_service.db_file.visible_health_status == FileSystemItemHealthStatus.NONE # not scanned yet db_service.db_file.scan() # scan the db file diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index 5170bcf3..5156a29f 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -69,8 +69,8 @@ class TestFileSystemRequiresScan: wildcard_list: - 0.0.0.1 port_list: - - 80 - - 5432 + - HTTP + - POSTGRES_SERVER protocol_list: - ICMP - TCP @@ -119,14 +119,20 @@ class TestFileSystemRequiresScan: assert obs_not_requiring_scan.observe(file_state)["health_status"] == 3 def test_folder_require_scan(self): - folder_state = {"health_status": 3, "visible_status": 1} + folder_state = {"health_status": 3, "visible_status": 1, "scanned_this_step": False} obs_requiring_scan = FolderObservation( [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True ) - assert obs_requiring_scan.observe(folder_state)["health_status"] == 1 + assert obs_requiring_scan.observe(folder_state)["health_status"] == 0 obs_not_requiring_scan = FolderObservation( [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=False ) assert obs_not_requiring_scan.observe(folder_state)["health_status"] == 3 + + folder_state = {"health_status": 3, "visible_status": 1, "scanned_this_step": True} + obs_requiring_scan = FolderObservation( + [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True + ) + assert obs_requiring_scan.observe(folder_state)["health_status"] == 1 diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py index 9cacdccf..9691080d 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py @@ -22,12 +22,12 @@ def test_file_scan(file_system): file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder") assert file.health_status == FileSystemItemHealthStatus.GOOD - assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file.visible_health_status == FileSystemItemHealthStatus.NONE file.corrupt() assert file.health_status == FileSystemItemHealthStatus.CORRUPT - assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file.visible_health_status == FileSystemItemHealthStatus.NONE file.scan() diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 2729e5e4..59f3f000 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -24,7 +24,7 @@ def test_file_scan_request(populated_file_system): file.corrupt() assert file.health_status == FileSystemItemHealthStatus.CORRUPT - assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file.visible_health_status == FileSystemItemHealthStatus.NONE fs.apply_request(request=["folder", folder.name, "file", file.name, "scan"]) @@ -94,7 +94,7 @@ def test_deleted_file_cannot_be_interacted_with(populated_file_system): assert fs.get_file(folder_name=folder.name, file_name=file.name).health_status == FileSystemItemHealthStatus.CORRUPT assert ( fs.get_file(folder_name=folder.name, file_name=file.name).visible_health_status - == FileSystemItemHealthStatus.GOOD + == FileSystemItemHealthStatus.NONE ) fs.apply_request(request=["delete", "file", folder.name, file.name]) diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py index 10393c6c..b5d9b269 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py @@ -44,25 +44,25 @@ def test_folder_scan(file_system): file2: File = folder.get_file_by_id(file_uuid=list(folder.files)[0]) assert folder.health_status == FileSystemItemHealthStatus.GOOD - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE + assert file1.visible_health_status == FileSystemItemHealthStatus.NONE + assert file2.visible_health_status == FileSystemItemHealthStatus.NONE folder.corrupt() assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE + assert file1.visible_health_status == FileSystemItemHealthStatus.NONE + assert file2.visible_health_status == FileSystemItemHealthStatus.NONE folder.scan() folder.apply_timestep(timestep=0) assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE + assert file1.visible_health_status == FileSystemItemHealthStatus.NONE + assert file2.visible_health_status == FileSystemItemHealthStatus.NONE folder.apply_timestep(timestep=1) folder.apply_timestep(timestep=2) diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 07c1ec46..72857638 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -29,18 +29,18 @@ def test_folder_scan_request(populated_file_system): folder.corrupt() assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE + assert file1.visible_health_status == FileSystemItemHealthStatus.NONE + assert file2.visible_health_status == FileSystemItemHealthStatus.NONE fs.apply_request(request=["folder", folder.name, "scan"]) folder.apply_timestep(timestep=0) assert folder.health_status == FileSystemItemHealthStatus.CORRUPT - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file1.visible_health_status == FileSystemItemHealthStatus.GOOD - assert file2.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE + assert file1.visible_health_status == FileSystemItemHealthStatus.NONE + assert file2.visible_health_status == FileSystemItemHealthStatus.NONE folder.apply_timestep(timestep=1) folder.apply_timestep(timestep=2) diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 605f8c3b..672a4b5f 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -70,13 +70,13 @@ def test_node_os_scan(node): # add folder and file to node folder: Folder = node.file_system.create_folder(folder_name="test_folder") folder.corrupt() - assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + assert folder.visible_health_status == FileSystemItemHealthStatus.NONE file: File = node.file_system.create_file(folder_name="test_folder", file_name="file.txt") file2: File = node.file_system.create_file(folder_name="test_folder", file_name="file2.txt") file.corrupt() file2.corrupt() - assert file.visible_health_status == FileSystemItemHealthStatus.GOOD + assert file.visible_health_status == FileSystemItemHealthStatus.NONE # run os scan node.apply_request(["os", "scan"]) From c30c5189becd55812299cf72fc5b75c8a0ec7105 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 21 Jan 2025 13:17:42 +0000 Subject: [PATCH 144/224] fixes based on PR suggestions --- src/primaite/game/agent/agent_log.py | 2 +- src/primaite/game/agent/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 31d74176..fac92a94 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -36,7 +36,7 @@ class AgentLog: super().__init__() self.agent_name = agent_name if agent_name else "unnamed_agent" self.current_timestep: int = 0 - self.current_episode: int = 0 + self.current_episode: int = 1 self.setup_logger() @property diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 05e3643a..aac898e1 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -155,7 +155,7 @@ class AbstractAgent(BaseModel, ABC): @classmethod def from_config(cls, config: Dict) -> AbstractAgent: - """Grab the relevatn agent class and construct an instance from a config dict.""" + """Grab the relevant agent class and construct an instance from a config dict.""" agent_type = config["type"] agent_class = cls._registry[agent_type] return agent_class(config=config) From dcce678045ef4214659a03a984624fa26e6a1982 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 21 Jan 2025 13:20:19 +0000 Subject: [PATCH 145/224] update changelog --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de94f6f6..315579d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Agents now follow a common configuration format, simplifying the configuration of agents and their extensibilty. - Actions within PrimAITE are now extensible, allowing for plugin support. - +- Added a config schema to `ObservationManager`, `ActionManager`, and `RewardFunction`. +- Streamlined the way agents are created from config +- Agent config no longer requires a dummy action space if the action space is empty, the same applies for observation space and reward function +- Actions now support a config schema, to allow yaml data validation and default parameter values +- Action parameters are no longer defined through IDs, instead meaningful data is provided directly in the action map +- Test and example YAMLs have been updated to match the new agent and action schemas, such as: + - Removed empty action spaces, observation spaces, or reward spaces for agent which didn't use them + - Relabeled action parameters to match the new action config schemas, and updated the values to no longer rely on indices + - Removed action space options which were previously used for assigning meaning to action space IDs +- Updated tests that don't use YAMLs to still use the new action and agent schemas ## [3.3.0] - 2024-09-04 From 94ee16afa6252422df6af438f3f473ca0112cb11 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 21 Jan 2025 13:39:06 +0000 Subject: [PATCH 146/224] Remove todo comments that have been completed --- src/primaite/game/agent/observations/firewall_observation.py | 1 - src/primaite/simulator/system/services/terminal/terminal.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index a89ddfc5..a194bb53 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -72,7 +72,6 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): self.ports: List[PortObservation] = [ PortObservation(where=self.where + ["NICs", port_num]) for port_num in (1, 2, 3) ] - # TODO: check what the port nums are for firewall. self.internal_inbound_acl = ACLObservation( where=self.where + ["internal_inbound_acl", "acl"], diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 0d93248e..bda8bad3 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -286,7 +286,6 @@ class Terminal(Service, identifier="Terminal"): :param password: Password for login. :return: boolean, True if successful, else False """ - # TODO: Un-comment this when UserSessionManager is merged. connection_uuid = self.parent.user_session_manager.local_login(username=username, password=password) if connection_uuid: self.sys_log.info(f"{self.name}: Login request authorised, connection uuid: {connection_uuid}") @@ -413,7 +412,6 @@ class Terminal(Service, identifier="Terminal"): if isinstance(payload, SSHPacket): if payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST: # validate & add connection - # TODO: uncomment this as part of 2781 username = payload.user_account.username password = payload.user_account.password connection_id = self.parent.user_session_manager.remote_login( From 3f94c40434d11034cd1dc5dc9e07800c03bbcec4 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 22 Jan 2025 10:49:42 +0000 Subject: [PATCH 147/224] Fix logger inititialisation in agents --- src/primaite/game/agent/interface.py | 7 +++ src/primaite/game/game.py | 1 + .../notebooks/Training-an-SB3-Agent.ipynb | 48 ++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index aac898e1..6a6bc323 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -68,6 +68,10 @@ class AbstractAgent(BaseModel, ABC): ) reward_function: RewardFunction.ConfigSchema = Field(default_factory=lambda: RewardFunction.ConfigSchema()) + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.logger: AgentLog = AgentLog(agent_name=self.config.ref) + config: "AbstractAgent.ConfigSchema" = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) logger: AgentLog = AgentLog(agent_name="Abstract_Agent") @@ -81,6 +85,7 @@ class AbstractAgent(BaseModel, ABC): def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) + print("cls identifier:", identifier) if identifier is None: return if identifier in cls._registry: @@ -157,6 +162,8 @@ class AbstractAgent(BaseModel, ABC): def from_config(cls, config: Dict) -> AbstractAgent: """Grab the relevant agent class and construct an instance from a config dict.""" agent_type = config["type"] + print("agent_type:", agent_type) + print("cls._registry:", cls._registry) agent_class = cls._registry[agent_type] return agent_class(config=config) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f59117f4..c828a462 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -493,6 +493,7 @@ class PrimaiteGame: agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: + print("agent_cfg:", agent_cfg) new_agent = AbstractAgent.from_config(agent_cfg) game.agents[agent_cfg["ref"]] = new_agent if isinstance(new_agent, ProxyAgent): diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index 2b554475..0e3245ae 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -29,18 +29,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cls identifier: AbstractScriptedAgent\n", + "cls identifier: ProxyAgent\n" + ] + } + ], "source": [ "from primaite.game.game import PrimaiteGame\n", "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent\n", "import yaml" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -49,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -69,9 +79,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "agent_cfg: {'ref': 'client_2_green_user', 'team': 'GREEN', 'type': 'ProbabilisticAgent', 'agent_settings': {'action_probabilities': {0: 0.3, 1: 0.6, 2: 0.1}}, 'action_space': {'action_map': {0: {'action': 'do_nothing', 'options': {}}, 1: {'action': 'node_application_execute', 'options': {'node_name': 'client_2', 'application_name': 'WebBrowser'}}, 2: {'action': 'node_application_execute', 'options': {'node_name': 'client_2', 'application_name': 'DatabaseClient'}}}}, 'reward_function': {'reward_components': [{'type': 'WEBPAGE_UNAVAILABLE_PENALTY', 'weight': 0.25, 'options': {'node_hostname': 'client_2'}}, {'type': 'GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY', 'weight': 0.05, 'options': {'node_hostname': 'client_2'}}]}}\n", + "agent_type: ProbabilisticAgent\n", + "cls._registry: {'AbstractScriptedAgent': , 'ProxyAgent': }\n" + ] + }, + { + "ename": "KeyError", + "evalue": "'ProbabilisticAgent'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m gym \u001b[38;5;241m=\u001b[39m \u001b[43mPrimaiteGymEnv\u001b[49m\u001b[43m(\u001b[49m\u001b[43menv_config\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcfg\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/session/environment.py:71\u001b[0m, in \u001b[0;36mPrimaiteGymEnv.__init__\u001b[0;34m(self, env_config)\u001b[0m\n\u001b[1;32m 69\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mio \u001b[38;5;241m=\u001b[39m PrimaiteIO\u001b[38;5;241m.\u001b[39mfrom_config(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mepisode_scheduler(\u001b[38;5;241m0\u001b[39m)\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mio_settings\u001b[39m\u001b[38;5;124m\"\u001b[39m, {}))\n\u001b[1;32m 70\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Handles IO for the environment. This produces sys logs, agent logs, etc.\"\"\"\u001b[39;00m\n\u001b[0;32m---> 71\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame: PrimaiteGame \u001b[38;5;241m=\u001b[39m \u001b[43mPrimaiteGame\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_config\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mepisode_scheduler\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 72\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Current game.\"\"\"\u001b[39;00m\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_agent_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mnext\u001b[39m(\u001b[38;5;28miter\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mrl_agents))\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/game/game.py:497\u001b[0m, in \u001b[0;36mPrimaiteGame.from_config\u001b[0;34m(cls, cfg)\u001b[0m\n\u001b[1;32m 495\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m agent_cfg \u001b[38;5;129;01min\u001b[39;00m agents_cfg:\n\u001b[1;32m 496\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124magent_cfg:\u001b[39m\u001b[38;5;124m\"\u001b[39m, agent_cfg)\n\u001b[0;32m--> 497\u001b[0m new_agent \u001b[38;5;241m=\u001b[39m \u001b[43mAbstractAgent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43magent_cfg\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 498\u001b[0m game\u001b[38;5;241m.\u001b[39magents[agent_cfg[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mref\u001b[39m\u001b[38;5;124m\"\u001b[39m]] \u001b[38;5;241m=\u001b[39m new_agent\n\u001b[1;32m 499\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(new_agent, ProxyAgent):\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/game/agent/interface.py:163\u001b[0m, in \u001b[0;36mAbstractAgent.from_config\u001b[0;34m(cls, config)\u001b[0m\n\u001b[1;32m 161\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124magent_type:\u001b[39m\u001b[38;5;124m\"\u001b[39m, agent_type)\n\u001b[1;32m 162\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcls._registry:\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28mcls\u001b[39m\u001b[38;5;241m.\u001b[39m_registry)\n\u001b[0;32m--> 163\u001b[0m agent_class \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_registry\u001b[49m\u001b[43m[\u001b[49m\u001b[43magent_type\u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 164\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m agent_class(config\u001b[38;5;241m=\u001b[39mconfig)\n", + "\u001b[0;31mKeyError\u001b[0m: 'ProbabilisticAgent'" + ] + } + ], "source": [ "gym = PrimaiteGymEnv(env_config=cfg)" ] @@ -191,7 +225,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, From 0aa691752aa268880f1a9f7531286e5a159136d0 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 22 Jan 2025 12:20:45 +0000 Subject: [PATCH 148/224] #3075: Tidy up debug print statements. --- src/primaite/game/agent/interface.py | 3 -- src/primaite/game/game.py | 2 +- .../notebooks/Training-an-SB3-Agent.ipynb | 51 ++++--------------- 3 files changed, 10 insertions(+), 46 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 6a6bc323..5dab6aac 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -85,7 +85,6 @@ class AbstractAgent(BaseModel, ABC): def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) - print("cls identifier:", identifier) if identifier is None: return if identifier in cls._registry: @@ -162,8 +161,6 @@ class AbstractAgent(BaseModel, ABC): def from_config(cls, config: Dict) -> AbstractAgent: """Grab the relevant agent class and construct an instance from a config dict.""" agent_type = config["type"] - print("agent_type:", agent_type) - print("cls._registry:", cls._registry) agent_class = cls._registry[agent_type] return agent_class(config=config) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c828a462..d9f6fa3c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -493,7 +493,7 @@ class PrimaiteGame: agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: - print("agent_cfg:", agent_cfg) + new_agent = AbstractAgent.from_config(agent_cfg) game.agents[agent_cfg["ref"]] = new_agent if isinstance(new_agent, ProxyAgent): diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index 0e3245ae..d3492f92 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -29,18 +29,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cls identifier: AbstractScriptedAgent\n", - "cls identifier: ProxyAgent\n" - ] - } - ], + "outputs": [], "source": [ "from primaite.game.game import PrimaiteGame\n", "from primaite.session.environment import PrimaiteGymEnv\n", @@ -50,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -59,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -79,33 +70,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "agent_cfg: {'ref': 'client_2_green_user', 'team': 'GREEN', 'type': 'ProbabilisticAgent', 'agent_settings': {'action_probabilities': {0: 0.3, 1: 0.6, 2: 0.1}}, 'action_space': {'action_map': {0: {'action': 'do_nothing', 'options': {}}, 1: {'action': 'node_application_execute', 'options': {'node_name': 'client_2', 'application_name': 'WebBrowser'}}, 2: {'action': 'node_application_execute', 'options': {'node_name': 'client_2', 'application_name': 'DatabaseClient'}}}}, 'reward_function': {'reward_components': [{'type': 'WEBPAGE_UNAVAILABLE_PENALTY', 'weight': 0.25, 'options': {'node_hostname': 'client_2'}}, {'type': 'GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY', 'weight': 0.05, 'options': {'node_hostname': 'client_2'}}]}}\n", - "agent_type: ProbabilisticAgent\n", - "cls._registry: {'AbstractScriptedAgent': , 'ProxyAgent': }\n" - ] - }, - { - "ename": "KeyError", - "evalue": "'ProbabilisticAgent'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m gym \u001b[38;5;241m=\u001b[39m \u001b[43mPrimaiteGymEnv\u001b[49m\u001b[43m(\u001b[49m\u001b[43menv_config\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcfg\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/session/environment.py:71\u001b[0m, in \u001b[0;36mPrimaiteGymEnv.__init__\u001b[0;34m(self, env_config)\u001b[0m\n\u001b[1;32m 69\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mio \u001b[38;5;241m=\u001b[39m PrimaiteIO\u001b[38;5;241m.\u001b[39mfrom_config(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mepisode_scheduler(\u001b[38;5;241m0\u001b[39m)\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mio_settings\u001b[39m\u001b[38;5;124m\"\u001b[39m, {}))\n\u001b[1;32m 70\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Handles IO for the environment. This produces sys logs, agent logs, etc.\"\"\"\u001b[39;00m\n\u001b[0;32m---> 71\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame: PrimaiteGame \u001b[38;5;241m=\u001b[39m \u001b[43mPrimaiteGame\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_config\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mepisode_scheduler\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 72\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Current game.\"\"\"\u001b[39;00m\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_agent_name \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mnext\u001b[39m(\u001b[38;5;28miter\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mrl_agents))\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/game/game.py:497\u001b[0m, in \u001b[0;36mPrimaiteGame.from_config\u001b[0;34m(cls, cfg)\u001b[0m\n\u001b[1;32m 495\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m agent_cfg \u001b[38;5;129;01min\u001b[39;00m agents_cfg:\n\u001b[1;32m 496\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124magent_cfg:\u001b[39m\u001b[38;5;124m\"\u001b[39m, agent_cfg)\n\u001b[0;32m--> 497\u001b[0m new_agent \u001b[38;5;241m=\u001b[39m \u001b[43mAbstractAgent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfrom_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43magent_cfg\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 498\u001b[0m game\u001b[38;5;241m.\u001b[39magents[agent_cfg[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mref\u001b[39m\u001b[38;5;124m\"\u001b[39m]] \u001b[38;5;241m=\u001b[39m new_agent\n\u001b[1;32m 499\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(new_agent, ProxyAgent):\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/game/agent/interface.py:163\u001b[0m, in \u001b[0;36mAbstractAgent.from_config\u001b[0;34m(cls, config)\u001b[0m\n\u001b[1;32m 161\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124magent_type:\u001b[39m\u001b[38;5;124m\"\u001b[39m, agent_type)\n\u001b[1;32m 162\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcls._registry:\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28mcls\u001b[39m\u001b[38;5;241m.\u001b[39m_registry)\n\u001b[0;32m--> 163\u001b[0m agent_class \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_registry\u001b[49m\u001b[43m[\u001b[49m\u001b[43magent_type\u001b[49m\u001b[43m]\u001b[49m\n\u001b[1;32m 164\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m agent_class(config\u001b[38;5;241m=\u001b[39mconfig)\n", - "\u001b[0;31mKeyError\u001b[0m: 'ProbabilisticAgent'" - ] - } - ], + "outputs": [], "source": [ "gym = PrimaiteGymEnv(env_config=cfg)" ] @@ -134,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -166,7 +133,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -211,7 +178,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, From 5563b9c62d179d2cbaa502ef0f98350ec3c9fde6 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 22 Jan 2025 16:55:59 +0000 Subject: [PATCH 149/224] #2869 - fixes to agents and remove redundant prints --- src/primaite/game/agent/__init__.py | 6 ++++++ src/primaite/game/agent/actions/acl.py | 2 +- src/primaite/game/agent/actions/node.py | 1 - src/primaite/game/agent/scripted_agents/random_agent.py | 6 +++--- src/primaite/game/game.py | 2 +- .../simulator/network/hardware/nodes/network/firewall.py | 8 -------- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/primaite/game/agent/__init__.py b/src/primaite/game/agent/__init__.py index 836b79af..c005c173 100644 --- a/src/primaite/game/agent/__init__.py +++ b/src/primaite/game/agent/__init__.py @@ -1 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from primaite.game.agent.interface import ProxyAgent +from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent +from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent +from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent, RandomAgent + +__all__ = ("ProbabilisticAgent", "ProxyAgent", "RandomAgent", "PeriodicAgent", "DataManipulationAgent") diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 3341868f..7b70d10d 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -26,7 +26,7 @@ class ACLAddRuleAbstractAction(AbstractAction, ABC): class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema base for ACL add rule abstract actions.""" - src_ip: IPV4Address + src_ip: Union[IPV4Address, Literal["ALL"]] protocol_name: Union[IPProtocol, Literal["ALL"]] permission: Literal["PERMIT", "DENY"] position: int diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index fbab18f0..5e1b6725 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -36,7 +36,6 @@ class NodeAbstractAction(AbstractAction, identifier="node_abstract"): @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - print(config) return ["network", "node", config.node_name, config.verb] diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 721b5293..9d82a063 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -44,10 +44,12 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): start_step: int = 5 "The timestep at which an agent begins performing it's actions" + start_variance: int = 0 frequency: int = 5 "The number of timesteps to wait between performing actions" variance: int = 0 "The amount the frequency can randomly change to" + max_executions: int = 999999 possible_start_nodes: List[str] target_application: str @@ -76,8 +78,6 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): default_factory=lambda: PeriodicAgent.AgentSettingsSchema() ) - max_executions: int = 999999 - "Maximum number of times the agent can execute its action." num_executions: int = 0 """Number of times the agent has executed an action.""" next_execution_timestep: int = 0 @@ -102,7 +102,7 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: """Do nothing, unless the current timestep is the next execution timestep, in which case do the action.""" - if timestep == self.next_execution_timestep and self.num_executions < self.max_executions: + if timestep == self.next_execution_timestep and self.num_executions < self.config.agent_settings.max_executions: self.num_executions += 1 self._set_next_execution_timestep( timestep + self.config.agent_settings.frequency, self.config.agent_settings.variance diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f59117f4..b869cfd4 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -383,7 +383,7 @@ class PrimaiteGame: if service_class is not None: _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") - new_node.software_manager.install(service_class) + new_node.software_manager.install(service_class, software_config=service_cfg.get("options", {})) new_service = new_node.software_manager.software[service_class.__name__] # fixing duration for the service diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index f1ca4930..ac7c12e3 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -258,23 +258,15 @@ class Firewall(Router, identifier="firewall"): :param dmz: If True, shows ACL rules for DMZ interfaces. :param markdown: If True, formats the output in Markdown, enhancing readability in Markdown-compatible viewers. """ - print(f"{self.hostname} Firewall Rules") - print() if external: self.external_inbound_acl.show(markdown) - print() self.external_outbound_acl.show(markdown) - print() if internal: self.internal_inbound_acl.show(markdown) - print() self.internal_outbound_acl.show(markdown) - print() if dmz: self.dmz_inbound_acl.show(markdown) - print() self.dmz_outbound_acl.show(markdown) - print() def receive_frame(self, frame: Frame, from_network_interface: RouterInterface): """ From 3957142afdf5083d2af032d6198f8c4dbbfc5827 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 22 Jan 2025 17:20:38 +0000 Subject: [PATCH 150/224] #2887 - Updates to Node components to use rom_config and allow for extensibility. Router and Firewall continue to have custom from_config. Some test fixes to reflect changes to functionality. --- .../source/how_to_guides/extensible_nodes.rst | 58 ++++++++ src/primaite/game/game.py | 7 +- src/primaite/simulator/core.py | 2 +- src/primaite/simulator/network/container.py | 28 ++-- .../simulator/network/hardware/base.py | 17 ++- .../network/hardware/nodes/host/host_node.py | 7 +- .../network/hardware/nodes/host/server.py | 17 +++ .../hardware/nodes/network/firewall.py | 82 ++++++----- .../network/hardware/nodes/network/router.py | 138 ++++++++++++++++-- .../network/hardware/nodes/network/switch.py | 6 +- src/primaite/simulator/system/software.py | 2 +- tests/assets/configs/data_manipulation.yaml | 2 +- tests/conftest.py | 99 ++++++++----- .../nodes/network/test_router_config.py | 1 + .../observations/test_acl_observations.py | 2 +- .../test_file_system_observations.py | 4 +- .../_network/_hardware/nodes/test_router.py | 2 +- 17 files changed, 350 insertions(+), 124 deletions(-) create mode 100644 docs/source/how_to_guides/extensible_nodes.rst diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst new file mode 100644 index 00000000..21907767 --- /dev/null +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -0,0 +1,58 @@ +.. only:: comment + + © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK + +.. _about: + + +Extensible Nodes +**************** + +Node classes within PrimAITE have been updated to allow for easier generation of custom nodes within simulations. + + +Changes to Node Class structure. +================================ + +Node classes all inherit from the base Node Class, though new classes should inherit from either HostNode or NetworkNode, subject to the intended application of the Node. + +The use of an `__init__` method is not necessary, as configurable variables for the class should be specified within the `config` of the class, and passed at run time via your YAML configuration using the `from_config` method. + + +An example of how additional Node classes is below, taken from `router.py` withing PrimAITE. + +.. code-block:: Python + +class Router(NetworkNode, identifier="router"): + """ Represents a network router within the simulation, managing routing and forwarding of IP packets across network interfaces.""" + + SYSTEM_SOFTWARE: ClassVar[Dict] = { + "UserSessionManager": UserSessionManager, + "UserManager": UserManager, + "Terminal": Terminal, + } + + network_interfaces: Dict[str, RouterInterface] = {} + "The Router Interfaces on the node." + network_interface: Dict[int, RouterInterface] = {} + "The Router Interfaces on the node by port id." + + sys_log: SysLog + + config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSchema()) + + class ConfigSchema(NetworkNode.ConfigSchema): + """Configuration Schema for Router Objects.""" + + num_ports: int = 5 + + hostname: ClassVar[str] = "Router" + + ports: list = [] + + + +Changes to YAML file. +===================== + +Nodes defined within configuration YAML files for use with PrimAITE 3.X should still be compatible following these changes. \ No newline at end of file diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 6599430a..b9dc9c4d 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -271,12 +271,13 @@ class PrimaiteGame: for node_cfg in nodes_cfg: n_type = node_cfg["type"] - node_config: dict = node_cfg["config"] + # node_config: dict = node_cfg["config"] + print(f"{n_type}:{node_cfg}") new_node = None if n_type in Node._registry: # simplify down Node creation: - new_node = Node._registry["n_type"].from_config(config=node_config) + new_node = Node._registry[n_type].from_config(config=node_cfg) else: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) @@ -313,7 +314,7 @@ class PrimaiteGame: service_class = SERVICE_TYPES_MAPPING[service_type] if service_class is not None: - _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") + _LOGGER.debug(f"installing {service_type} on node {new_node.config.hostname}") new_node.software_manager.install(service_class, **service_cfg.get("options", {})) new_service = new_node.software_manager.software[service_class.__name__] diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 567a0493..dc4ae73b 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -3,7 +3,7 @@ """Core of the PrimAITE Simulator.""" import warnings from abc import abstractmethod -from typing import Callable, Dict, Iterable, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Literal, Optional, Tuple, Union from uuid import uuid4 from prettytable import PrettyTable diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index bf677d5c..aac82633 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -180,7 +180,7 @@ class Network(SimComponent): table.align = "l" table.title = "Nodes" for node in self.nodes.values(): - table.add_row((node.hostname, type(node)._identifier, node.operating_state.name)) + table.add_row((node.config.hostname, type(node)._identifier, node.operating_state.name)) print(table) if ip_addresses: @@ -196,7 +196,7 @@ class Network(SimComponent): if port.ip_address != IPv4Address("127.0.0.1"): port_str = port.port_name if port.port_name else port.port_num table.add_row( - [node.hostname, port_str, port.ip_address, port.subnet_mask, node.default_gateway] + [node.config.hostname, port_str, port.ip_address, port.subnet_mask, node.default_gateway] ) print(table) @@ -215,9 +215,9 @@ class Network(SimComponent): if node in [link.endpoint_a.parent, link.endpoint_b.parent]: table.add_row( [ - link.endpoint_a.parent.hostname, + link.endpoint_a.parent.config.hostname, str(link.endpoint_a), - link.endpoint_b.parent.hostname, + link.endpoint_b.parent.config.hostname, str(link.endpoint_b), link.is_up, link.bandwidth, @@ -251,7 +251,7 @@ class Network(SimComponent): state = super().describe_state() state.update( { - "nodes": {node.hostname: node.describe_state() for node in self.nodes.values()}, + "nodes": {node.config.hostname: node.describe_state() for node in self.nodes.values()}, "links": {}, } ) @@ -259,8 +259,8 @@ class Network(SimComponent): for _, link in self.links.items(): node_a = link.endpoint_a._connected_node node_b = link.endpoint_b._connected_node - hostname_a = node_a.hostname if node_a else None - hostname_b = node_b.hostname if node_b else None + hostname_a = node_a.config.hostname if node_a else None + hostname_b = node_b.config.hostname if node_b else None port_a = link.endpoint_a.port_num port_b = link.endpoint_b.port_num link_key = f"{hostname_a}:eth-{port_a}<->{hostname_b}:eth-{port_b}" @@ -286,9 +286,9 @@ class Network(SimComponent): self.nodes[node.uuid] = node self._node_id_map[len(self.nodes)] = node node.parent = self - self._nx_graph.add_node(node.hostname) + self._nx_graph.add_node(node.config.hostname) _LOGGER.debug(f"Added node {node.uuid} to Network {self.uuid}") - self._node_request_manager.add_request(name=node.hostname, request_type=RequestType(func=node._request_manager)) + self._node_request_manager.add_request(name=node.config.hostname, request_type=RequestType(func=node._request_manager)) def get_node_by_hostname(self, hostname: str) -> Optional[Node]: """ @@ -300,7 +300,7 @@ class Network(SimComponent): :return: The Node if it exists in the network. """ for node in self.nodes.values(): - if node.hostname == hostname: + if node.config.hostname == hostname: return node def remove_node(self, node: Node) -> None: @@ -313,7 +313,7 @@ class Network(SimComponent): :type node: Node """ if node not in self: - _LOGGER.warning(f"Can't remove node {node.hostname}. It's not in the network.") + _LOGGER.warning(f"Can't remove node {node.config.hostname}. It's not in the network.") return self.nodes.pop(node.uuid) for i, _node in self._node_id_map.items(): @@ -321,8 +321,8 @@ class Network(SimComponent): self._node_id_map.pop(i) break node.parent = None - self._node_request_manager.remove_request(name=node.hostname) - _LOGGER.info(f"Removed node {node.hostname} from network {self.uuid}") + self._node_request_manager.remove_request(name=node.config.hostname) + _LOGGER.info(f"Removed node {node.config.hostname} from network {self.uuid}") def connect( self, endpoint_a: WiredNetworkInterface, endpoint_b: WiredNetworkInterface, bandwidth: int = 100, **kwargs @@ -352,7 +352,7 @@ class Network(SimComponent): link = Link(endpoint_a=endpoint_a, endpoint_b=endpoint_b, bandwidth=bandwidth, **kwargs) self.links[link.uuid] = link self._link_id_map[len(self.links)] = link - self._nx_graph.add_edge(endpoint_a.parent.hostname, endpoint_b.parent.hostname) + self._nx_graph.add_edge(endpoint_a.parent.config.hostname, endpoint_b.parent.config.hostname) link.parent = self _LOGGER.debug(f"Added link {link.uuid} to connect {endpoint_a} and {endpoint_b}") return link diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 822714cb..dbe9705b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -431,7 +431,7 @@ class WiredNetworkInterface(NetworkInterface, ABC): self.enabled = True self._connected_node.sys_log.info(f"Network Interface {self} enabled") self.pcap = PacketCapture( - hostname=self._connected_node.hostname, port_num=self.port_num, port_name=self.port_name + hostname=self._connected_node.config.hostname, port_num=self.port_num, port_name=self.port_name ) if self._connected_link: self._connected_link.endpoint_up() @@ -1515,14 +1515,16 @@ class Node(SimComponent, ABC): _identifier: ClassVar[str] = "unknown" """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" - config: Node.ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) + config: Node.ConfigSchema + """Configuration items within Node""" class ConfigSchema(BaseModel, ABC): """Configuration Schema for Node based classes.""" model_config = ConfigDict(arbitrary_types_allowed=True) """Configure pydantic to allow arbitrary types and to let the instance have attributes not present in the model.""" - hostname: str + + hostname: str = "default" "The node hostname on the network." revealed_to_red: bool = False @@ -1552,6 +1554,7 @@ class Node(SimComponent, ABC): red_scan_countdown: int = 0 "Time steps until reveal to red scan is complete." + @classmethod def from_config(cls, config: Dict) -> "Node": """Create Node object from a given configuration dictionary.""" @@ -1586,11 +1589,11 @@ class Node(SimComponent, ABC): provided. """ if not kwargs.get("sys_log"): - kwargs["sys_log"] = SysLog(kwargs["hostname"]) + kwargs["sys_log"] = SysLog(kwargs["config"].hostname) if not kwargs.get("session_manager"): kwargs["session_manager"] = SessionManager(sys_log=kwargs.get("sys_log")) if not kwargs.get("root"): - kwargs["root"] = SIM_OUTPUT.path / kwargs["hostname"] + kwargs["root"] = SIM_OUTPUT.path / kwargs["config"].hostname if not kwargs.get("file_system"): kwargs["file_system"] = FileSystem(sys_log=kwargs["sys_log"], sim_root=kwargs["root"] / "fs") if not kwargs.get("software_manager"): @@ -1601,10 +1604,12 @@ class Node(SimComponent, ABC): file_system=kwargs.get("file_system"), dns_server=kwargs.get("dns_server"), ) + super().__init__(**kwargs) self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager + self.power_on() @property def user_manager(self) -> Optional[UserManager]: @@ -1856,7 +1861,7 @@ class Node(SimComponent, ABC): state = super().describe_state() state.update( { - "hostname": self.hostname, + "hostname": self.config.hostname, "operating_state": self.operating_state.value, "NICs": { eth_num: network_interface.describe_state() diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 00f21342..23db025d 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -333,10 +333,13 @@ class HostNode(Node, identifier="HostNode"): """Configuration Schema for HostNode class.""" hostname: str = "HostNode" + ip_address: IPV4Address = "192.168.0.1" + subnet_mask: IPV4Address = "255.255.255.0" + default_gateway: IPV4Address = "192.168.10.1" - def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): + def __init__(self, **kwargs): super().__init__(**kwargs) - self.connect_nic(NIC(ip_address=ip_address, subnet_mask=subnet_mask)) + self.connect_nic(NIC(ip_address=kwargs["config"].ip_address, subnet_mask=kwargs["config"].subnet_mask)) @property def nmap(self) -> Optional[NMAP]: diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index e16cfd8f..1b3f6c58 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,4 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from typing import ClassVar +from pydantic import Field from primaite.simulator.network.hardware.nodes.host.host_node import HostNode @@ -30,8 +32,23 @@ class Server(HostNode, identifier="server"): * Web Browser """ + config: "Server.ConfigSchema" = Field(default_factory=lambda: Server.ConfigSchema()) + + class ConfigSchema(HostNode.ConfigSchema): + """Configuration Schema for Server class.""" + + hostname: str = "server" + class Printer(HostNode, identifier="printer"): """Printer? I don't even know her!.""" # TODO: Implement printer-specific behaviour + + + config: "Printer.ConfigSchema" = Field(default_factory=lambda: Printer.ConfigSchema()) + + class ConfigSchema(HostNode.ConfigSchema): + """Configuration Schema for Printer class.""" + + hostname: ClassVar[str] = "printer" \ No newline at end of file diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index c7e22d49..2ebfe44a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -99,19 +99,22 @@ class Firewall(Router, identifier="firewall"): ) """Access Control List for managing traffic leaving towards an external network.""" + _identifier: str = "firewall" + config: "Firewall.ConfigSchema" = Field(default_factory=lambda: Firewall.ConfigSchema()) - class ConfigSchema(Router.ConfigSChema): + class ConfigSchema(Router.ConfigSchema): """Configuration Schema for Firewall 'Nodes' within PrimAITE.""" - hostname: str = "Firewall" + hostname: str = "firewall" num_ports: int = 0 + operating_state: NodeOperatingState = NodeOperatingState.ON def __init__(self, **kwargs): if not kwargs.get("sys_log"): - kwargs["sys_log"] = SysLog(self.config.hostname) + kwargs["sys_log"] = SysLog(kwargs["config"].hostname) - super().__init__(hostname=self.config.hostname, num_ports=self.config.num_ports, **kwargs) + super().__init__(**kwargs) self.connect_nic( RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", port_name="external") @@ -124,22 +127,22 @@ class Firewall(Router, identifier="firewall"): ) # Update ACL objects with firewall's hostname and syslog to allow accurate logging self.internal_inbound_acl.sys_log = kwargs["sys_log"] - self.internal_inbound_acl.name = f"{hostname} - Internal Inbound" + self.internal_inbound_acl.name = f"{kwargs['config'].hostname} - Internal Inbound" self.internal_outbound_acl.sys_log = kwargs["sys_log"] - self.internal_outbound_acl.name = f"{hostname} - Internal Outbound" + self.internal_outbound_acl.name = f"{kwargs['config'].hostname} - Internal Outbound" self.dmz_inbound_acl.sys_log = kwargs["sys_log"] - self.dmz_inbound_acl.name = f"{hostname} - DMZ Inbound" + self.dmz_inbound_acl.name = f"{kwargs['config'].hostname} - DMZ Inbound" self.dmz_outbound_acl.sys_log = kwargs["sys_log"] - self.dmz_outbound_acl.name = f"{hostname} - DMZ Outbound" + self.dmz_outbound_acl.name = f"{kwargs['config'].hostname} - DMZ Outbound" self.external_inbound_acl.sys_log = kwargs["sys_log"] - self.external_inbound_acl.name = f"{hostname} - External Inbound" + self.external_inbound_acl.name = f"{kwargs['config'].hostname} - External Inbound" self.external_outbound_acl.sys_log = kwargs["sys_log"] - self.external_outbound_acl.name = f"{hostname} - External Outbound" + self.external_outbound_acl.name = f"{kwargs['config'].hostname} - External Outbound" def _init_request_manager(self) -> RequestManager: """ @@ -567,18 +570,21 @@ class Firewall(Router, identifier="firewall"): self.dmz_port.enable() @classmethod - def from_config(cls, cfg: dict) -> "Firewall": + def from_config(cls, config: dict) -> "Firewall": """Create a firewall based on a config dict.""" - firewall = Firewall( - hostname=cfg["hostname"], - operating_state=NodeOperatingState.ON - if not (p := cfg.get("operating_state")) - else NodeOperatingState[p.upper()], - ) - if "ports" in cfg: - internal_port = cfg["ports"]["internal_port"] - external_port = cfg["ports"]["external_port"] - dmz_port = cfg["ports"].get("dmz_port") + # firewall = Firewall( + # hostname=config["hostname"], + # operating_state=NodeOperatingState.ON + # if not (p := config.get("operating_state")) + # else NodeOperatingState[p.upper()], + # ) + + firewall = Firewall(config = cls.ConfigSchema(**config)) + + if "ports" in config: + internal_port = config["ports"]["internal_port"] + external_port = config["ports"]["external_port"] + dmz_port = config["ports"].get("dmz_port") # configure internal port firewall.configure_internal_port( @@ -598,10 +604,10 @@ class Firewall(Router, identifier="firewall"): ip_address=IPV4Address(dmz_port.get("ip_address")), subnet_mask=IPV4Address(dmz_port.get("subnet_mask", "255.255.255.0")), ) - if "acl" in cfg: + if "acl" in config: # acl rules for internal_inbound_acl - if cfg["acl"]["internal_inbound_acl"]: - for r_num, r_cfg in cfg["acl"]["internal_inbound_acl"].items(): + if config["acl"]["internal_inbound_acl"]: + for r_num, r_cfg in config["acl"]["internal_inbound_acl"].items(): firewall.internal_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -615,8 +621,8 @@ class Firewall(Router, identifier="firewall"): ) # acl rules for internal_outbound_acl - if cfg["acl"]["internal_outbound_acl"]: - for r_num, r_cfg in cfg["acl"]["internal_outbound_acl"].items(): + if config["acl"]["internal_outbound_acl"]: + for r_num, r_cfg in config["acl"]["internal_outbound_acl"].items(): firewall.internal_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -630,8 +636,8 @@ class Firewall(Router, identifier="firewall"): ) # acl rules for dmz_inbound_acl - if cfg["acl"]["dmz_inbound_acl"]: - for r_num, r_cfg in cfg["acl"]["dmz_inbound_acl"].items(): + if config["acl"]["dmz_inbound_acl"]: + for r_num, r_cfg in config["acl"]["dmz_inbound_acl"].items(): firewall.dmz_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -645,8 +651,8 @@ class Firewall(Router, identifier="firewall"): ) # acl rules for dmz_outbound_acl - if cfg["acl"]["dmz_outbound_acl"]: - for r_num, r_cfg in cfg["acl"]["dmz_outbound_acl"].items(): + if config["acl"]["dmz_outbound_acl"]: + for r_num, r_cfg in config["acl"]["dmz_outbound_acl"].items(): firewall.dmz_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -660,8 +666,8 @@ class Firewall(Router, identifier="firewall"): ) # acl rules for external_inbound_acl - if cfg["acl"].get("external_inbound_acl"): - for r_num, r_cfg in cfg["acl"]["external_inbound_acl"].items(): + if config["acl"].get("external_inbound_acl"): + for r_num, r_cfg in config["acl"]["external_inbound_acl"].items(): firewall.external_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -675,8 +681,8 @@ class Firewall(Router, identifier="firewall"): ) # acl rules for external_outbound_acl - if cfg["acl"].get("external_outbound_acl"): - for r_num, r_cfg in cfg["acl"]["external_outbound_acl"].items(): + if config["acl"].get("external_outbound_acl"): + for r_num, r_cfg in config["acl"]["external_outbound_acl"].items(): firewall.external_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -689,16 +695,16 @@ class Firewall(Router, identifier="firewall"): position=r_num, ) - if "routes" in cfg: - for route in cfg.get("routes"): + if "routes" in config: + for route in config.get("routes"): firewall.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric", 0)), ) - if "default_route" in cfg: - next_hop_ip_address = cfg["default_route"].get("next_hop_ip_address", None) + if "default_route" in config: + next_hop_ip_address = config["default_route"].get("next_hop_ip_address", None) if next_hop_ip_address: firewall.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 83fa066d..e475df66 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1212,21 +1212,34 @@ class Router(NetworkNode, identifier="router"): network_interface: Dict[int, RouterInterface] = {} "The Router Interfaces on the node by port id." - config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSchema()) + sys_log: SysLog = None + + acl: AccessControlList = None + + route_table: RouteTable = None + + config: "Router.ConfigSchema" class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Router Objects.""" num_ports: int = 5 + """Number of ports available for this Router. Default is 5""" + hostname: str = "Router" - ports: list = [] - sys_log: SysLog = SysLog(hostname) - acl: AccessControlList = AccessControlList(sys_log=sys_log, implicit_action=ACLAction.DENY, name=hostname) - route_table: RouteTable = RouteTable(sys_log=sys_log) + + ports: Dict[Union[int, str], Dict] = {} + def __init__(self, **kwargs): - super().__init__(hostname=self.config.hostname, num_ports=self.config.num_ports, **kwargs) - self.session_manager = RouterSessionManager(sys_log=self.config.sys_log) + if not kwargs.get("sys_log"): + kwargs["sys_log"] = SysLog(kwargs["config"].hostname) + if not kwargs.get("acl"): + kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname) + if not kwargs.get("route_table"): + kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"]) + super().__init__(**kwargs) + self.session_manager = RouterSessionManager(sys_log=self.sys_log) self.session_manager.node = self self.software_manager.session_manager = self.session_manager self.session_manager.software_manager = self.software_manager @@ -1234,9 +1247,11 @@ class Router(NetworkNode, identifier="router"): network_interface = RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0") self.connect_nic(network_interface) self.network_interface[i] = network_interface - self.operating_state = NodeOperatingState.ON + self._set_default_acl() + + def _install_system_software(self): """ Installs essential system software and network services on the router. @@ -1260,10 +1275,10 @@ class Router(NetworkNode, identifier="router"): Initializes the router's ACL (Access Control List) with default rules, permitting essential protocols like ARP and ICMP, which are necessary for basic network operations and diagnostics. """ - self.config.acl.add_rule( + self.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 ) - self.config.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) + self.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) def setup_for_episode(self, episode: int): """ @@ -1287,7 +1302,7 @@ class Router(NetworkNode, identifier="router"): More information in user guide and docstring for SimComponent._init_request_manager. """ rm = super()._init_request_manager() - rm.add_request("acl", RequestType(func=self.config.acl._request_manager)) + rm.add_request("acl", RequestType(func=self.acl._request_manager)) return rm def ip_is_router_interface(self, ip_address: IPv4Address, enabled_only: bool = False) -> bool: @@ -1341,7 +1356,7 @@ class Router(NetworkNode, identifier="router"): """ state = super().describe_state() state["num_ports"] = self.config.num_ports - state["acl"] = self.config.acl.describe_state() + state["acl"] = self.acl.describe_state() return state def check_send_frame_to_session_manager(self, frame: Frame) -> bool: @@ -1562,7 +1577,7 @@ class Router(NetworkNode, identifier="router"): print(table) def setup_router(self, cfg: dict) -> Router: - """ TODO: This is the extra bit of Router's from_config metho. Needs sorting.""" + """TODO: This is the extra bit of Router's from_config metho. Needs sorting.""" if "ports" in cfg: for port_num, port_cfg in cfg["ports"].items(): self.configure_port( @@ -1594,5 +1609,100 @@ class Router(NetworkNode, identifier="router"): if "default_route" in cfg: next_hop_ip_address = cfg["default_route"].get("next_hop_ip_address", None) if next_hop_ip_address: - self.config.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) + self.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) return self + + + @classmethod + def from_config(cls, config: dict, **kwargs) -> "Router": + """Create a router based on a config dict. + + Schema: + - hostname (str): unique name for this router. + - num_ports (int, optional): Number of network ports on the router. 8 by default + - ports (dict): Dict with integers from 1 - num_ports as keys. The values should be another dict specifying + ip_address and subnet_mask assigned to that ports (as strings) + - acl (dict): Dict with integers from 1 - max_acl_rules as keys. The key defines the position within the ACL + where the rule will be added (lower number is resolved first). The values should describe valid ACL + Rules as: + - action (str): either PERMIT or DENY + - src_port (str, optional): the named port such as HTTP, HTTPS, or POSTGRES_SERVER + - dst_port (str, optional): the named port such as HTTP, HTTPS, or POSTGRES_SERVER + - protocol (str, optional): the named IP protocol such as ICMP, TCP, or UDP + - src_ip_address (str, optional): IP address octet written in base 10 + - dst_ip_address (str, optional): IP address octet written in base 10 + - routes (list[dict]): List of route dicts with values: + - address (str): The destination address of the route. + - subnet_mask (str): The subnet mask of the route. + - next_hop_ip_address (str): The next hop IP for the route. + - metric (int): The metric of the route. Optional. + - default_route: + - next_hop_ip_address (str): The next hop IP for the route. + + Example config: + ``` + { + 'hostname': 'router_1', + 'num_ports': 5, + 'ports': { + 1: { + 'ip_address' : '192.168.1.1', + 'subnet_mask' : '255.255.255.0', + }, + 2: { + 'ip_address' : '192.168.0.1', + 'subnet_mask' : '255.255.255.252', + } + }, + 'acl' : { + 21: {'action': 'PERMIT', 'src_port': 'HTTP', dst_port: 'HTTP'}, + 22: {'action': 'PERMIT', 'src_port': 'ARP', 'dst_port': 'ARP'}, + 23: {'action': 'PERMIT', 'protocol': 'ICMP'}, + }, + 'routes' : [ + {'address': '192.168.0.0', 'subnet_mask': '255.255.255.0', 'next_hop_ip_address': '192.168.1.2'} + ], + 'default_route': {'next_hop_ip_address': '192.168.0.2'} + } + ``` + + :param cfg: Router config adhering to schema described in main docstring body + :type cfg: dict + :return: Configured router. + :rtype: Router + """ + router = Router(config=Router.ConfigSchema(**config) + ) + if "ports" in config: + for port_num, port_cfg in config["ports"].items(): + router.configure_port( + port=port_num, + ip_address=port_cfg["ip_address"], + subnet_mask=IPv4Address(port_cfg.get("subnet_mask", "255.255.255.0")), + ) + if "acl" in config: + for r_num, r_cfg in config["acl"].items(): + router.acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], + protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], + src_ip_address=r_cfg.get("src_ip"), + src_wildcard_mask=r_cfg.get("src_wildcard_mask"), + dst_ip_address=r_cfg.get("dst_ip"), + dst_wildcard_mask=r_cfg.get("dst_wildcard_mask"), + position=r_num, + ) + if "routes" in config: + for route in config.get("routes"): + router.route_table.add_route( + address=IPv4Address(route.get("address")), + subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), + next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), + metric=float(route.get("metric", 0)), + ) + if "default_route" in config: + next_hop_ip_address = config["default_route"].get("next_hop_ip_address", None) + if next_hop_ip_address: + router.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) + return router \ No newline at end of file diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index a2d0050b..2ca0cafd 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from typing import Dict, Optional +from typing import ClassVar, Dict, Optional from prettytable import MARKDOWN, PrettyTable from pydantic import Field @@ -102,7 +102,7 @@ class Switch(NetworkNode, identifier="switch"): mac_address_table: Dict[str, SwitchPort] = {} "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." - config: "Switch.ConfigSchema" = Field(default_factory=lambda: Switch.ConfigSchema()) + config: "Switch.ConfigSchema" class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Switch nodes within PrimAITE.""" @@ -113,7 +113,7 @@ class Switch(NetworkNode, identifier="switch"): def __init__(self, **kwargs): super().__init__(**kwargs) - for i in range(1, self.config.num_ports + 1): + for i in range(1, kwargs["config"].num_ports + 1): self.connect_nic(SwitchPort()) def _install_system_software(self): diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 34c893eb..9a30f3e3 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -294,7 +294,7 @@ class IOSoftware(Software): """ if self.software_manager and self.software_manager.node.operating_state != NodeOperatingState.ON: self.software_manager.node.sys_log.error( - f"{self.name} Error: {self.software_manager.node.hostname} is not powered on." + f"{self.name} Error: {self.software_manager.node.config.hostname} is not powered on." ) return False return True diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 97442903..bddea1a0 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -187,7 +187,7 @@ agents: num_files: 1 num_nics: 2 include_num_access: false - include_nmne: true + include_nmne: true monitored_traffic: icmp: - NONE diff --git a/tests/conftest.py b/tests/conftest.py index 6cbcfa84..08c16537 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -195,68 +195,91 @@ def example_network() -> Network: network = Network() # Router 1 + + router_1_cfg = {"hostname":"router_1", "type":"router"} + # router_1 = Router(hostname="router_1", start_up_duration=0) - router_1 = Router(hostname="router_1", start_up_duration=0) + router_1 = Router.from_config(config=router_1_cfg) router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") # Switch 1 - # switch_1_config = Switch.ConfigSchema() - switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0) + + switch_1_cfg = {"hostname": "switch_1", "type": "switch"} + + switch_1 = Switch.from_config(config=switch_1_cfg) + + # switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0) switch_1.power_on() network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[8]) router_1.enable_port(1) # Switch 2 - # switch_2_config = Switch.ConfigSchema() - switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) + switch_2_config = {"hostname": "switch_2", "type": "switch", "num_ports": 8} + # switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) + switch_2 = Switch.from_config(config=switch_2_config) switch_2.power_on() network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8]) router_1.enable_port(2) - # Client 1 - client_1 = Computer( - hostname="client_1", - ip_address="192.168.10.21", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1", - start_up_duration=0, - ) + # # Client 1 + + client_1_cfg = {"type": "computer", + "hostname": "client_1", + "ip_address": "192.168.10.21", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "start_up_duration": 0} + + client_1=Computer.from_config(config=client_1_cfg) + client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) - # Client 2 - client_2 = Computer( - hostname="client_2", - ip_address="192.168.10.22", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1", - start_up_duration=0, - ) + # # Client 2 + + client_2_cfg = {"type": "computer", + "hostname": "client_2", + "ip_address": "192.168.10.22", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "start_up_duration": 0, + } + + client_2 = Computer.from_config(config=client_2_cfg) + client_2.power_on() network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.network_interface[2]) - # Server 1 - server_1 = Server( - hostname="server_1", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + # # Server 1 + + server_1_cfg = {"type": "server", + "hostname": "server_1", + "ip_address":"192.168.1.10", + "subnet_mask":"255.255.255.0", + "default_gateway":"192.168.1.1", + "start_up_duration":0, + } + + server_1 = Server.from_config(config=server_1_cfg) + server_1.power_on() network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1]) - # DServer 2 - server_2 = Server( - hostname="server_2", - ip_address="192.168.1.14", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + # # DServer 2 + + server_2_cfg = {"type": "server", + "hostname": "server_2", + "ip_address":"192.168.1.14", + "subnet_mask":"255.255.255.0", + "default_gateway":"192.168.1.1", + "start_up_duration":0, + } + + server_2 = Server.from_config(config=server_2_cfg) + server_2.power_on() network.connect(endpoint_b=server_2.network_interface[1], endpoint_a=switch_1.network_interface[2]) @@ -264,6 +287,8 @@ def example_network() -> Network: assert all(link.is_up for link in network.links.values()) + client_1.software_manager.show() + return network diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index 16f4dee5..c9691fab 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -6,6 +6,7 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index 02cf005a..68964b90 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -36,7 +36,7 @@ def test_acl_observations(simulation): router.acl.add_rule(action=ACLAction.PERMIT, dst_port=PORT_LOOKUP["NTP"], src_port=PORT_LOOKUP["NTP"], position=1) acl_obs = ACLObservation( - where=["network", "nodes", router.hostname, "acl", "acl"], + where=["network", "nodes", router.config.hostname, "acl", "acl"], ip_list=[], port_list=[123, 80, 5432], protocol_list=["tcp", "udp", "icmp"], diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index 0268cb95..a56deb2b 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -24,7 +24,7 @@ def test_file_observation(simulation): file = pc.file_system.create_file(file_name="dog.png") dog_file_obs = FileObservation( - where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"], + where=["network", "nodes", pc.config.hostname, "file_system", "folders", "root", "files", "dog.png"], include_num_access=False, file_system_requires_scan=True, ) @@ -52,7 +52,7 @@ def test_folder_observation(simulation): file = pc.file_system.create_file(file_name="dog.png", folder_name="test_folder") root_folder_obs = FolderObservation( - where=["network", "nodes", pc.hostname, "file_system", "folders", "test_folder"], + where=["network", "nodes", pc.config.hostname, "file_system", "folders", "test_folder"], include_num_access=False, file_system_requires_scan=True, num_files=1, diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index fe0c3a57..5a0ebe8f 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -50,7 +50,7 @@ def test_wireless_router_from_config(): }, } - rt = Router.from_config(cfg=cfg) + rt = Router.from_config(config=cfg) assert rt.num_ports == 6 From 65355f83e85874dfa1f3ca941ecc90cf10254332 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 23 Jan 2025 09:52:14 +0000 Subject: [PATCH 151/224] #2887 - Commit before switching branch --- src/primaite/game/game.py | 2 - .../network/hardware/nodes/host/server.py | 2 +- .../hardware/nodes/network/firewall.py | 7 --- .../network/hardware/nodes/network/router.py | 47 ++----------------- 4 files changed, 5 insertions(+), 53 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 009a376f..6a902c40 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -266,8 +266,6 @@ class PrimaiteGame: for node_cfg in nodes_cfg: n_type = node_cfg["type"] - # node_config: dict = node_cfg["config"] - print(f"{n_type}:{node_cfg}") new_node = None if n_type in Node._registry: diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index 1b3f6c58..f1abefc2 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -51,4 +51,4 @@ class Printer(HostNode, identifier="printer"): class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Printer class.""" - hostname: ClassVar[str] = "printer" \ No newline at end of file + hostname: str = "printer" \ No newline at end of file diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 2ebfe44a..a30c49bd 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -572,13 +572,6 @@ class Firewall(Router, identifier="firewall"): @classmethod def from_config(cls, config: dict) -> "Firewall": """Create a firewall based on a config dict.""" - # firewall = Firewall( - # hostname=config["hostname"], - # operating_state=NodeOperatingState.ON - # if not (p := config.get("operating_state")) - # else NodeOperatingState[p.upper()], - # ) - firewall = Firewall(config = cls.ConfigSchema(**config)) if "ports" in config: diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index e475df66..4f9d9ca4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1212,11 +1212,11 @@ class Router(NetworkNode, identifier="router"): network_interface: Dict[int, RouterInterface] = {} "The Router Interfaces on the node by port id." - sys_log: SysLog = None + sys_log: SysLog - acl: AccessControlList = None + acl: AccessControlList - route_table: RouteTable = None + route_table: RouteTable config: "Router.ConfigSchema" @@ -1250,8 +1250,6 @@ class Router(NetworkNode, identifier="router"): self._set_default_acl() - - def _install_system_software(self): """ Installs essential system software and network services on the router. @@ -1403,7 +1401,7 @@ class Router(NetworkNode, identifier="router"): return # Check if it's permitted - permitted, rule = self.config.acl.is_permitted(frame) + permitted, rule = self.acl.is_permitted(frame) if not permitted: at_port = self._get_port_of_nic(from_network_interface) @@ -1576,43 +1574,6 @@ class Router(NetworkNode, identifier="router"): ) print(table) - def setup_router(self, cfg: dict) -> Router: - """TODO: This is the extra bit of Router's from_config metho. Needs sorting.""" - if "ports" in cfg: - for port_num, port_cfg in cfg["ports"].items(): - self.configure_port( - port=port_num, - ip_address=port_cfg["ip_address"], - subnet_mask=IPv4Address(port_cfg.get("subnet_mask", "255.255.255.0")), - ) - if "acl" in cfg: - for r_num, r_cfg in cfg["acl"].items(): - self.config.acl.add_rule( - action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else PORT_LOOKUP[p], - protocol=None if not (p := r_cfg.get("protocol")) else PROTOCOL_LOOKUP[p], - src_ip_address=r_cfg.get("src_ip"), - src_wildcard_mask=r_cfg.get("src_wildcard_mask"), - dst_ip_address=r_cfg.get("dst_ip"), - dst_wildcard_mask=r_cfg.get("dst_wildcard_mask"), - position=r_num, - ) - if "routes" in cfg: - for route in cfg.get("routes"): - self.config.route_table.add_route( - address=IPv4Address(route.get("address")), - subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), - next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), - metric=float(route.get("metric", 0)), - ) - if "default_route" in cfg: - next_hop_ip_address = cfg["default_route"].get("next_hop_ip_address", None) - if next_hop_ip_address: - self.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) - return self - - @classmethod def from_config(cls, config: dict, **kwargs) -> "Router": """Create a router based on a config dict. From 8eeba691a9755e1bc237f0687bba2e82061a8fdb Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 23 Jan 2025 15:16:49 +0000 Subject: [PATCH 152/224] #3075: Remove unnecessary print statement. --- src/primaite/game/agent/actions/node.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index fbab18f0..5e1b6725 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -36,7 +36,6 @@ class NodeAbstractAction(AbstractAction, identifier="node_abstract"): @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - print(config) return ["network", "node", config.node_name, config.verb] From b9d2cd25f3b992349bb156c646dbab7f66106388 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 23 Jan 2025 15:28:10 +0000 Subject: [PATCH 153/224] #2887 - Unit test fixes ahead of raising PR. --- src/primaite/game/agent/actions/node.py | 1 - src/primaite/simulator/network/creation.py | 23 +-- .../simulator/network/hardware/base.py | 2 +- .../network/hardware/nodes/network/switch.py | 2 +- src/primaite/simulator/network/networks.py | 135 ++++++++++-------- tests/conftest.py | 9 +- .../_network/_hardware/nodes/test_acl.py | 9 +- .../_network/_hardware/nodes/test_router.py | 2 +- .../_network/_hardware/nodes/test_switch.py | 6 +- .../test_network_interface_actions.py | 8 +- .../_network/_hardware/test_node_actions.py | 18 ++- .../_simulator/_network/test_container.py | 6 +- .../_simulator/_network/test_creation.py | 2 +- .../_red_applications/test_c2_suite.py | 24 ++-- .../_red_applications/test_dos_bot.py | 11 +- .../_applications/test_database_client.py | 8 +- .../_system/_services/test_database.py | 9 +- .../_system/_services/test_dns_client.py | 22 ++- .../_system/_services/test_dns_server.py | 22 +-- .../_system/_services/test_ftp_client.py | 17 +-- .../_system/_services/test_ftp_server.py | 14 +- .../_system/_services/test_web_server.py | 14 +- 22 files changed, 222 insertions(+), 142 deletions(-) diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index fbab18f0..5e1b6725 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -36,7 +36,6 @@ class NodeAbstractAction(AbstractAction, identifier="node_abstract"): @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - print(config) return ["network", "node", config.node_name, config.verb] diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index ebd17638..3221939b 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -153,7 +153,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Create a core switch if more than one edge switch is needed if num_of_switches > 1: - core_switch = Switch(hostname=f"switch_core_{config.lan_name}", start_up_duration=0) + core_switch = Switch.from_config(config = {"type":"switch","hostname":f"switch_core_{config.lan_name}", "start_up_duration": 0 }) core_switch.power_on() network.add_node(core_switch) core_switch_port = 1 @@ -164,7 +164,8 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Optionally include a router in the LAN if config.include_router: default_gateway = IPv4Address(f"192.168.{config.subnet_base}.1") - router = Router(hostname=f"router_{config.lan_name}", start_up_duration=0) + # router = Router(hostname=f"router_{config.lan_name}", start_up_duration=0) + router = Router.from_config(config={"hostname":f"router_{config.lan_name}", "type": "router", "start_up_duration": 0}) router.power_on() router.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 @@ -177,7 +178,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Initialise the first edge switch and connect to the router or core switch switch_port = 0 switch_n = 1 - switch = Switch(hostname=f"switch_edge_{switch_n}_{config.lan_name}", start_up_duration=0) + switch = Switch.from_config(config={"type": "switch","hostname":f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration":0}) switch.power_on() network.add_node(switch) if num_of_switches > 1: @@ -195,7 +196,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): if switch_port == effective_network_interface: switch_n += 1 switch_port = 0 - switch = Switch(hostname=f"switch_edge_{switch_n}_{config.lan_name}", start_up_duration=0) + switch = Switch.from_config(config={"type": "switch","hostname":f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration":0}) switch.power_on() network.add_node(switch) # Connect the new switch to the router or core switch @@ -212,13 +213,13 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): ) # Create and add a PC to the network - pc = Computer( - hostname=f"pc_{i}_{config.lan_name}", - ip_address=f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", - subnet_mask="255.255.255.0", - default_gateway=default_gateway, - start_up_duration=0, - ) + pc_cfg = {"type": "computer", + "hostname": f"pc_{i}_{config.lan_name}", + "ip_address": f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", + "default_gateway": "192.168.10.1", + "start_up_duration": 0, + } + pc = Computer.from_config(config = pc_cfg) pc.power_on() network.add_node(pc) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 872b1bdf..f68b627a 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1979,7 +1979,7 @@ class Node(SimComponent, ABC): else: if self.operating_state == NodeOperatingState.SHUTTING_DOWN: self.operating_state = NodeOperatingState.OFF - self.sys_log.info(f"{self.hostname}: Turned off") + self.sys_log.info(f"{self.config.hostname}: Turned off") self._shut_down_actions() # if resetting turn back on diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 2ca0cafd..e97c5321 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -129,7 +129,7 @@ class Switch(NetworkNode, identifier="switch"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Switch Ports" + table.title = f"{self.config.hostname} Switch Ports" for port_num, port in self.network_interface.items(): table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) print(table) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index c840748e..4d881343 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -128,32 +128,34 @@ def arcd_uc2_network() -> Network: network = Network() # Router 1 - router_1 = Router(hostname="router_1", num_ports=5, start_up_duration=0) + router_1 = Router.from_config(config={"type":"router", "hostname":"router_1", "num_ports":5, "start_up_duration":0}) router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") # Switch 1 - switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0) + switch_1 = Switch.from_config(config={"type":"switch", "hostname":"switch_1", "num_ports":8, "start_up_duration":0}) switch_1.power_on() network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[8]) router_1.enable_port(1) # Switch 2 - switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) + switch_2 = Switch.from_config(config={"type":"switch", "hostname":"switch_2", "num_ports":8, "start_up_duration":0}) switch_2.power_on() network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8]) router_1.enable_port(2) # Client 1 - client_1 = Computer( - hostname="client_1", - ip_address="192.168.10.21", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1", - dns_server=IPv4Address("192.168.1.10"), - start_up_duration=0, - ) + client_1_cfg = {"type": "computer", + "hostname": "client_1", + "ip_address": "192.168.10.21", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } + client_1: Computer = Computer.from_config(config = client_1_cfg) + client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) client_1.software_manager.install(DatabaseClient) @@ -172,14 +174,17 @@ def arcd_uc2_network() -> Network: ) # Client 2 - client_2 = Computer( - hostname="client_2", - ip_address="192.168.10.22", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1", - dns_server=IPv4Address("192.168.1.10"), - start_up_duration=0, - ) + + client_2_cfg = {"type": "computer", + "hostname": "client_2", + "ip_address": "192.168.10.22", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } + client_2: Computer = Computer.from_config(config = client_2_cfg) + client_2.power_on() client_2.software_manager.install(DatabaseClient) db_client_2 = client_2.software_manager.software.get("DatabaseClient") @@ -193,27 +198,34 @@ def arcd_uc2_network() -> Network: ) # Domain Controller - domain_controller = Server( - hostname="domain_controller", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + + domain_controller_cfg = {"type": "server", + "hostname": "domain_controller", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0 + } + + domain_controller = Server.from_config(config=domain_controller_cfg) domain_controller.power_on() domain_controller.software_manager.install(DNSServer) network.connect(endpoint_b=domain_controller.network_interface[1], endpoint_a=switch_1.network_interface[1]) # Database Server - database_server = Server( - hostname="database_server", - ip_address="192.168.1.14", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - dns_server=IPv4Address("192.168.1.10"), - start_up_duration=0, - ) + + database_server_cfg = {"type": "server", + "hostname": "database_server", + "ip_address": "192.168.1.14", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0 + } + + database_server = Server.from_config(config=database_server_cfg) + database_server.power_on() network.connect(endpoint_b=database_server.network_interface[1], endpoint_a=switch_1.network_interface[3]) @@ -223,14 +235,18 @@ def arcd_uc2_network() -> Network: database_service.configure_backup(backup_server=IPv4Address("192.168.1.16")) # Web Server - web_server = Server( - hostname="web_server", - ip_address="192.168.1.12", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - dns_server=IPv4Address("192.168.1.10"), - start_up_duration=0, - ) + + + web_server_cfg = {"type": "server", + "hostname": "web_server", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0 + } + web_server = Server.from_config(config=web_server_cfg) + web_server.power_on() web_server.software_manager.install(DatabaseClient) @@ -247,27 +263,30 @@ def arcd_uc2_network() -> Network: dns_server_service.dns_register("arcd.com", web_server.network_interface[1].ip_address) # Backup Server - backup_server = Server( - hostname="backup_server", - ip_address="192.168.1.16", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - dns_server=IPv4Address("192.168.1.10"), - start_up_duration=0, - ) + backup_server_cfg = {"type": "server", + "hostname": "backup_server", + "ip_address": "192.168.1.16", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0 + } + backup_server: Server = Server.from_config(config=backup_server_cfg) + backup_server.power_on() backup_server.software_manager.install(FTPServer) network.connect(endpoint_b=backup_server.network_interface[1], endpoint_a=switch_1.network_interface[4]) # Security Suite - security_suite = Server( - hostname="security_suite", - ip_address="192.168.1.110", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - dns_server=IPv4Address("192.168.1.10"), - start_up_duration=0, - ) + security_suite_cfg = {"type": "server", + "hostname": "backup_server", + "ip_address": "192.168.1.110", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0 + } + security_suite: Server = Server.from_config(config=security_suite_cfg) security_suite.power_on() network.connect(endpoint_b=security_suite.network_interface[1], endpoint_a=switch_1.network_interface[7]) security_suite.connect_nic(NIC(ip_address="192.168.10.110", subnet_mask="255.255.255.0")) diff --git a/tests/conftest.py b/tests/conftest.py index 287f216c..fc86bb4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,7 +119,14 @@ def application_class(): @pytest.fixture(scope="function") def file_system() -> FileSystem: - computer = Computer(hostname="fs_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + # computer = Computer(hostname="fs_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + computer_cfg = {"type": "computer", + "hostname": "fs_node", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + computer = Computer.from_config(config=computer_cfg) computer.power_on() return computer.file_system diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py index 79392d66..ee7eb08f 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_acl.py @@ -25,7 +25,8 @@ def router_with_acl_rules(): :return: A configured Router object with ACL rules. """ - router = Router("Router") + router_cfg = {"hostname": "router_1", "type": "router"} + router = Router.from_config(config=router_cfg) acl = router.acl # Add rules here as needed acl.add_rule( @@ -62,7 +63,8 @@ def router_with_wildcard_acl(): :return: A Router object with configured ACL rules, including rules with wildcard masking. """ - router = Router("Router") + router_cfg = {"hostname": "router_1", "type": "router"} + router = Router.from_config(config=router_cfg) acl = router.acl # Rule to permit traffic from a specific source IP and port to a specific destination IP and port acl.add_rule( @@ -243,7 +245,8 @@ def test_ip_traffic_from_specific_subnet(): - Traffic from outside the 192.168.1.0/24 subnet is denied. """ - router = Router("Router") + router_cfg = {"hostname": "router_1", "type": "router"} + router = Router.from_config(config=router_cfg) acl = router.acl # Add rules here as needed acl.add_rule( diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index 5a0ebe8f..e9d16533 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -52,7 +52,7 @@ def test_wireless_router_from_config(): rt = Router.from_config(config=cfg) - assert rt.num_ports == 6 + assert rt.config.num_ports == 6 assert rt.network_interface[1].ip_address == IPv4Address("192.168.1.1") assert rt.network_interface[1].subnet_mask == IPv4Address("255.255.255.0") diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index e6bff60e..dbc04f6d 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -7,7 +7,11 @@ from primaite.simulator.network.hardware.nodes.network.switch import Switch @pytest.fixture(scope="function") def switch() -> Switch: - switch: Switch = Switch(hostname="switch_1", num_ports=8, start_up_duration=0) + switch_cfg = {"type": "switch", + "hostname": "switch_1", + "num_ports": 8, + "start_up_duration": 0} + switch: Switch = Switch.from_config(config=switch_cfg) switch.power_on() switch.show() return switch diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py index 5cff4407..0e0023cd 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py @@ -7,7 +7,13 @@ from primaite.simulator.network.hardware.nodes.host.computer import Computer @pytest.fixture def node() -> Node: - return Computer(hostname="test", ip_address="192.168.1.2", subnet_mask="255.255.255.0") + computer_cfg = {"type": "computer", + "hostname": "test", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0"} + computer = Computer.from_config(config=computer_cfg) + + return computer def test_nic_enabled_validator(node): diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 672a4b5f..d077f46b 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -12,8 +12,16 @@ from tests.conftest import DummyApplication, DummyService @pytest.fixture def node() -> Node: - return Computer(hostname="test", ip_address="192.168.1.2", subnet_mask="255.255.255.0") + computer_cfg = {"type": "computer", + "hostname": "test", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "shut_down_duration": 3, + "operating_state": "OFF", + } + computer = Computer.from_config(config=computer_cfg) + return computer def test_node_startup(node): assert node.operating_state == NodeOperatingState.OFF @@ -166,7 +174,7 @@ def test_node_is_on_validator(node): """Test that the node is on validator.""" node.power_on() - for i in range(node.start_up_duration + 1): + for i in range(node.config.start_up_duration + 1): node.apply_timestep(i) validator = Node._NodeIsOnValidator(node=node) @@ -174,7 +182,7 @@ def test_node_is_on_validator(node): assert validator(request=[], context={}) node.power_off() - for i in range(node.shut_down_duration + 1): + for i in range(node.config.shut_down_duration + 1): node.apply_timestep(i) assert validator(request=[], context={}) is False @@ -184,7 +192,7 @@ def test_node_is_off_validator(node): """Test that the node is on validator.""" node.power_on() - for i in range(node.start_up_duration + 1): + for i in range(node.config.start_up_duration + 1): node.apply_timestep(i) validator = Node._NodeIsOffValidator(node=node) @@ -192,7 +200,7 @@ def test_node_is_off_validator(node): assert validator(request=[], context={}) is False node.power_off() - for i in range(node.shut_down_duration + 1): + for i in range(node.config.shut_down_duration + 1): node.apply_timestep(i) assert validator(request=[], context={}) diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index b1de710a..d175b865 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -61,12 +61,12 @@ def test_apply_timestep_to_nodes(network): client_1.power_off() assert client_1.operating_state is NodeOperatingState.SHUTTING_DOWN - for i in range(client_1.shut_down_duration + 1): + for i in range(client_1.config.shut_down_duration + 1): network.apply_timestep(timestep=i) assert client_1.operating_state is NodeOperatingState.OFF - network.apply_timestep(client_1.shut_down_duration + 2) + network.apply_timestep(client_1.config.shut_down_duration + 2) assert client_1.operating_state is NodeOperatingState.OFF @@ -74,7 +74,7 @@ def test_removing_node_that_does_not_exist(network): """Node that does not exist on network should not affect existing nodes.""" assert len(network.nodes) is 7 - network.remove_node(Computer(hostname="new_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0")) + network.remove_node(Computer.from_config(config = {"type":"computer","hostname":"new_node", "ip_address":"192.168.1.2", "subnet_mask":"255.255.255.0"})) assert len(network.nodes) is 7 diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py index 9885df67..29331d08 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -19,7 +19,7 @@ def _assert_valid_creation(net: Network, lan_name, subnet_base, pcs_ip_block_sta num_routers = 1 if include_router else 0 total_nodes = num_pcs + num_switches + num_routers - assert all((n.hostname.endswith(lan_name) for n in net.nodes.values())) + assert all((n.config.hostname.endswith(lan_name) for n in net.nodes.values())) assert len(net.computer_nodes) == num_pcs assert len(net.switch_nodes) == num_switches assert len(net.router_nodes) == num_routers diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 17f8445a..5d8bea80 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -16,19 +16,25 @@ def basic_c2_network() -> Network: network = Network() # Creating two generic nodes for the C2 Server and the C2 Beacon. + computer_a_cfg = {"type": "computer", + "hostname": "computer_a", + "ip_address": "192.168.0.1", + "subnet_mask": "255.255.255.252", + "start_up_duration": 0} + computer_a = Computer.from_config(config = computer_a_cfg) - computer_a = Computer( - hostname="computer_a", - ip_address="192.168.0.1", - subnet_mask="255.255.255.252", - start_up_duration=0, - ) computer_a.power_on() computer_a.software_manager.install(software_class=C2Server) - computer_b = Computer( - hostname="computer_b", ip_address="192.168.0.2", subnet_mask="255.255.255.252", start_up_duration=0 - ) + + computer_b_cfg = {"type": "computer", + "hostname": "computer_b", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.252", + "start_up_duration": 0, + } + + computer_b = Computer.from_config(config=computer_b_cfg) computer_b.power_on() computer_b.software_manager.install(software_class=C2Beacon) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index 9d8b7809..02b13724 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -12,9 +12,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def dos_bot() -> DoSBot: - computer = Computer( - hostname="compromised_pc", ip_address="192.168.0.1", subnet_mask="255.255.255.0", start_up_duration=0 - ) + computer_cfg = {"type":"computer", + "hostname": "compromised_pc", + "ip_address": "192.168.0.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + computer: Computer = Computer.from_config(config=computer_cfg) + computer.power_on() computer.software_manager.install(DoSBot) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index 5917fde7..6e32b646 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -17,14 +17,14 @@ from primaite.simulator.system.services.database.database_service import Databas def database_client_on_computer() -> Tuple[DatabaseClient, Computer]: network = Network() - db_server = Server(hostname="db_server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", start_up_duration=0) + db_server: Server = Server.from_config(config={"type": "server", "hostname":"db_server", "ip_address":"192.168.0.1", "subnet_mask":"255.255.255.0", "start_up_duration":0}) db_server.power_on() db_server.software_manager.install(DatabaseService) db_server.software_manager.software["DatabaseService"].start() - db_client = Computer( - hostname="db_client", ip_address="192.168.0.2", subnet_mask="255.255.255.0", start_up_duration=0 - ) + db_client: Computer = Computer.from_config(config = {"type":"computer", + "hostname":"db_client", "ip_address":"192.168.0.2", "subnet_mask":"255.255.255.0", "start_up_duration":0 + }) db_client.power_on() db_client.software_manager.install(DatabaseClient) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index ef165c8f..b7ba2d04 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -8,7 +8,14 @@ from primaite.simulator.system.services.database.database_service import Databas @pytest.fixture(scope="function") def database_server() -> Node: - node = Computer(hostname="db_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + node_cfg = {"type": "computer", + "hostname": "db_node", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + + node = Computer.from_config(config=node_cfg) node.power_on() node.software_manager.install(DatabaseService) node.software_manager.software.get("DatabaseService").start() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 1bc5b353..3f621331 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -14,13 +14,21 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def dns_client() -> Computer: - node = Computer( - hostname="dns_client", - ip_address="192.168.1.11", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - dns_server=IPv4Address("192.168.1.10"), - ) + + node_cfg = {"type": "computer", + "hostname": "dns_client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10")} + node = Computer.from_config(config=node_cfg) + # node = Computer( + # hostname="dns_client", + # ip_address="192.168.1.11", + # subnet_mask="255.255.255.0", + # default_gateway="192.168.1.1", + # dns_server=IPv4Address("192.168.1.10"), + # ) return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 3bc2b1a4..8df96099 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -16,13 +16,13 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def dns_server() -> Node: - node = Server( - hostname="dns_server", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + node_cfg = {"type": "server", + "hostname": "dns_server", + "ip_address": "192.168.1.10", + "subnet_mask":"255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration":0} + node = Server.from_config(config=node_cfg) node.power_on() node.software_manager.install(software_class=DNSServer) return node @@ -55,7 +55,13 @@ def test_dns_server_receive(dns_server): # register the web server in the domain controller dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) - client = Computer(hostname="client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", start_up_duration=0) + client_cfg = {"type": "computer", + "hostname": "client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + client = Computer.from_config(config=client_cfg) client.power_on() client.dns_server = IPv4Address("192.168.1.10") network = Network() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index d3e679db..c6e10b7d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -16,13 +16,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def ftp_client() -> Node: - node = Computer( - hostname="ftp_client", - ip_address="192.168.1.11", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + node_cfg = {"type": "computer", + "hostname": "ftp_client", + "ip_address":"192.168.1.11", + "subnet_mask":"255.255.255.0", + "default_gateway":"192.168.1.1", + "start_up_duration": 0, + } + node = Computer.from_config(config=node_cfg) node.power_on() return node @@ -94,7 +95,7 @@ def test_offline_ftp_client_receives_request(ftp_client): ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") ftp_client.power_off() - for i in range(ftp_client.shut_down_duration + 1): + for i in range(ftp_client.config.shut_down_duration + 1): ftp_client.apply_timestep(timestep=i) assert ftp_client.operating_state is NodeOperatingState.OFF diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 37c3d019..5cae88e0 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -14,13 +14,13 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def ftp_server() -> Node: - node = Server( - hostname="ftp_server", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + node_cfg = {"type": "server", + "hostname":"ftp_server", + "ip_address":"192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration":0} + node = Server.from_config(config=node_cfg) node.power_on() node.software_manager.install(software_class=FTPServer) return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 606a195c..f0901b70 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -16,13 +16,13 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def web_server() -> Server: - node = Server( - hostname="web_server", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + node_cfg = {"type": "server", + "hostname":"web_server", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway":"192.168.1.1", + "start_up_duration":0 } + node = Server.from_config(config=node_cfg) node.power_on() node.software_manager.install(WebServer) node.software_manager.software.get("WebServer").start() From 30c177c2722ea43a80dbf918d2839b883f57a63c Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 23 Jan 2025 17:07:15 +0000 Subject: [PATCH 154/224] #2887 - Additional test failure fixes --- .../simulator/network/hardware/base.py | 2 +- .../hardware/nodes/network/wireless_router.py | 8 +-- tests/conftest.py | 57 ++++++++++--------- .../observations/test_firewall_observation.py | 5 +- .../observations/test_link_observations.py | 10 ++-- .../observations/test_nic_observations.py | 6 +- .../observations/test_node_observations.py | 4 +- .../observations/test_router_observation.py | 4 +- .../test_software_observations.py | 4 +- .../game_layer/test_action_mask.py | 1 + .../game_layer/test_actions.py | 1 + .../network/test_airspace_config.py | 1 + .../network/test_broadcast.py | 47 ++++++++------- .../_system/_applications/test_web_browser.py | 22 +++---- 14 files changed, 88 insertions(+), 84 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index f68b627a..d462f75c 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1760,7 +1760,7 @@ class Node(SimComponent, ABC): self.software_manager.install(application_class) application_instance = self.software_manager.software.get(application_name) self.applications[application_instance.uuid] = application_instance - _LOGGER.debug(f"Added application {application_instance.name} to node {self.hostname}") + _LOGGER.debug(f"Added application {application_instance.name} to node {self.config.hostname}") self._application_request_manager.add_request( application_name, RequestType(func=application_instance._request_manager) ) diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 2c4b5976..75e4d5ea 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -126,16 +126,16 @@ class WirelessRouter(Router, identifier="wireless_router"): config: "WirelessRouter.ConfigSchema" = Field(default_factory=lambda: WirelessRouter.ConfigSchema()) - class ConfigSchema(Router.ConfigSChema): + class ConfigSchema(Router.ConfigSchema): """Configuration Schema for WirelessRouter nodes within PrimAITE.""" hostname: str = "WirelessRouter" - def __init__(self, hostname: str, airspace: AirSpace, **kwargs): - super().__init__(hostname=hostname, num_ports=0, airspace=airspace, **kwargs) + def __init__(self, **kwargs): + super().__init__(hostname=kwargs["config"].hostname, num_ports=0, airspace=kwargs["config"].airspace, **kwargs) self.connect_nic( - WirelessAccessPoint(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", airspace=airspace) + WirelessAccessPoint(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", airspace=kwargs["config"].airspace) ) self.connect_nic(RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0")) diff --git a/tests/conftest.py b/tests/conftest.py index fc86bb4d..1bdc217c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -348,29 +348,29 @@ def install_stuff_to_sim(sim: Simulation): # 1: Set up network hardware # 1.1: Configure the router - router = Router(hostname="router", num_ports=3, start_up_duration=0) + router = Router.from_config(config={"type":"router", "hostname":"router", "num_ports":3, "start_up_duration":0}) router.power_on() router.configure_port(port=1, ip_address="10.0.1.1", subnet_mask="255.255.255.0") router.configure_port(port=2, ip_address="10.0.2.1", subnet_mask="255.255.255.0") # 1.2: Create and connect switches - switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) + switch_1 = Switch.from_config(config={"type":"switch", "hostname":"switch_1", "num_ports":6, "start_up_duration":0}) switch_1.power_on() network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) router.enable_port(1) - switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) + switch_2 = Switch.from_config(config={"type":"switch", "hostname":"switch_2", "num_ports":6, "start_up_duration":0}) switch_2.power_on() network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) router.enable_port(2) # 1.3: Create and connect computer - client_1 = Computer( - hostname="client_1", - ip_address="10.0.1.2", - subnet_mask="255.255.255.0", - default_gateway="10.0.1.1", - start_up_duration=0, - ) + client_1_cfg = {"type": "computer", + "hostname": "client_1", + "ip_address":"10.0.1.2", + "subnet_mask":"255.255.255.0", + "default_gateway": "10.0.1.1", + "start_up_duration":0} + client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() network.connect( endpoint_a=client_1.network_interface[1], @@ -378,23 +378,26 @@ def install_stuff_to_sim(sim: Simulation): ) # 1.4: Create and connect servers - server_1 = Server( - hostname="server_1", - ip_address="10.0.2.2", - subnet_mask="255.255.255.0", - default_gateway="10.0.2.1", - start_up_duration=0, - ) + server_1_cfg = {"type": "server", + "hostname":"server_1", + "ip_address": "10.0.2.2", + "subnet_mask":"255.255.255.0", + "default_gateway":"10.0.2.1", + "start_up_duration": 0} + + + server_1: Server = Server.from_config(config=server_1_cfg) server_1.power_on() network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_2.network_interface[1]) + server_2_cfg = {"type": "server", + "hostname":"server_2", + "ip_address": "10.0.2.3", + "subnet_mask":"255.255.255.0", + "default_gateway":"10.0.2.1", + "start_up_duration": 0} - server_2 = Server( - hostname="server_2", - ip_address="10.0.2.3", - subnet_mask="255.255.255.0", - default_gateway="10.0.2.1", - start_up_duration=0, - ) + + server_2: Server = Server.from_config(config=server_2_cfg) server_2.power_on() network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2]) @@ -442,18 +445,18 @@ def install_stuff_to_sim(sim: Simulation): assert acl_rule is None # 5.2: Assert the client is correctly configured - c: Computer = [node for node in sim.network.nodes.values() if node.hostname == "client_1"][0] + c: Computer = [node for node in sim.network.nodes.values() if node.config.hostname == "client_1"][0] assert c.software_manager.software.get("WebBrowser") is not None assert c.software_manager.software.get("DNSClient") is not None assert str(c.network_interface[1].ip_address) == "10.0.1.2" # 5.3: Assert that server_1 is correctly configured - s1: Server = [node for node in sim.network.nodes.values() if node.hostname == "server_1"][0] + s1: Server = [node for node in sim.network.nodes.values() if node.config.hostname == "server_1"][0] assert str(s1.network_interface[1].ip_address) == "10.0.2.2" assert s1.software_manager.software.get("DNSServer") is not None # 5.4: Assert that server_2 is correctly configured - s2: Server = [node for node in sim.network.nodes.values() if node.hostname == "server_2"][0] + s2: Server = [node for node in sim.network.nodes.values() if node.config.hostname == "server_2"][0] assert str(s2.network_interface[1].ip_address) == "10.0.2.3" assert s2.software_manager.software.get("WebServer") is not None diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 97608132..6b0d4359 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -25,7 +25,8 @@ def check_default_rules(acl_obs): def test_firewall_observation(): """Test adding/removing acl rules and enabling/disabling ports.""" net = Network() - firewall = Firewall(hostname="firewall", operating_state=NodeOperatingState.ON) + firewall_cfg = {"type": "firewall", "hostname": "firewall", "opertating_state": NodeOperatingState.ON} + firewall = Firewall.from_config(config=firewall_cfg) firewall_observation = FirewallObservation( where=[], num_rules=7, @@ -116,7 +117,7 @@ def test_firewall_observation(): assert all(observation["PORTS"][i]["operating_status"] == 2 for i in range(1, 4)) # connect a switch to the firewall and check that only the correct port is updated - switch = Switch(hostname="switch", num_ports=1, operating_state=NodeOperatingState.ON) + switch: Switch = Switch.from_config(config={"type": "switch", "hostname":"switch", "num_ports":1, "operating_state":NodeOperatingState.ON}) link = net.connect(firewall.network_interface[1], switch.network_interface[1]) assert firewall.network_interface[1].enabled observation = firewall_observation.observe(firewall.describe_state()) diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py index 630e29ea..b5cd6134 100644 --- a/tests/integration_tests/game_layer/observations/test_link_observations.py +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -56,12 +56,12 @@ def test_link_observation(): """Check the shape and contents of the link observation.""" net = Network() sim = Simulation(network=net) - switch = Switch(hostname="switch", num_ports=5, operating_state=NodeOperatingState.ON) - computer_1 = Computer( - hostname="computer_1", ip_address="10.0.0.1", subnet_mask="255.255.255.0", start_up_duration=0 + switch: Switch = Switch.from_config(config={"type":"switch", "hostname":"switch", "num_ports":5, "operating_state":NodeOperatingState.ON}) + computer_1: Computer = Computer.from_config(config={"type": "computer", + "hostname":"computer_1", "ip_address":"10.0.0.1", "subnet_mask":"255.255.255.0", "start_up_duration":0} ) - computer_2 = Computer( - hostname="computer_2", ip_address="10.0.0.2", subnet_mask="255.255.255.0", start_up_duration=0 + computer_2: Computer = Computer.from_config(config={"type":"computer", + "hostname":"computer_2", "ip_address":"10.0.0.2", "subnet_mask":"255.255.255.0", "start_up_duration":0} ) computer_1.power_on() computer_2.power_on() diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index bd9417ba..2a311853 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -75,7 +75,7 @@ def test_nic(simulation): nic: NIC = pc.network_interface[1] - nic_obs = NICObservation(where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=True) + nic_obs = NICObservation(where=["network", "nodes", pc.config.hostname, "NICs", 1], include_nmne=True) # Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs nmne_config = { @@ -108,7 +108,7 @@ def test_nic_categories(simulation): """Test the NIC observation nmne count categories.""" pc: Computer = simulation.network.get_node_by_hostname("client_1") - nic_obs = NICObservation(where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=True) + nic_obs = NICObservation(where=["network", "nodes", pc.config.hostname, "NICs", 1], include_nmne=True) assert nic_obs.high_nmne_threshold == 10 # default assert nic_obs.med_nmne_threshold == 5 # default @@ -163,7 +163,7 @@ def test_nic_monitored_traffic(simulation): pc2: Computer = simulation.network.get_node_by_hostname("client_2") nic_obs = NICObservation( - where=["network", "nodes", pc.hostname, "NICs", 1], include_nmne=False, monitored_traffic=monitored_traffic + where=["network", "nodes", pc.config.hostname, "NICs", 1], include_nmne=False, monitored_traffic=monitored_traffic ) simulation.pre_timestep(0) # apply timestep to whole sim diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py index 63ca8f6b..09eb3fe4 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -25,7 +25,7 @@ def test_host_observation(simulation): pc: Computer = simulation.network.get_node_by_hostname("client_1") host_obs = HostObservation( - where=["network", "nodes", pc.hostname], + where=["network", "nodes", pc.config.hostname], num_applications=0, num_files=1, num_folders=1, @@ -56,7 +56,7 @@ def test_host_observation(simulation): observation_state = host_obs.observe(simulation.describe_state()) assert observation_state.get("operating_status") == 4 # shutting down - for i in range(pc.shut_down_duration + 1): + for i in range(pc.config.shut_down_duration + 1): pc.apply_timestep(i) observation_state = host_obs.observe(simulation.describe_state()) diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index f4bfb193..131af57f 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -16,7 +16,7 @@ from primaite.utils.validation.port import PORT_LOOKUP def test_router_observation(): """Test adding/removing acl rules and enabling/disabling ports.""" net = Network() - router = Router(hostname="router", num_ports=5, operating_state=NodeOperatingState.ON) + router = Router.from_config(config={"type": "router", "hostname":"router", "num_ports":5, "operating_state":NodeOperatingState.ON}) ports = [PortObservation(where=["NICs", i]) for i in range(1, 6)] acl = ACLObservation( @@ -89,7 +89,7 @@ def test_router_observation(): assert all(observed_output["PORTS"][i]["operating_status"] == 2 for i in range(1, 6)) # connect a switch to the router and check that only the correct port is updated - switch = Switch(hostname="switch", num_ports=1, operating_state=NodeOperatingState.ON) + switch: Switch = Switch.from_config(config={"type": "switch", "hostname":"switch", "num_ports":1, "operating_state":NodeOperatingState.ON}) link = net.connect(router.network_interface[1], switch.network_interface[1]) assert router.network_interface[1].enabled observed_output = router_observation.observe(router.describe_state()) diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py index 291ee395..7957625a 100644 --- a/tests/integration_tests/game_layer/observations/test_software_observations.py +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -29,7 +29,7 @@ def test_service_observation(simulation): ntp_server = pc.software_manager.software.get("NTPServer") assert ntp_server - service_obs = ServiceObservation(where=["network", "nodes", pc.hostname, "services", "NTPServer"]) + service_obs = ServiceObservation(where=["network", "nodes", pc.config.hostname, "services", "NTPServer"]) assert service_obs.space["operating_status"] == spaces.Discrete(7) assert service_obs.space["health_status"] == spaces.Discrete(5) @@ -54,7 +54,7 @@ def test_application_observation(simulation): web_browser: WebBrowser = pc.software_manager.software.get("WebBrowser") assert web_browser - app_obs = ApplicationObservation(where=["network", "nodes", pc.hostname, "applications", "WebBrowser"]) + app_obs = ApplicationObservation(where=["network", "nodes", pc.config.hostname, "applications", "WebBrowser"]) web_browser.close() observation_state = app_obs.observe(simulation.describe_state()) diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 75965f16..ebba1119 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -3,6 +3,7 @@ from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.system.services.service import ServiceOperatingState +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from tests.conftest import TEST_ASSETS_ROOT CFG_PATH = TEST_ASSETS_ROOT / "configs/test_primaite_session.yaml" diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 5a308cf8..9d9b528c 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -17,6 +17,7 @@ from typing import Tuple import pytest import yaml +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv diff --git a/tests/integration_tests/network/test_airspace_config.py b/tests/integration_tests/network/test_airspace_config.py index e8abc0f2..fd3f6f28 100644 --- a/tests/integration_tests/network/test_airspace_config.py +++ b/tests/integration_tests/network/test_airspace_config.py @@ -2,6 +2,7 @@ import yaml from primaite.game.game import PrimaiteGame +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from tests import TEST_ASSETS_ROOT diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index ed40334f..5c30d2ac 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -84,44 +84,47 @@ class BroadcastTestClient(Application, identifier="BroadcastTestClient"): def broadcast_network() -> Network: network = Network() - client_1 = Computer( - hostname="client_1", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + client_1_cfg = {"type": "computer", + "hostname": "client_1", + "ip_address":"192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration":0} + + client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() client_1.software_manager.install(BroadcastTestClient) application_1 = client_1.software_manager.software["BroadcastTestClient"] application_1.run() + client_2_cfg = {"type": "computer", + "hostname": "client_2", + "ip_address":"192.168.1.3", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration":0} - client_2 = Computer( - hostname="client_2", - ip_address="192.168.1.3", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + client_2: Computer = Computer.from_config(config=client_2_cfg) client_2.power_on() client_2.software_manager.install(BroadcastTestClient) application_2 = client_2.software_manager.software["BroadcastTestClient"] application_2.run() - server_1 = Server( - hostname="server_1", - ip_address="192.168.1.1", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + server_1_cfg = {"type": "server", + "hostname": "server_1", + "ip_address":"192.168.1.1", + "subnet_mask": "255.255.255.0", + "default_gateway":"192.168.1.1", + "start_up_duration": 0} + + server_1 :Server = Server.from_config(config=server_1_cfg) + server_1.power_on() server_1.software_manager.install(BroadcastTestService) service: BroadcastTestService = server_1.software_manager.software["BroadcastService"] service.start() - switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) + switch_1: Switch = Switch.from_config(config={"type": "switch", "hostname":"switch_1", "num_ports":6, "start_up_duration":0}) switch_1.power_on() network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.network_interface[1]) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index f78b3261..85cd369f 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -12,13 +12,10 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def web_browser() -> WebBrowser: - computer = Computer( - hostname="web_client", - ip_address="192.168.1.11", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + computer_cfg = {"type": "computer", "hostname": "web_client", "ip_address": "192.168.1.11", "subnet_mask": "255.255.255.0", "default_gateway": "192.168.1.1", "start_up_duration": 0} + + computer: Computer = Computer.from_config(config=computer_cfg) + computer.power_on() # Web Browser should be pre-installed in computer web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") @@ -28,13 +25,10 @@ def web_browser() -> WebBrowser: def test_create_web_client(): - computer = Computer( - hostname="web_client", - ip_address="192.168.1.11", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + computer_cfg = {"type": "computer", "hostname": "web_client", "ip_address": "192.168.1.11", "subnet_mask": "255.255.255.0", "default_gateway": "192.168.1.1", "start_up_duration": 0} + + computer: Computer = Computer.from_config(config=computer_cfg) + computer.power_on() # Web Browser should be pre-installed in computer web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") From a7395c466e6d27cb671d047fe4cdf6350af8c468 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 23 Jan 2025 17:42:59 +0000 Subject: [PATCH 155/224] #2887 - Final test changes before end of day --- .../test_action_integration.py | 10 ++-- .../test_c2_suite_integration.py | 52 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index 8b81b7d3..2d493045 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -12,12 +12,12 @@ def test_passing_actions_down(monkeypatch) -> None: sim = Simulation() - pc1 = Computer(hostname="PC-1", ip_address="10.10.1.1", subnet_mask="255.255.255.0") + pc1 = Computer.from_config(config={"type":"computer", "hostname":"PC-1", "ip_address":"10.10.1.1", "subnet_mask":"255.255.255.0"}) pc1.start_up_duration = 0 pc1.power_on() - pc2 = Computer(hostname="PC-2", ip_address="10.10.1.2", subnet_mask="255.255.255.0") - srv = Server(hostname="WEBSERVER", ip_address="10.10.1.100", subnet_mask="255.255.255.0") - s1 = Switch(hostname="switch1") + pc2 = Computer.from_config(config={"type":"computer", "hostname":"PC-2", "ip_address":"10.10.1.2", "subnet_mask":"255.255.255.0"}) + srv = Server.from_config(config={"type":"server", "hostname":"WEBSERVER", "ip_address":"10.10.1.100", "subnet_mask":"255.255.255.0"}) + s1 = Switch.from_config(config={"type":"switch", "hostname":"switch1"}) for n in [pc1, pc2, srv, s1]: sim.network.add_node(n) @@ -48,6 +48,6 @@ def test_passing_actions_down(monkeypatch) -> None: assert not action_invoked # call the patched method - sim.apply_request(["network", "node", pc1.hostname, "file_system", "folder", "downloads", "repair"]) + sim.apply_request(["network", "node", pc1.config.hostname, "file_system", "folder", "downloads", "repair"]) assert action_invoked diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 40226be6..86a68865 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -34,52 +34,54 @@ def basic_network() -> Network: # Creating two generic nodes for the C2 Server and the C2 Beacon. - node_a = Computer( - hostname="node_a", - ip_address="192.168.0.2", - subnet_mask="255.255.255.252", - default_gateway="192.168.0.1", - start_up_duration=0, - ) + node_a_cfg = {"type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.252", + "default_gateway": "192.168.0.1", + "start_up_duration": 0} + + node_a: Computer = Computer.from_config(config=node_a_cfg) node_a.power_on() node_a.software_manager.get_open_ports() node_a.software_manager.install(software_class=C2Server) - node_b = Computer( - hostname="node_b", - ip_address="192.168.255.2", - subnet_mask="255.255.255.248", - default_gateway="192.168.255.1", - start_up_duration=0, - ) - + node_b_cfg = {"type": "computer", + "hostname": "node_b", + "ip_address": "192.168.255.2", + "subnet_mask": "255.255.255.248", + "default_gateway": "192.168.255.1", + "start_up_duration": 0} + + node_b: Computer = Computer.from_config(config=node_b_cfg) node_b.power_on() node_b.software_manager.install(software_class=C2Beacon) # Creating a generic computer for testing remote terminal connections. - node_c = Computer( - hostname="node_c", - ip_address="192.168.255.3", - subnet_mask="255.255.255.248", - default_gateway="192.168.255.1", - start_up_duration=0, - ) + node_c_cfg = {"type": "computer", + "hostname": "node_c", + "ip_address": "192.168.255.3", + "subnet_mask": "255.255.255.248", + "default_gateway": "192.168.255.1", + "start_up_duration": 0} + + node_c: Computer = Computer.from_config(config=node_c_cfg) node_c.power_on() # Creating a router to sit between node 1 and node 2. - router = Router(hostname="router", num_ports=3, start_up_duration=0) + router = Router.from_config(config={"type":"router", "hostname":"router", "num_ports":3, "start_up_duration":0}) # Default allow all. router.acl.add_rule(action=ACLAction.PERMIT) router.power_on() # Creating switches for each client. - switch_1 = Switch(hostname="switch_1", num_ports=6, start_up_duration=0) + switch_1 = Switch.from_config(config={"type":"switch", "hostname":"switch_1", "num_ports":6, "start_up_duration":0}) switch_1.power_on() # Connecting the switches to the router. router.configure_port(port=1, ip_address="192.168.0.1", subnet_mask="255.255.255.252") network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) - switch_2 = Switch(hostname="switch_2", num_ports=6, start_up_duration=0) + switch_2 = Switch.from_config(config={"type":"switch", "hostname":"switch_2", "num_ports":6, "start_up_duration":0}) switch_2.power_on() network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) From 0570ab984d99b35dd6fa8ac4e4065bb6d39f503b Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 27 Jan 2025 16:35:40 +0000 Subject: [PATCH 156/224] #2887 - Node class changes to address some test failures. Addressed some inconsistencies around operating_state, amended instantiation of some Nodes in test environments --- .../source/how_to_guides/extensible_nodes.rst | 4 +- src/primaite/simulator/network/airspace.py | 2 +- src/primaite/simulator/network/container.py | 12 +- src/primaite/simulator/network/creation.py | 35 +++-- .../simulator/network/hardware/base.py | 19 +-- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/server.py | 6 +- .../hardware/nodes/network/firewall.py | 5 +- .../network/hardware/nodes/network/router.py | 12 +- .../network/hardware/nodes/network/switch.py | 10 +- .../hardware/nodes/network/wireless_router.py | 36 +++-- src/primaite/simulator/network/networks.py | 134 ++++++++-------- tests/conftest.py | 145 ++++++++++-------- .../test_action_integration.py | 14 +- .../nodes/network/test_router_config.py | 2 +- .../nodes/test_node_config.py | 2 + .../test_episode_scheduler.py | 1 + .../actions/test_node_request_permission.py | 6 +- .../observations/test_firewall_observation.py | 4 +- .../observations/test_link_observations.py | 24 ++- .../observations/test_nic_observations.py | 4 +- .../observations/test_router_observation.py | 8 +- .../game_layer/test_action_mask.py | 2 +- .../game_layer/test_actions.py | 2 +- .../game_layer/test_observations.py | 7 +- .../network/test_broadcast.py | 48 +++--- .../network/test_firewall.py | 4 +- .../test_c2_suite_integration.py | 58 ++++--- .../system/test_dns_client_server.py | 2 +- .../system/test_service_on_node.py | 34 ++-- .../system/test_web_client_server.py | 2 +- .../_network/_hardware/nodes/test_switch.py | 5 +- .../test_network_interface_actions.py | 5 +- .../_network/_hardware/test_node_actions.py | 13 +- .../_simulator/_network/test_container.py | 11 +- .../_red_applications/test_c2_suite.py | 28 ++-- .../_red_applications/test_dos_bot.py | 15 +- .../_applications/test_database_client.py | 22 ++- .../_system/_applications/test_web_browser.py | 22 ++- .../_system/_services/test_database.py | 13 +- .../_system/_services/test_dns_client.py | 24 ++- .../_system/_services/test_dns_server.py | 27 ++-- .../_system/_services/test_ftp_client.py | 15 +- .../_system/_services/test_ftp_server.py | 14 +- .../_system/_services/test_terminal.py | 65 +++++--- .../_system/_services/test_web_server.py | 14 +- 46 files changed, 548 insertions(+), 391 deletions(-) diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 21907767..f0b78b08 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -46,7 +46,7 @@ class Router(NetworkNode, identifier="router"): num_ports: int = 5 - hostname: ClassVar[str] = "Router" + hostname: str = "Router" ports: list = [] @@ -55,4 +55,4 @@ class Router(NetworkNode, identifier="router"): Changes to YAML file. ===================== -Nodes defined within configuration YAML files for use with PrimAITE 3.X should still be compatible following these changes. \ No newline at end of file +Nodes defined within configuration YAML files for use with PrimAITE 3.X should still be compatible following these changes. diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 1f6fe6b0..5549eb78 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -320,7 +320,7 @@ class WirelessNetworkInterface(NetworkInterface, ABC): self.enabled = True self._connected_node.sys_log.info(f"Network Interface {self} enabled") self.pcap = PacketCapture( - hostname=self._connected_node.hostname, port_num=self.port_num, port_name=self.port_name + hostname=self._connected_node.config.hostname, port_num=self.port_num, port_name=self.port_name ) self.airspace.add_wireless_interface(self) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index aac82633..982495a4 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -196,7 +196,13 @@ class Network(SimComponent): if port.ip_address != IPv4Address("127.0.0.1"): port_str = port.port_name if port.port_name else port.port_num table.add_row( - [node.config.hostname, port_str, port.ip_address, port.subnet_mask, node.default_gateway] + [ + node.config.hostname, + port_str, + port.ip_address, + port.subnet_mask, + node.default_gateway, + ] ) print(table) @@ -288,7 +294,9 @@ class Network(SimComponent): node.parent = self self._nx_graph.add_node(node.config.hostname) _LOGGER.debug(f"Added node {node.uuid} to Network {self.uuid}") - self._node_request_manager.add_request(name=node.config.hostname, request_type=RequestType(func=node._request_manager)) + self._node_request_manager.add_request( + name=node.config.hostname, request_type=RequestType(func=node._request_manager) + ) def get_node_by_hostname(self, hostname: str) -> Optional[Node]: """ diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 3221939b..2a981d59 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -153,7 +153,9 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Create a core switch if more than one edge switch is needed if num_of_switches > 1: - core_switch = Switch.from_config(config = {"type":"switch","hostname":f"switch_core_{config.lan_name}", "start_up_duration": 0 }) + core_switch = Switch.from_config( + config={"type": "switch", "hostname": f"switch_core_{config.lan_name}", "start_up_duration": 0} + ) core_switch.power_on() network.add_node(core_switch) core_switch_port = 1 @@ -165,7 +167,9 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): if config.include_router: default_gateway = IPv4Address(f"192.168.{config.subnet_base}.1") # router = Router(hostname=f"router_{config.lan_name}", start_up_duration=0) - router = Router.from_config(config={"hostname":f"router_{config.lan_name}", "type": "router", "start_up_duration": 0}) + router = Router.from_config( + config={"hostname": f"router_{config.lan_name}", "type": "router", "start_up_duration": 0} + ) router.power_on() router.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22 @@ -178,7 +182,9 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Initialise the first edge switch and connect to the router or core switch switch_port = 0 switch_n = 1 - switch = Switch.from_config(config={"type": "switch","hostname":f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration":0}) + switch = Switch.from_config( + config={"type": "switch", "hostname": f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration": 0} + ) switch.power_on() network.add_node(switch) if num_of_switches > 1: @@ -196,7 +202,13 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): if switch_port == effective_network_interface: switch_n += 1 switch_port = 0 - switch = Switch.from_config(config={"type": "switch","hostname":f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration":0}) + switch = Switch.from_config( + config={ + "type": "switch", + "hostname": f"switch_edge_{switch_n}_{config.lan_name}", + "start_up_duration": 0, + } + ) switch.power_on() network.add_node(switch) # Connect the new switch to the router or core switch @@ -213,13 +225,14 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): ) # Create and add a PC to the network - pc_cfg = {"type": "computer", - "hostname": f"pc_{i}_{config.lan_name}", - "ip_address": f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", - "default_gateway": "192.168.10.1", - "start_up_duration": 0, - } - pc = Computer.from_config(config = pc_cfg) + pc_cfg = { + "type": "computer", + "hostname": f"pc_{i}_{config.lan_name}", + "ip_address": f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", + "default_gateway": "192.168.10.1", + "start_up_duration": 0, + } + pc = Computer.from_config(config=pc_cfg) pc.power_on() network.add_node(pc) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index d462f75c..de97f22b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1529,22 +1529,21 @@ class Node(SimComponent, ABC): _identifier: ClassVar[str] = "unknown" """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" - config: Node.ConfigSchema + config: Node.ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) """Configuration items within Node""" class ConfigSchema(BaseModel, ABC): """Configuration Schema for Node based classes.""" model_config = ConfigDict(arbitrary_types_allowed=True) - """Configure pydantic to allow arbitrary types and to let the instance have attributes not present in the model.""" - + """Configure pydantic to allow arbitrary types, let the instance have attributes not present in the model.""" hostname: str = "default" "The node hostname on the network." revealed_to_red: bool = False "Informs whether the node has been revealed to a red agent." - start_up_duration: int = 0 + start_up_duration: int = 3 "Time steps needed for the node to start up." start_up_countdown: int = 0 @@ -1617,12 +1616,10 @@ class Node(SimComponent, ABC): file_system=kwargs.get("file_system"), dns_server=kwargs.get("dns_server"), ) - super().__init__(**kwargs) self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager - self.power_on() @property def user_manager(self) -> Optional[UserManager]: @@ -1713,7 +1710,7 @@ class Node(SimComponent, ABC): @property def fail_message(self) -> str: """Message that is reported when a request is rejected by this validator.""" - return f"Cannot perform request on node '{self.node.hostname}' because it is not powered on." + return f"Cannot perform request on node '{self.node.config.hostname}' because it is not powered on." class _NodeIsOffValidator(RequestPermissionValidator): """ @@ -1732,7 +1729,7 @@ class Node(SimComponent, ABC): @property def fail_message(self) -> str: """Message that is reported when a request is rejected by this validator.""" - return f"Cannot perform request on node '{self.node.hostname}' because it is not turned off." + return f"Cannot perform request on node '{self.node.config.hostname}' because it is not turned off." def _init_request_manager(self) -> RequestManager: """ @@ -1900,7 +1897,7 @@ class Node(SimComponent, ABC): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Open Ports" + table.title = f"{self.config.hostname} Open Ports" for port in self.software_manager.get_open_ports(): if port > 0: table.add_row([port]) @@ -1927,7 +1924,7 @@ class Node(SimComponent, ABC): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Network Interface Cards" + table.title = f"{self.config.hostname} Network Interface Cards" for port, network_interface in self.network_interface.items(): ip_address = "" if hasattr(network_interface, "ip_address"): @@ -1967,7 +1964,7 @@ class Node(SimComponent, ABC): else: if self.operating_state == NodeOperatingState.BOOTING: self.operating_state = NodeOperatingState.ON - self.sys_log.info(f"{self.hostname}: Turned on") + self.sys_log.info(f"{self.config.hostname}: Turned on") for network_interface in self.network_interfaces.values(): network_interface.enable() diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 85857a44..1aebc3af 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -37,7 +37,7 @@ class Computer(HostNode, identifier="computer"): SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} - config: "Computer.ConfigSchema" = Field(default_factory=lambda: Computer.ConfigSchema()) + config: "Computer.ConfigSchema" = Field(default_factory=lambda: Computer.ConfigSchema()) class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Computer class.""" diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index f1abefc2..bdf4e8c2 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,6 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from typing import ClassVar + from pydantic import Field + from primaite.simulator.network.hardware.nodes.host.host_node import HostNode @@ -45,10 +46,9 @@ class Printer(HostNode, identifier="printer"): # TODO: Implement printer-specific behaviour - config: "Printer.ConfigSchema" = Field(default_factory=lambda: Printer.ConfigSchema()) class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Printer class.""" - hostname: str = "printer" \ No newline at end of file + hostname: str = "printer" diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index a30c49bd..6c582cd5 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -108,7 +108,6 @@ class Firewall(Router, identifier="firewall"): hostname: str = "firewall" num_ports: int = 0 - operating_state: NodeOperatingState = NodeOperatingState.ON def __init__(self, **kwargs): if not kwargs.get("sys_log"): @@ -242,7 +241,7 @@ class Firewall(Router, identifier="firewall"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Network Interfaces" + table.title = f"{self.config.hostname} Network Interfaces" ports = {"External": self.external_port, "Internal": self.internal_port, "DMZ": self.dmz_port} for port, network_interface in ports.items(): table.add_row( @@ -572,7 +571,7 @@ class Firewall(Router, identifier="firewall"): @classmethod def from_config(cls, config: dict) -> "Firewall": """Create a firewall based on a config dict.""" - firewall = Firewall(config = cls.ConfigSchema(**config)) + firewall = Firewall(config=cls.ConfigSchema(**config)) if "ports" in config: internal_port = config["ports"]["internal_port"] diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 4f9d9ca4..3ecb761b 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1218,7 +1218,7 @@ class Router(NetworkNode, identifier="router"): route_table: RouteTable - config: "Router.ConfigSchema" + config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSchema()) class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Router Objects.""" @@ -1230,12 +1230,13 @@ class Router(NetworkNode, identifier="router"): ports: Dict[Union[int, str], Dict] = {} - def __init__(self, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(kwargs["config"].hostname) if not kwargs.get("acl"): - kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname) + kwargs["acl"] = AccessControlList( + sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname + ) if not kwargs.get("route_table"): kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"]) super().__init__(**kwargs) @@ -1632,8 +1633,7 @@ class Router(NetworkNode, identifier="router"): :return: Configured router. :rtype: Router """ - router = Router(config=Router.ConfigSchema(**config) - ) + router = Router(config=Router.ConfigSchema(**config)) if "ports" in config: for port_num, port_cfg in config["ports"].items(): router.configure_port( @@ -1666,4 +1666,4 @@ class Router(NetworkNode, identifier="router"): next_hop_ip_address = config["default_route"].get("next_hop_ip_address", None) if next_hop_ip_address: router.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) - return router \ No newline at end of file + return router diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index e97c5321..3cb335f7 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from typing import ClassVar, Dict, Optional +from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable from pydantic import Field @@ -89,11 +89,7 @@ class SwitchPort(WiredNetworkInterface): class Switch(NetworkNode, identifier="switch"): - """ - A class representing a Layer 2 network switch. - - :ivar num_ports: The number of ports on the switch. Default is 24. - """ + """A class representing a Layer 2 network switch.""" network_interfaces: Dict[str, SwitchPort] = {} "The SwitchPorts on the Switch." @@ -102,7 +98,7 @@ class Switch(NetworkNode, identifier="switch"): mac_address_table: Dict[str, SwitchPort] = {} "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." - config: "Switch.ConfigSchema" + config: "Switch.ConfigSchema" = Field(default_factory=lambda: Switch.ConfigSchema()) class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Switch nodes within PrimAITE.""" diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 75e4d5ea..70e655ac 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -122,7 +122,6 @@ class WirelessRouter(Router, identifier="wireless_router"): network_interfaces: Dict[str, Union[RouterInterface, WirelessAccessPoint]] = {} network_interface: Dict[int, Union[RouterInterface, WirelessAccessPoint]] = {} - airspace: AirSpace config: "WirelessRouter.ConfigSchema" = Field(default_factory=lambda: WirelessRouter.ConfigSchema()) @@ -130,12 +129,15 @@ class WirelessRouter(Router, identifier="wireless_router"): """Configuration Schema for WirelessRouter nodes within PrimAITE.""" hostname: str = "WirelessRouter" + airspace: Optional[AirSpace] = None def __init__(self, **kwargs): - super().__init__(hostname=kwargs["config"].hostname, num_ports=0, airspace=kwargs["config"].airspace, **kwargs) + super().__init__(**kwargs) self.connect_nic( - WirelessAccessPoint(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", airspace=kwargs["config"].airspace) + WirelessAccessPoint( + ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", airspace=kwargs["config"].airspace + ) ) self.connect_nic(RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0")) @@ -233,7 +235,7 @@ class WirelessRouter(Router, identifier="wireless_router"): ) @classmethod - def from_config(cls, cfg: Dict, **kwargs) -> "WirelessRouter": + def from_config(cls, config: Dict, **kwargs) -> "WirelessRouter": """Generate the wireless router from config. Schema: @@ -261,21 +263,21 @@ class WirelessRouter(Router, identifier="wireless_router"): :rtype: WirelessRouter """ operating_state = ( - NodeOperatingState.ON if not (p := cfg.get("operating_state")) else NodeOperatingState[p.upper()] + NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] ) - router = cls(hostname=cfg["hostname"], operating_state=operating_state, airspace=kwargs["airspace"]) - if "router_interface" in cfg: - ip_address = cfg["router_interface"]["ip_address"] - subnet_mask = cfg["router_interface"]["subnet_mask"] + router = cls(config=cls.ConfigSchema(**config)) + if "router_interface" in config: + ip_address = config["router_interface"]["ip_address"] + subnet_mask = config["router_interface"]["subnet_mask"] router.configure_router_interface(ip_address=ip_address, subnet_mask=subnet_mask) - if "wireless_access_point" in cfg: - ip_address = cfg["wireless_access_point"]["ip_address"] - subnet_mask = cfg["wireless_access_point"]["subnet_mask"] - frequency = AirSpaceFrequency._registry[cfg["wireless_access_point"]["frequency"]] + if "wireless_access_point" in config: + ip_address = config["wireless_access_point"]["ip_address"] + subnet_mask = config["wireless_access_point"]["subnet_mask"] + frequency = AirSpaceFrequency._registry[config["wireless_access_point"]["frequency"]] router.configure_wireless_access_point(ip_address=ip_address, subnet_mask=subnet_mask, frequency=frequency) - if "acl" in cfg: - for r_num, r_cfg in cfg["acl"].items(): + if "acl" in config: + for r_num, r_cfg in config["acl"].items(): router.acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -287,8 +289,8 @@ class WirelessRouter(Router, identifier="wireless_router"): dst_wildcard_mask=r_cfg.get("dst_wildcard_mask"), position=r_num, ) - if "routes" in cfg: - for route in cfg.get("routes"): + if "routes" in config: + for route in config.get("routes"): router.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 4d881343..0579f137 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -128,33 +128,40 @@ def arcd_uc2_network() -> Network: network = Network() # Router 1 - router_1 = Router.from_config(config={"type":"router", "hostname":"router_1", "num_ports":5, "start_up_duration":0}) + router_1 = Router.from_config( + config={"type": "router", "hostname": "router_1", "num_ports": 5, "start_up_duration": 0} + ) router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") # Switch 1 - switch_1 = Switch.from_config(config={"type":"switch", "hostname":"switch_1", "num_ports":8, "start_up_duration":0}) + switch_1 = Switch.from_config( + config={"type": "switch", "hostname": "switch_1", "num_ports": 8, "start_up_duration": 0} + ) switch_1.power_on() network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[8]) router_1.enable_port(1) # Switch 2 - switch_2 = Switch.from_config(config={"type":"switch", "hostname":"switch_2", "num_ports":8, "start_up_duration":0}) + switch_2 = Switch.from_config( + config={"type": "switch", "hostname": "switch_2", "num_ports": 8, "start_up_duration": 0} + ) switch_2.power_on() network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8]) router_1.enable_port(2) # Client 1 - client_1_cfg = {"type": "computer", - "hostname": "client_1", - "ip_address": "192.168.10.21", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.10.1", - "dns_server": IPv4Address("192.168.1.10"), - "start_up_duration": 0, - } - client_1: Computer = Computer.from_config(config = client_1_cfg) + client_1_cfg = { + "type": "computer", + "hostname": "client_1", + "ip_address": "192.168.10.21", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } + client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) @@ -175,15 +182,16 @@ def arcd_uc2_network() -> Network: # Client 2 - client_2_cfg = {"type": "computer", - "hostname": "client_2", - "ip_address": "192.168.10.22", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.10.1", - "dns_server": IPv4Address("192.168.1.10"), - "start_up_duration": 0, - } - client_2: Computer = Computer.from_config(config = client_2_cfg) + client_2_cfg = { + "type": "computer", + "hostname": "client_2", + "ip_address": "192.168.10.22", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } + client_2: Computer = Computer.from_config(config=client_2_cfg) client_2.power_on() client_2.software_manager.install(DatabaseClient) @@ -199,13 +207,14 @@ def arcd_uc2_network() -> Network: # Domain Controller - domain_controller_cfg = {"type": "server", - "hostname": "domain_controller", - "ip_address": "192.168.1.10", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "start_up_duration": 0 - } + domain_controller_cfg = { + "type": "server", + "hostname": "domain_controller", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } domain_controller = Server.from_config(config=domain_controller_cfg) domain_controller.power_on() @@ -215,14 +224,15 @@ def arcd_uc2_network() -> Network: # Database Server - database_server_cfg = {"type": "server", - "hostname": "database_server", - "ip_address": "192.168.1.14", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "dns_server": IPv4Address("192.168.1.10"), - "start_up_duration": 0 - } + database_server_cfg = { + "type": "server", + "hostname": "database_server", + "ip_address": "192.168.1.14", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } database_server = Server.from_config(config=database_server_cfg) @@ -236,15 +246,15 @@ def arcd_uc2_network() -> Network: # Web Server - - web_server_cfg = {"type": "server", - "hostname": "web_server", - "ip_address": "192.168.1.11", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "dns_server": IPv4Address("192.168.1.10"), - "start_up_duration": 0 - } + web_server_cfg = { + "type": "server", + "hostname": "web_server", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } web_server = Server.from_config(config=web_server_cfg) web_server.power_on() @@ -263,14 +273,15 @@ def arcd_uc2_network() -> Network: dns_server_service.dns_register("arcd.com", web_server.network_interface[1].ip_address) # Backup Server - backup_server_cfg = {"type": "server", - "hostname": "backup_server", - "ip_address": "192.168.1.16", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "dns_server": IPv4Address("192.168.1.10"), - "start_up_duration": 0 - } + backup_server_cfg = { + "type": "server", + "hostname": "backup_server", + "ip_address": "192.168.1.16", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } backup_server: Server = Server.from_config(config=backup_server_cfg) backup_server.power_on() @@ -278,14 +289,15 @@ def arcd_uc2_network() -> Network: network.connect(endpoint_b=backup_server.network_interface[1], endpoint_a=switch_1.network_interface[4]) # Security Suite - security_suite_cfg = {"type": "server", - "hostname": "backup_server", - "ip_address": "192.168.1.110", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "dns_server": IPv4Address("192.168.1.10"), - "start_up_duration": 0 - } + security_suite_cfg = { + "type": "server", + "hostname": "backup_server", + "ip_address": "192.168.1.110", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + "start_up_duration": 0, + } security_suite: Server = Server.from_config(config=security_suite_cfg) security_suite.power_on() network.connect(endpoint_b=security_suite.network_interface[1], endpoint_a=switch_1.network_interface[7]) diff --git a/tests/conftest.py b/tests/conftest.py index 1bdc217c..6ac227ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,13 +119,13 @@ def application_class(): @pytest.fixture(scope="function") def file_system() -> FileSystem: - # computer = Computer(hostname="fs_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) - computer_cfg = {"type": "computer", - "hostname": "fs_node", - "ip_address": "192.168.1.2", - "subnet_mask": "255.255.255.0", - "start_up_duration": 0, - } + computer_cfg = { + "type": "computer", + "hostname": "fs_node", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } computer = Computer.from_config(config=computer_cfg) computer.power_on() return computer.file_system @@ -136,23 +136,29 @@ def client_server() -> Tuple[Computer, Server]: network = Network() # Create Computer - computer = Computer( - hostname="computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + computer_cfg = { + "type": "computer", + "hostname": "computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } + + computer: Computer = Computer.from_config(config=computer_cfg) computer.power_on() # Create Server - server = Server( - hostname="server", - ip_address="192.168.1.3", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + server_cfg = { + "type": "server", + "hostname": "server", + "ip_address": "192.168.1.3", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } + + server: Server = Server.from_config(config=server_cfg) server.power_on() # Connect Computer and Server @@ -169,26 +175,33 @@ def client_switch_server() -> Tuple[Computer, Switch, Server]: network = Network() # Create Computer - computer = Computer( - hostname="computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + computer_cfg = { + "type": "computer", + "hostname": "computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } + + computer: Computer = Computer.from_config(config=computer_cfg) computer.power_on() # Create Server - server = Server( - hostname="server", - ip_address="192.168.1.3", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - ) + server_cfg = { + "type": "server", + "hostname": "server", + "ip_address": "192.168.1.3", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } + + server: Server = Server.from_config(config=server_cfg) server.power_on() - switch = Switch(hostname="switch", start_up_duration=0) + # Create Switch + switch: Switch = Switch.from_config(config={"type": "switch", "hostname": "switch", "start_up_duration": 0}) switch.power_on() network.connect(endpoint_a=computer.network_interface[1], endpoint_b=switch.network_interface[1]) @@ -219,7 +232,7 @@ def example_network() -> Network: # Router 1 - router_1_cfg = {"hostname": "router_1", "type": "router"} + router_1_cfg = {"hostname": "router_1", "type": "router", "start_up_duration":0} # router_1 = Router(hostname="router_1", start_up_duration=0) router_1 = Router.from_config(config=router_1_cfg) @@ -229,7 +242,7 @@ def example_network() -> Network: # Switch 1 - switch_1_cfg = {"hostname": "switch_1", "type": "switch"} + switch_1_cfg = {"hostname": "switch_1", "type": "switch", "start_up_duration": 0} switch_1 = Switch.from_config(config=switch_1_cfg) @@ -240,7 +253,7 @@ def example_network() -> Network: router_1.enable_port(1) # Switch 2 - switch_2_config = {"hostname": "switch_2", "type": "switch", "num_ports": 8} + switch_2_config = {"hostname": "switch_2", "type": "switch", "num_ports": 8, "start_up_duration":0} # switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) switch_2 = Switch.from_config(config=switch_2_config) switch_2.power_on() @@ -348,28 +361,34 @@ def install_stuff_to_sim(sim: Simulation): # 1: Set up network hardware # 1.1: Configure the router - router = Router.from_config(config={"type":"router", "hostname":"router", "num_ports":3, "start_up_duration":0}) + router = Router.from_config(config={"type": "router", "hostname": "router", "num_ports": 3, "start_up_duration": 0}) router.power_on() router.configure_port(port=1, ip_address="10.0.1.1", subnet_mask="255.255.255.0") router.configure_port(port=2, ip_address="10.0.2.1", subnet_mask="255.255.255.0") # 1.2: Create and connect switches - switch_1 = Switch.from_config(config={"type":"switch", "hostname":"switch_1", "num_ports":6, "start_up_duration":0}) + switch_1 = Switch.from_config( + config={"type": "switch", "hostname": "switch_1", "num_ports": 6, "start_up_duration": 0} + ) switch_1.power_on() network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) router.enable_port(1) - switch_2 = Switch.from_config(config={"type":"switch", "hostname":"switch_2", "num_ports":6, "start_up_duration":0}) + switch_2 = Switch.from_config( + config={"type": "switch", "hostname": "switch_2", "num_ports": 6, "start_up_duration": 0} + ) switch_2.power_on() network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) router.enable_port(2) # 1.3: Create and connect computer - client_1_cfg = {"type": "computer", - "hostname": "client_1", - "ip_address":"10.0.1.2", - "subnet_mask":"255.255.255.0", - "default_gateway": "10.0.1.1", - "start_up_duration":0} + client_1_cfg = { + "type": "computer", + "hostname": "client_1", + "ip_address": "10.0.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "10.0.1.1", + "start_up_duration": 0, + } client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() network.connect( @@ -378,24 +397,26 @@ def install_stuff_to_sim(sim: Simulation): ) # 1.4: Create and connect servers - server_1_cfg = {"type": "server", - "hostname":"server_1", - "ip_address": "10.0.2.2", - "subnet_mask":"255.255.255.0", - "default_gateway":"10.0.2.1", - "start_up_duration": 0} - + server_1_cfg = { + "type": "server", + "hostname": "server_1", + "ip_address": "10.0.2.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "10.0.2.1", + "start_up_duration": 0, + } server_1: Server = Server.from_config(config=server_1_cfg) server_1.power_on() network.connect(endpoint_a=server_1.network_interface[1], endpoint_b=switch_2.network_interface[1]) - server_2_cfg = {"type": "server", - "hostname":"server_2", - "ip_address": "10.0.2.3", - "subnet_mask":"255.255.255.0", - "default_gateway":"10.0.2.1", - "start_up_duration": 0} - + server_2_cfg = { + "type": "server", + "hostname": "server_2", + "ip_address": "10.0.2.3", + "subnet_mask": "255.255.255.0", + "default_gateway": "10.0.2.1", + "start_up_duration": 0, + } server_2: Server = Server.from_config(config=server_2_cfg) server_2.power_on() diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index 2d493045..0fd0aa19 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -12,12 +12,18 @@ def test_passing_actions_down(monkeypatch) -> None: sim = Simulation() - pc1 = Computer.from_config(config={"type":"computer", "hostname":"PC-1", "ip_address":"10.10.1.1", "subnet_mask":"255.255.255.0"}) + pc1 = Computer.from_config( + config={"type": "computer", "hostname": "PC-1", "ip_address": "10.10.1.1", "subnet_mask": "255.255.255.0"} + ) pc1.start_up_duration = 0 pc1.power_on() - pc2 = Computer.from_config(config={"type":"computer", "hostname":"PC-2", "ip_address":"10.10.1.2", "subnet_mask":"255.255.255.0"}) - srv = Server.from_config(config={"type":"server", "hostname":"WEBSERVER", "ip_address":"10.10.1.100", "subnet_mask":"255.255.255.0"}) - s1 = Switch.from_config(config={"type":"switch", "hostname":"switch1"}) + pc2 = Computer.from_config( + config={"type": "computer", "hostname": "PC-2", "ip_address": "10.10.1.2", "subnet_mask": "255.255.255.0"} + ) + srv = Server.from_config( + config={"type": "server", "hostname": "WEBSERVER", "ip_address": "10.10.1.100", "subnet_mask": "255.255.255.0"} + ) + s1 = Switch.from_config(config={"type": "switch", "hostname": "switch1"}) for n in [pc1, pc2, srv, s1]: sim.network.add_node(n) diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index c9691fab..7ca3a6aa 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -5,8 +5,8 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.firewall import Firewall +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config diff --git a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py index 764a7aac..6ccbf4e1 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py @@ -3,6 +3,8 @@ from primaite.config.load import data_manipulation_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config diff --git a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py index c588829b..1352f894 100644 --- a/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py +++ b/tests/integration_tests/configuration_file_parsing/test_episode_scheduler.py @@ -4,6 +4,7 @@ import yaml from primaite.session.environment import PrimaiteGymEnv from primaite.session.ray_envs import PrimaiteRayEnv, PrimaiteRayMARLEnv +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from tests.conftest import TEST_ASSETS_ROOT folder_path = TEST_ASSETS_ROOT / "configs" / "scenario_with_placeholders" diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index 8a438673..32c9e8a5 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -35,7 +35,7 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.SHUTTING_DOWN - for i in range(client_1.shut_down_duration + 1): + for i in range(client_1.config.shut_down_duration + 1): action = ("do_nothing", {}) agent.store_action(action) game.step() @@ -49,7 +49,7 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.BOOTING - for i in range(client_1.start_up_duration + 1): + for i in range(client_1.config.start_up_duration + 1): action = ("do_nothing", {}) agent.store_action(action) game.step() @@ -79,7 +79,7 @@ def test_node_cannot_be_shut_down_if_node_is_already_off(game_and_agent_fixture: client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.power_off() - for i in range(client_1.shut_down_duration + 1): + for i in range(client_1.config.shut_down_duration + 1): action = ("do_nothing", {}) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 6b0d4359..17c7775f 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -117,7 +117,9 @@ def test_firewall_observation(): assert all(observation["PORTS"][i]["operating_status"] == 2 for i in range(1, 4)) # connect a switch to the firewall and check that only the correct port is updated - switch: Switch = Switch.from_config(config={"type": "switch", "hostname":"switch", "num_ports":1, "operating_state":NodeOperatingState.ON}) + switch: Switch = Switch.from_config( + config={"type": "switch", "hostname": "switch", "num_ports": 1, "operating_state": NodeOperatingState.ON} + ) link = net.connect(firewall.network_interface[1], switch.network_interface[1]) assert firewall.network_interface[1].enabled observation = firewall_observation.observe(firewall.describe_state()) diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py index b5cd6134..f95d35c2 100644 --- a/tests/integration_tests/game_layer/observations/test_link_observations.py +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -56,12 +56,26 @@ def test_link_observation(): """Check the shape and contents of the link observation.""" net = Network() sim = Simulation(network=net) - switch: Switch = Switch.from_config(config={"type":"switch", "hostname":"switch", "num_ports":5, "operating_state":NodeOperatingState.ON}) - computer_1: Computer = Computer.from_config(config={"type": "computer", - "hostname":"computer_1", "ip_address":"10.0.0.1", "subnet_mask":"255.255.255.0", "start_up_duration":0} + switch: Switch = Switch.from_config( + config={"type": "switch", "hostname": "switch", "num_ports": 5, "operating_state": NodeOperatingState.ON} ) - computer_2: Computer = Computer.from_config(config={"type":"computer", - "hostname":"computer_2", "ip_address":"10.0.0.2", "subnet_mask":"255.255.255.0", "start_up_duration":0} + computer_1: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer_1", + "ip_address": "10.0.0.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) + computer_2: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer_2", + "ip_address": "10.0.0.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } ) computer_1.power_on() computer_2.power_on() diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index 2a311853..5e1c0f81 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -163,7 +163,9 @@ def test_nic_monitored_traffic(simulation): pc2: Computer = simulation.network.get_node_by_hostname("client_2") nic_obs = NICObservation( - where=["network", "nodes", pc.config.hostname, "NICs", 1], include_nmne=False, monitored_traffic=monitored_traffic + where=["network", "nodes", pc.config.hostname, "NICs", 1], + include_nmne=False, + monitored_traffic=monitored_traffic, ) simulation.pre_timestep(0) # apply timestep to whole sim diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index 131af57f..8335867d 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -16,7 +16,9 @@ from primaite.utils.validation.port import PORT_LOOKUP def test_router_observation(): """Test adding/removing acl rules and enabling/disabling ports.""" net = Network() - router = Router.from_config(config={"type": "router", "hostname":"router", "num_ports":5, "operating_state":NodeOperatingState.ON}) + router = Router.from_config( + config={"type": "router", "hostname": "router", "num_ports": 5, "operating_state": NodeOperatingState.ON} + ) ports = [PortObservation(where=["NICs", i]) for i in range(1, 6)] acl = ACLObservation( @@ -89,7 +91,9 @@ def test_router_observation(): assert all(observed_output["PORTS"][i]["operating_status"] == 2 for i in range(1, 6)) # connect a switch to the router and check that only the correct port is updated - switch: Switch = Switch.from_config(config={"type": "switch", "hostname":"switch", "num_ports":1, "operating_state":NodeOperatingState.ON}) + switch: Switch = Switch.from_config( + config={"type": "switch", "hostname": "switch", "num_ports": 1, "operating_state": NodeOperatingState.ON} + ) link = net.connect(router.network_interface[1], switch.network_interface[1]) assert router.network_interface[1].enabled observed_output = router_observation.observe(router.describe_state()) diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index ebba1119..4ac7b9a6 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -2,8 +2,8 @@ from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter +from primaite.simulator.system.services.service import ServiceOperatingState from tests.conftest import TEST_ASSETS_ROOT CFG_PATH = TEST_ASSETS_ROOT / "configs/test_primaite_session.yaml" diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 9d9b528c..cd230546 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -17,11 +17,11 @@ from typing import Tuple import pytest import yaml -from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index 5afad296..090725b5 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -8,14 +8,17 @@ from primaite.simulator.sim_container import Simulation def test_file_observation(): sim = Simulation() - pc = Computer(hostname="beep", ip_address="123.123.123.123", subnet_mask="255.255.255.0") + pc: Computer = Computer.from_config(config={"type":"computer", + "hostname":"beep", + "ip_address":"123.123.123.123", + "subnet_mask":"255.255.255.0"}) sim.network.add_node(pc) f = pc.file_system.create_file(file_name="dog.png") state = sim.describe_state() dog_file_obs = FileObservation( - where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"], + where=["network", "nodes", pc.config.hostname, "file_system", "folders", "root", "files", "dog.png"], include_num_access=False, file_system_requires_scan=False, ) diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index 5c30d2ac..5469e803 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -84,24 +84,28 @@ class BroadcastTestClient(Application, identifier="BroadcastTestClient"): def broadcast_network() -> Network: network = Network() - client_1_cfg = {"type": "computer", - "hostname": "client_1", - "ip_address":"192.168.1.2", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "start_up_duration":0} + client_1_cfg = { + "type": "computer", + "hostname": "client_1", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() client_1.software_manager.install(BroadcastTestClient) application_1 = client_1.software_manager.software["BroadcastTestClient"] application_1.run() - client_2_cfg = {"type": "computer", - "hostname": "client_2", - "ip_address":"192.168.1.3", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "start_up_duration":0} + client_2_cfg = { + "type": "computer", + "hostname": "client_2", + "ip_address": "192.168.1.3", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } client_2: Computer = Computer.from_config(config=client_2_cfg) client_2.power_on() @@ -109,14 +113,16 @@ def broadcast_network() -> Network: application_2 = client_2.software_manager.software["BroadcastTestClient"] application_2.run() - server_1_cfg = {"type": "server", - "hostname": "server_1", - "ip_address":"192.168.1.1", - "subnet_mask": "255.255.255.0", - "default_gateway":"192.168.1.1", - "start_up_duration": 0} + server_1_cfg = { + "type": "server", + "hostname": "server_1", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } - server_1 :Server = Server.from_config(config=server_1_cfg) + server_1: Server = Server.from_config(config=server_1_cfg) server_1.power_on() @@ -124,7 +130,9 @@ def broadcast_network() -> Network: service: BroadcastTestService = server_1.software_manager.software["BroadcastService"] service.start() - switch_1: Switch = Switch.from_config(config={"type": "switch", "hostname":"switch_1", "num_ports":6, "start_up_duration":0}) + switch_1: Switch = Switch.from_config( + config={"type": "switch", "hostname": "switch_1", "num_ports": 6, "start_up_duration": 0} + ) switch_1.power_on() network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.network_interface[1]) diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 24fbfd05..69f3e5ab 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -41,7 +41,9 @@ def dmz_external_internal_network() -> Network: """ network = Network() - firewall_node: Firewall = Firewall(hostname="firewall_1", start_up_duration=0) + firewall_node: Firewall = Firewall.from_config( + config={"type": "firewall", "hostname": "firewall_1", "start_up_duration": 0} + ) firewall_node.power_on() # configure firewall ports firewall_node.configure_external_port( diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 86a68865..1cc4e8e2 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -34,54 +34,64 @@ def basic_network() -> Network: # Creating two generic nodes for the C2 Server and the C2 Beacon. - node_a_cfg = {"type": "computer", - "hostname": "node_a", - "ip_address": "192.168.0.2", - "subnet_mask": "255.255.255.252", - "default_gateway": "192.168.0.1", - "start_up_duration": 0} - + node_a_cfg = { + "type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.252", + "default_gateway": "192.168.0.1", + "start_up_duration": 0, + } + node_a: Computer = Computer.from_config(config=node_a_cfg) node_a.power_on() node_a.software_manager.get_open_ports() node_a.software_manager.install(software_class=C2Server) - node_b_cfg = {"type": "computer", - "hostname": "node_b", - "ip_address": "192.168.255.2", - "subnet_mask": "255.255.255.248", - "default_gateway": "192.168.255.1", - "start_up_duration": 0} - + node_b_cfg = { + "type": "computer", + "hostname": "node_b", + "ip_address": "192.168.255.2", + "subnet_mask": "255.255.255.248", + "default_gateway": "192.168.255.1", + "start_up_duration": 0, + } + node_b: Computer = Computer.from_config(config=node_b_cfg) node_b.power_on() node_b.software_manager.install(software_class=C2Beacon) # Creating a generic computer for testing remote terminal connections. - node_c_cfg = {"type": "computer", - "hostname": "node_c", - "ip_address": "192.168.255.3", - "subnet_mask": "255.255.255.248", - "default_gateway": "192.168.255.1", - "start_up_duration": 0} - + node_c_cfg = { + "type": "computer", + "hostname": "node_c", + "ip_address": "192.168.255.3", + "subnet_mask": "255.255.255.248", + "default_gateway": "192.168.255.1", + "start_up_duration": 0, + } + node_c: Computer = Computer.from_config(config=node_c_cfg) node_c.power_on() # Creating a router to sit between node 1 and node 2. - router = Router.from_config(config={"type":"router", "hostname":"router", "num_ports":3, "start_up_duration":0}) + router = Router.from_config(config={"type": "router", "hostname": "router", "num_ports": 3, "start_up_duration": 0}) # Default allow all. router.acl.add_rule(action=ACLAction.PERMIT) router.power_on() # Creating switches for each client. - switch_1 = Switch.from_config(config={"type":"switch", "hostname":"switch_1", "num_ports":6, "start_up_duration":0}) + switch_1 = Switch.from_config( + config={"type": "switch", "hostname": "switch_1", "num_ports": 6, "start_up_duration": 0} + ) switch_1.power_on() # Connecting the switches to the router. router.configure_port(port=1, ip_address="192.168.0.1", subnet_mask="255.255.255.252") network.connect(endpoint_a=router.network_interface[1], endpoint_b=switch_1.network_interface[6]) - switch_2 = Switch.from_config(config={"type":"switch", "hostname":"switch_2", "num_ports":6, "start_up_duration":0}) + switch_2 = Switch.from_config( + config={"type": "switch", "hostname": "switch_2", "num_ports": 6, "start_up_duration": 0} + ) switch_2.power_on() network.connect(endpoint_a=router.network_interface[2], endpoint_b=switch_2.network_interface[6]) diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 38caf1a2..8266c814 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -72,7 +72,7 @@ def test_dns_client_requests_offline_dns_server(dns_client_and_dns_server): server.power_off() - for i in range(server.shut_down_duration + 1): + for i in range(server.config.shut_down_duration + 1): server.apply_timestep(timestep=i) assert server.operating_state == NodeOperatingState.OFF diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 4e73a050..15fd3ccd 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -13,13 +13,15 @@ from primaite.simulator.system.services.service import Service, ServiceOperating def populated_node( service_class, ) -> Tuple[Server, Service]: - server = Server( - hostname="server", - ip_address="192.168.0.1", - subnet_mask="255.255.255.0", - start_up_duration=0, - shut_down_duration=0, - ) + server_cfg = { + "type": "server", + "hostname": "server", + "ip_address": "192.168.0.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + "shut_down_duration": 0, + } + server: Server = Server.from_config(config=server_cfg) server.power_on() server.software_manager.install(service_class) @@ -31,14 +33,16 @@ def populated_node( def test_service_on_offline_node(service_class): """Test to check that the service cannot be interacted with when node it is on is off.""" - computer: Computer = Computer( - hostname="test_computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - shut_down_duration=0, - ) + computer_cfg = { + "type": "computer", + "hostname": "test_computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + "shut_down_duration": 0, + } + computer: Computer = Computer.from_config(config=computer_cfg) computer.power_on() computer.software_manager.install(service_class) diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index 8aea34c1..8873a494 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -94,7 +94,7 @@ def test_web_page_request_from_shut_down_server(web_client_and_web_server): server.power_off() - for i in range(server.shut_down_duration + 1): + for i in range(server.config.shut_down_duration + 1): server.apply_timestep(timestep=i) # node should be off diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index dbc04f6d..e45fe45d 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -7,10 +7,7 @@ from primaite.simulator.network.hardware.nodes.network.switch import Switch @pytest.fixture(scope="function") def switch() -> Switch: - switch_cfg = {"type": "switch", - "hostname": "switch_1", - "num_ports": 8, - "start_up_duration": 0} + switch_cfg = {"type": "switch", "hostname": "switch_1", "num_ports": 8, "start_up_duration": 0} switch: Switch = Switch.from_config(config=switch_cfg) switch.power_on() switch.show() diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py index 0e0023cd..cb2d3935 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_network_interface_actions.py @@ -7,10 +7,7 @@ from primaite.simulator.network.hardware.nodes.host.computer import Computer @pytest.fixture def node() -> Node: - computer_cfg = {"type": "computer", - "hostname": "test", - "ip_address": "192.168.1.2", - "subnet_mask": "255.255.255.0"} + computer_cfg = {"type": "computer", "hostname": "test", "ip_address": "192.168.1.2", "subnet_mask": "255.255.255.0"} computer = Computer.from_config(config=computer_cfg) return computer diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index d077f46b..f6308a21 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -12,13 +12,12 @@ from tests.conftest import DummyApplication, DummyService @pytest.fixture def node() -> Node: - computer_cfg = {"type": "computer", - "hostname": "test", - "ip_address": "192.168.1.2", - "subnet_mask": "255.255.255.0", - "shut_down_duration": 3, - "operating_state": "OFF", - } + computer_cfg = { + "type": "computer", + "hostname": "test", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + } computer = Computer.from_config(config=computer_cfg) return computer diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index d175b865..9a54f7b2 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -74,7 +74,16 @@ def test_removing_node_that_does_not_exist(network): """Node that does not exist on network should not affect existing nodes.""" assert len(network.nodes) is 7 - network.remove_node(Computer.from_config(config = {"type":"computer","hostname":"new_node", "ip_address":"192.168.1.2", "subnet_mask":"255.255.255.0"})) + network.remove_node( + Computer.from_config( + config={ + "type": "computer", + "hostname": "new_node", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + } + ) + ) assert len(network.nodes) is 7 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 5d8bea80..4ce224f7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -16,23 +16,25 @@ def basic_c2_network() -> Network: network = Network() # Creating two generic nodes for the C2 Server and the C2 Beacon. - computer_a_cfg = {"type": "computer", - "hostname": "computer_a", - "ip_address": "192.168.0.1", - "subnet_mask": "255.255.255.252", - "start_up_duration": 0} - computer_a = Computer.from_config(config = computer_a_cfg) + computer_a_cfg = { + "type": "computer", + "hostname": "computer_a", + "ip_address": "192.168.0.1", + "subnet_mask": "255.255.255.252", + "start_up_duration": 0, + } + computer_a = Computer.from_config(config=computer_a_cfg) computer_a.power_on() computer_a.software_manager.install(software_class=C2Server) - - computer_b_cfg = {"type": "computer", - "hostname": "computer_b", - "ip_address": "192.168.0.2", - "subnet_mask": "255.255.255.252", - "start_up_duration": 0, - } + computer_b_cfg = { + "type": "computer", + "hostname": "computer_b", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.252", + "start_up_duration": 0, + } computer_b = Computer.from_config(config=computer_b_cfg) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index 02b13724..f73e661e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -12,12 +12,13 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def dos_bot() -> DoSBot: - computer_cfg = {"type":"computer", - "hostname": "compromised_pc", - "ip_address": "192.168.0.1", - "subnet_mask": "255.255.255.0", - "start_up_duration": 0, - } + computer_cfg = { + "type": "computer", + "hostname": "compromised_pc", + "ip_address": "192.168.0.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } computer: Computer = Computer.from_config(config=computer_cfg) computer.power_on() @@ -39,7 +40,7 @@ def test_dos_bot_cannot_run_when_node_offline(dos_bot): dos_bot_node.power_off() - for i in range(dos_bot_node.shut_down_duration + 1): + for i in range(dos_bot_node.config.shut_down_duration + 1): dos_bot_node.apply_timestep(timestep=i) assert dos_bot_node.operating_state is NodeOperatingState.OFF diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index 6e32b646..f2b538c0 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -17,14 +17,28 @@ from primaite.simulator.system.services.database.database_service import Databas def database_client_on_computer() -> Tuple[DatabaseClient, Computer]: network = Network() - db_server: Server = Server.from_config(config={"type": "server", "hostname":"db_server", "ip_address":"192.168.0.1", "subnet_mask":"255.255.255.0", "start_up_duration":0}) + db_server: Server = Server.from_config( + config={ + "type": "server", + "hostname": "db_server", + "ip_address": "192.168.0.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) db_server.power_on() db_server.software_manager.install(DatabaseService) db_server.software_manager.software["DatabaseService"].start() - db_client: Computer = Computer.from_config(config = {"type":"computer", - "hostname":"db_client", "ip_address":"192.168.0.2", "subnet_mask":"255.255.255.0", "start_up_duration":0 - }) + db_client: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "db_client", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) db_client.power_on() db_client.software_manager.install(DatabaseClient) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index 85cd369f..a76e4c07 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -12,8 +12,15 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def web_browser() -> WebBrowser: - computer_cfg = {"type": "computer", "hostname": "web_client", "ip_address": "192.168.1.11", "subnet_mask": "255.255.255.0", "default_gateway": "192.168.1.1", "start_up_duration": 0} - + computer_cfg = { + "type": "computer", + "hostname": "web_client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } + computer: Computer = Computer.from_config(config=computer_cfg) computer.power_on() @@ -25,8 +32,15 @@ def web_browser() -> WebBrowser: def test_create_web_client(): - computer_cfg = {"type": "computer", "hostname": "web_client", "ip_address": "192.168.1.11", "subnet_mask": "255.255.255.0", "default_gateway": "192.168.1.1", "start_up_duration": 0} - + computer_cfg = { + "type": "computer", + "hostname": "web_client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } + computer: Computer = Computer.from_config(config=computer_cfg) computer.power_on() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index b7ba2d04..3382e2f7 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -8,12 +8,13 @@ from primaite.simulator.system.services.database.database_service import Databas @pytest.fixture(scope="function") def database_server() -> Node: - node_cfg = {"type": "computer", - "hostname": "db_node", - "ip_address": "192.168.1.2", - "subnet_mask": "255.255.255.0", - "start_up_duration": 0, - } + node_cfg = { + "type": "computer", + "hostname": "db_node", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } node = Computer.from_config(config=node_cfg) node.power_on() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 3f621331..430e3835 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -14,21 +14,15 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def dns_client() -> Computer: - - node_cfg = {"type": "computer", - "hostname": "dns_client", - "ip_address": "192.168.1.11", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "dns_server": IPv4Address("192.168.1.10")} + node_cfg = { + "type": "computer", + "hostname": "dns_client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "dns_server": IPv4Address("192.168.1.10"), + } node = Computer.from_config(config=node_cfg) - # node = Computer( - # hostname="dns_client", - # ip_address="192.168.1.11", - # subnet_mask="255.255.255.0", - # default_gateway="192.168.1.1", - # dns_server=IPv4Address("192.168.1.10"), - # ) return node @@ -69,7 +63,7 @@ def test_dns_client_check_domain_exists_when_not_running(dns_client): dns_client.power_off() - for i in range(dns_client.shut_down_duration + 1): + for i in range(dns_client.config.shut_down_duration + 1): dns_client.apply_timestep(timestep=i) assert dns_client.operating_state is NodeOperatingState.OFF diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 8df96099..fd193415 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -16,12 +16,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def dns_server() -> Node: - node_cfg = {"type": "server", - "hostname": "dns_server", - "ip_address": "192.168.1.10", - "subnet_mask":"255.255.255.0", - "default_gateway": "192.168.1.1", - "start_up_duration":0} + node_cfg = { + "type": "server", + "hostname": "dns_server", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } node = Server.from_config(config=node_cfg) node.power_on() node.software_manager.install(software_class=DNSServer) @@ -55,12 +57,13 @@ def test_dns_server_receive(dns_server): # register the web server in the domain controller dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) - client_cfg = {"type": "computer", - "hostname": "client", - "ip_address": "192.168.1.11", - "subnet_mask": "255.255.255.0", - "start_up_duration": 0, - } + client_cfg = { + "type": "computer", + "hostname": "client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } client = Computer.from_config(config=client_cfg) client.power_on() client.dns_server = IPv4Address("192.168.1.10") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index c6e10b7d..28ca194e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -16,13 +16,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def ftp_client() -> Node: - node_cfg = {"type": "computer", - "hostname": "ftp_client", - "ip_address":"192.168.1.11", - "subnet_mask":"255.255.255.0", - "default_gateway":"192.168.1.1", - "start_up_duration": 0, - } + node_cfg = { + "type": "computer", + "hostname": "ftp_client", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } node = Computer.from_config(config=node_cfg) node.power_on() return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 5cae88e0..ea8ab071 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -14,12 +14,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def ftp_server() -> Node: - node_cfg = {"type": "server", - "hostname":"ftp_server", - "ip_address":"192.168.1.10", - "subnet_mask": "255.255.255.0", - "default_gateway": "192.168.1.1", - "start_up_duration":0} + node_cfg = { + "type": "server", + "hostname": "ftp_server", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } node = Server.from_config(config=node_cfg) node.power_on() node.software_manager.install(software_class=FTPServer) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 08bef92d..1666f008 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -29,8 +29,8 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def terminal_on_computer() -> Tuple[Terminal, Computer]: - computer: Computer = Computer( - hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0 + computer: Computer = Computer.from_config(config={"type":"computer", + "hostname":"node_a", "ip_address":"192.168.0.10", "subnet_mask":"255.255.255.0", "start_up_duration":0} ) computer.power_on() terminal: Terminal = computer.software_manager.software.get("Terminal") @@ -41,11 +41,19 @@ def terminal_on_computer() -> Tuple[Terminal, Computer]: @pytest.fixture(scope="function") def basic_network() -> Network: network = Network() - node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a = Computer.from_config(config={"type":"computer", + "hostname":"node_a", + "ip_address":"192.168.0.10", + "subnet_mask":"255.255.255.0", + "start_up_duration":0}) node_a.power_on() node_a.software_manager.get_open_ports() - node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b = Computer.from_config(config={"type":"computer", + "hostname":"node_b", + "ip_address":"192.168.0.11", + "subnet_mask":"255.255.255.0", + "start_up_duration":0}) node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) @@ -57,18 +65,20 @@ def wireless_wan_network(): network = Network() # Configure PC A - pc_a = Computer( - hostname="pc_a", - ip_address="192.168.0.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.0.1", - start_up_duration=0, - ) + pc_a_cfg = {"type": "computer", + "hostname":"pc_a", + "ip_address":"192.168.0.2", + "subnet_mask":"255.255.255.0", + "default_gateway":"192.168.0.1", + "start_up_duration":0, + } + + pc_a = Computer.from_config(config=pc_a_cfg) pc_a.power_on() network.add_node(pc_a) # Configure Router 1 - router_1 = WirelessRouter(hostname="router_1", start_up_duration=0, airspace=network.airspace) + router_1 = WirelessRouter.from_config(config={"type":"wireless_router", "hostname":"router_1", "start_up_duration":0, "airspace":network.airspace}) router_1.power_on() network.add_node(router_1) @@ -88,18 +98,21 @@ def wireless_wan_network(): ) # Configure PC B - pc_b = Computer( - hostname="pc_b", - ip_address="192.168.2.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.2.1", - start_up_duration=0, - ) + + pc_b_cfg = {"type": "computer", + "hostname":"pc_b", + "ip_address":"192.168.2.2", + "subnet_mask":"255.255.255.0", + "default_gateway":"192.168.2.1", + "start_up_duration":0, + } + + pc_b = Computer.from_config(config=pc_b_cfg) pc_b.power_on() network.add_node(pc_b) # Configure Router 2 - router_2 = WirelessRouter(hostname="router_2", start_up_duration=0, airspace=network.airspace) + router_2 = WirelessRouter.from_config(config={"type":"wireless_router", "hostname":"router_2", "start_up_duration":0, "airspace":network.airspace}) router_2.power_on() network.add_node(router_2) @@ -131,7 +144,7 @@ def game_and_agent_fixture(game_and_agent): game, agent = game_and_agent client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - client_1.start_up_duration = 3 + client_1.config.start_up_duration = 3 return game, agent @@ -143,7 +156,11 @@ def test_terminal_creation(terminal_on_computer): def test_terminal_install_default(): """Terminal should be auto installed onto Nodes""" - computer = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + computer: Computer = Computer.from_config(config={"type":"computer", + "hostname":"node_a", + "ip_address":"192.168.0.10", + "subnet_mask":"255.255.255.0", + "start_up_duration":0}) computer.power_on() assert computer.software_manager.software.get("Terminal") @@ -151,7 +168,7 @@ def test_terminal_install_default(): def test_terminal_not_on_switch(): """Ensure terminal does not auto-install to switch""" - test_switch = Switch(hostname="Test") + test_switch = Switch.from_config(config={"type":"switch", "hostname":"Test"}) assert not test_switch.software_manager.software.get("Terminal") @@ -357,8 +374,6 @@ def test_multiple_remote_terminals_same_node(basic_network): for attempt in range(3): remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") - terminal_a.show() - assert len(terminal_a._connections) == 3 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index f0901b70..916e1991 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -16,12 +16,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def web_server() -> Server: - node_cfg = {"type": "server", - "hostname":"web_server", - "ip_address": "192.168.1.10", - "subnet_mask": "255.255.255.0", - "default_gateway":"192.168.1.1", - "start_up_duration":0 } + node_cfg = { + "type": "server", + "hostname": "web_server", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } node = Server.from_config(config=node_cfg) node.power_on() node.software_manager.install(WebServer) From f4b73057d2094f9c248ad8f3ae757501378afff3 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 27 Jan 2025 17:08:33 +0000 Subject: [PATCH 157/224] #3075: Update notebooks to reflect extension changes. --- src/primaite/notebooks/Action-masking.ipynb | 17 +++++----- ...ommand-and-Control-E2E-Demonstration.ipynb | 16 +++++++-- ...a-Manipulation-Customising-Red-Agent.ipynb | 33 +++++++++++++------ .../Data-Manipulation-E2E-Demonstration.ipynb | 15 +++++---- .../Getting-Information-Out-Of-PrimAITE.ipynb | 6 ++-- ...ege-Escalation-and-Data-Loss-Example.ipynb | 15 +++++++-- .../Training-an-RLLIB-MARL-System.ipynb | 20 +++++++++-- .../notebooks/Training-an-RLLib-Agent.ipynb | 23 ++++++++++--- .../notebooks/Using-Episode-Schedules.ipynb | 15 ++------- src/primaite/notebooks/multi-processing.ipynb | 18 +++++----- 10 files changed, 116 insertions(+), 62 deletions(-) diff --git a/src/primaite/notebooks/Action-masking.ipynb b/src/primaite/notebooks/Action-masking.ipynb index 7fde0a49..64015080 100644 --- a/src/primaite/notebooks/Action-masking.ipynb +++ b/src/primaite/notebooks/Action-masking.ipynb @@ -22,13 +22,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.config.load import data_manipulation_config_path\n", - "from prettytable import PrettyTable" + "from prettytable import PrettyTable\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent\n" ] }, { @@ -103,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -116,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -156,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -167,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -204,7 +205,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -218,7 +219,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 66a684de..625510b8 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -16,6 +16,15 @@ "execution_count": null, "metadata": {}, "outputs": [], + "source": [ + "!primaite setup" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], "source": [ "# Imports\n", "import yaml\n", @@ -27,7 +36,8 @@ "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command\n", "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", - "from primaite.simulator.network.hardware.nodes.host.server import Server" + "from primaite.simulator.network.hardware.nodes.host.server import Server\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent" ] }, { @@ -43,7 +53,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -1763,7 +1773,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 756fc44f..de6d3fe5 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -36,12 +36,13 @@ "from primaite.game.agent.interface import AgentHistoryItem\n", "from primaite.session.environment import PrimaiteGymEnv\n", "import yaml\n", - "from pprint import pprint" + "from pprint import pprint\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent, data_manipulation_bot" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -68,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -79,7 +80,8 @@ " if red_action == 'do_nothing':\n", " red_str = 'DO NOTHING'\n", " elif red_action == 'node_application_execute':\n", - " client = \"client 1\" if red_info.parameters['node_id'] == 0 else \"client 2\"\n", + " client = \"client 1\" if red_info.parameters['node_name'] == 0 else \"client 2\"\n", + "\n", " red_str = f\"ATTACK from {client}\"\n", " return red_str" ] @@ -243,7 +245,8 @@ "outputs": [], "source": [ "change = yaml.safe_load(\"\"\"\n", - "start_settings:\n", + " possible_start_nodes: [client_1, client_2]\n", + " target_application: DataManipulationBot\n", " start_step: 25\n", " frequency: 20\n", " variance: 0\n", @@ -253,7 +256,9 @@ " cfg = yaml.safe_load(f)\n", " for agent in cfg['agents']:\n", " if agent['ref'] == \"data_manipulation_attacker\":\n", + " print(f\"{agent['agent_settings']=}\")\n", " agent['agent_settings'] = change\n", + " print(f\"{agent['agent_settings']=}\")\n", "\n", "env = PrimaiteGymEnv(env_config = cfg)\n", "env.reset()\n", @@ -310,9 +315,17 @@ "outputs": [], "source": [ "change = yaml.safe_load(\"\"\"\n", - "# TODO:\n", + " action_space:\n", + " action_map:\n", + " 0:\n", + " action: do_nothing\n", + " options: {}\n", + " 1:\n", + " action: node_application_execute\n", + " options:\n", + " node_name: client_1\n", + " application_name: DataManipulationBot\n", "\"\"\")\n", - "#TODO 2869 fix\n", "\n", "with open(data_manipulation_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n", @@ -438,7 +451,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -452,7 +465,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index dbc6f0c1..3818bb18 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -382,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "tags": [] }, @@ -394,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "tags": [] }, @@ -405,7 +405,8 @@ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.game.agent.interface import AgentHistoryItem\n", "import yaml\n", - "from pprint import pprint\n" + "from pprint import pprint\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent, data_manipulation_bot\n" ] }, { @@ -429,9 +430,9 @@ " cfg = yaml.safe_load(f)\n", " # set success probability to 1.0 to avoid rerunning cells.\n", " cfg['simulation']['network']['nodes'][8]['applications'][0]['options']['data_manipulation_p_of_success'] = 1.0\n", - " cfg['simulation']['network']['nodes'][9]['applications'][0]['options']['data_manipulation_p_of_success'] = 1.0\n", + " cfg['simulation']['network']['nodes'][9]['applications'][1]['options']['data_manipulation_p_of_success'] = 1.0\n", " cfg['simulation']['network']['nodes'][8]['applications'][0]['options']['port_scan_p_of_success'] = 1.0\n", - " cfg['simulation']['network']['nodes'][9]['applications'][0]['options']['port_scan_p_of_success'] = 1.0\n", + " cfg['simulation']['network']['nodes'][9]['applications'][1]['options']['port_scan_p_of_success'] = 1.0\n", " # don't flatten observations so that we can see what is going on\n", " cfg['agents'][3]['agent_settings']['flatten_obs'] = False\n", "\n", @@ -450,7 +451,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -461,7 +462,7 @@ " if red_action == 'do_nothing':\n", " red_str = 'DO NOTHING'\n", " elif red_action == 'node_application_execute':\n", - " client = \"client 1\" if red_info.parameters['node_id'] == 0 else \"client 2\"\n", + " client = \"client 1\" if red_info.parameters['node_name'] == 0 else \"client 2\"\n", " red_str = f\"ATTACK from {client}\"\n", " return red_str" ] diff --git a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb index f8691d7d..d7d60d76 100644 --- a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb +++ b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb @@ -32,6 +32,8 @@ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", "from notebook.services.config import ConfigManager\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent\n", + "\n", "\n", "cm = ConfigManager().update('notebook', {'limit_output': 50}) # limit output lines to 50 - for neatness\n", "\n", @@ -157,7 +159,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -171,7 +173,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb index fcda4dbd..35d3813a 100644 --- a/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb +++ b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb @@ -54,6 +54,15 @@ { "cell_type": "code", "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, "metadata": { "tags": [] }, @@ -68,7 +77,7 @@ "from primaite.simulator.network.hardware.nodes.host.server import Server\n", "from primaite.simulator.system.applications.database_client import DatabaseClient\n", "from primaite.simulator.system.applications.web_browser import WebBrowser\n", - "from primaite.simulator.system.services.database.database_service import DatabaseService" + "from primaite.simulator.system.services.database.database_service import DatabaseService\n" ] }, { @@ -80,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "tags": [] }, @@ -103,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "tags": [] }, diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index 76cab86a..dadb399e 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -40,6 +40,7 @@ "import ray\n", "from ray.rllib.algorithms.ppo import PPOConfig\n", "from primaite.session.ray_envs import PrimaiteRayMARLEnv\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent\n", "\n", "with open(PRIMAITE_PATHS.user_config_path / 'example_config/data_manipulation_marl.yaml', 'r') as f:\n", " cfg = yaml.safe_load(f)\n", @@ -56,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -102,7 +103,20 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "*** SIGTERM received at time=1737996337 on cpu 7 ***\n", + "PC: @ 0x7f3649b0fe2e (unknown) epoll_wait\n", + " @ 0x7f3649a2c520 (unknown) (unknown)\n", + "[2025-01-27 16:45:37,381 E 117142 117142] logging.cc:440: *** SIGTERM received at time=1737996337 on cpu 7 ***\n", + "[2025-01-27 16:45:37,381 E 117142 117142] logging.cc:440: PC: @ 0x7f3649b0fe2e (unknown) epoll_wait\n", + "[2025-01-27 16:45:37,381 E 117142 117142] logging.cc:440: @ 0x7f3649a2c520 (unknown) (unknown)\n" + ] + } + ], "source": [ "eval = algo.evaluate()" ] @@ -110,7 +124,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index 7252b046..64a9e7ab 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -32,6 +32,8 @@ "from primaite.session.ray_envs import PrimaiteRayEnv\n", "import ray\n", "from ray.rllib.algorithms.ppo import PPOConfig\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent\n", + "\n", "\n", "# If you get an error saying this config file doesn't exist, you may need to run `primaite setup` in your command line\n", "# to copy the files to your user data path.\n", @@ -50,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -96,7 +98,20 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "*** SIGTERM received at time=1737996055 on cpu 1 ***\n", + "PC: @ 0x7f6e254a6e2e (unknown) epoll_wait\n", + " @ 0x7f6e253c3520 (unknown) (unknown)\n", + "[2025-01-27 16:40:55,343 E 114171 114171] logging.cc:440: *** SIGTERM received at time=1737996055 on cpu 1 ***\n", + "[2025-01-27 16:40:55,343 E 114171 114171] logging.cc:440: PC: @ 0x7f6e254a6e2e (unknown) epoll_wait\n", + "[2025-01-27 16:40:55,344 E 114171 114171] logging.cc:440: @ 0x7f6e253c3520 (unknown) (unknown)\n" + ] + } + ], "source": [ "eval = algo.evaluate()" ] @@ -104,7 +119,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -118,7 +133,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index d08ca67b..311fe4fb 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -48,6 +48,7 @@ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite import PRIMAITE_PATHS\n", "from prettytable import PrettyTable\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent, data_manipulation_bot\n", "scenario_path = PRIMAITE_PATHS.user_config_path / \"example_config/scenario_with_placeholders\"" ] }, @@ -409,21 +410,9 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/multi-processing.ipynb b/src/primaite/notebooks/multi-processing.ipynb index ad386f34..798bf3ff 100644 --- a/src/primaite/notebooks/multi-processing.ipynb +++ b/src/primaite/notebooks/multi-processing.ipynb @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ @@ -37,13 +37,13 @@ "from stable_baselines3 import PPO\n", "from stable_baselines3.common.utils import set_random_seed\n", "from stable_baselines3.common.vec_env import SubprocVecEnv\n", - "\n", - "from primaite.session.environment import PrimaiteGymEnv\n" + "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.game.agent.scripted_agents import probabilistic_agent\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": {}, "outputs": [], "source": [ @@ -69,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -90,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": {}, "outputs": [], "source": [ @@ -138,7 +138,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -152,7 +152,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, From e1f2f73db08840c7c9670cb54a4feb0424301e9d Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 28 Jan 2025 09:37:58 +0000 Subject: [PATCH 158/224] #2887 - Test changes to correct NodeOperatingState is correct per passed config. --- src/primaite/simulator/network/hardware/base.py | 1 + .../game_layer/observations/test_link_observations.py | 2 +- .../game_layer/observations/test_router_observation.py | 4 ++-- .../integration_tests/system/test_database_on_node.py | 10 +++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index de97f22b..21f2946b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1574,6 +1574,7 @@ class Node(SimComponent, ABC): msg = f"Configuration contains an invalid Node type: {config['type']}" return ValueError(msg) obj = cls(config=cls.ConfigSchema(**config)) + obj.operating_state = NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] return obj def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py index f95d35c2..1ab50a68 100644 --- a/tests/integration_tests/game_layer/observations/test_link_observations.py +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -57,7 +57,7 @@ def test_link_observation(): net = Network() sim = Simulation(network=net) switch: Switch = Switch.from_config( - config={"type": "switch", "hostname": "switch", "num_ports": 5, "operating_state": NodeOperatingState.ON} + config={"type": "switch", "hostname": "switch", "num_ports": 5, "operating_state": "ON"} ) computer_1: Computer = Computer.from_config( config={ diff --git a/tests/integration_tests/game_layer/observations/test_router_observation.py b/tests/integration_tests/game_layer/observations/test_router_observation.py index 8335867d..495e102d 100644 --- a/tests/integration_tests/game_layer/observations/test_router_observation.py +++ b/tests/integration_tests/game_layer/observations/test_router_observation.py @@ -17,7 +17,7 @@ def test_router_observation(): """Test adding/removing acl rules and enabling/disabling ports.""" net = Network() router = Router.from_config( - config={"type": "router", "hostname": "router", "num_ports": 5, "operating_state": NodeOperatingState.ON} + config={"type": "router", "hostname": "router", "num_ports": 5, "operating_state": "ON"} ) ports = [PortObservation(where=["NICs", i]) for i in range(1, 6)] @@ -92,7 +92,7 @@ def test_router_observation(): # connect a switch to the router and check that only the correct port is updated switch: Switch = Switch.from_config( - config={"type": "switch", "hostname": "switch", "num_ports": 1, "operating_state": NodeOperatingState.ON} + config={"type": "switch", "hostname": "switch", "num_ports": 1, "operating_state": "ON"} ) link = net.connect(router.network_interface[1], switch.network_interface[1]) assert router.network_interface[1].enabled diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index bb25f8c8..87aca129 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -20,11 +20,11 @@ from primaite.simulator.system.software import SoftwareHealthState @pytest.fixture(scope="function") def peer_to_peer() -> Tuple[Computer, Computer]: network = Network() - node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a: Computer = Computer.from_config(config={"type":"computer", "hostname":"node_a", "ip_address":"192.168.0.10", "subnet_mask":"255.255.255.0", "start_up_duration":0}) node_a.power_on() node_a.software_manager.get_open_ports() - node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b: Computer = Computer.from_config(config={"type":"computer", "hostname":"node_b", "ip_address":"192.168.0.11", "subnet_mask":"255.255.255.0", "start_up_duration":0}) node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) @@ -412,8 +412,8 @@ def test_database_service_can_terminate_connection(peer_to_peer): def test_client_connection_terminate_does_not_terminate_another_clients_connection(): network = Network() - db_server = Server( - hostname="db_client", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0 + db_server: Server = Server.from_config(config={"type":"server", + "hostname":"db_client", "ip_address":"192.168.0.11", "subnet_mask":"255.255.255.0", "start_up_duration":0} ) db_server.power_on() @@ -465,6 +465,6 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti def test_database_server_install_ftp_client(): - server = Server(hostname="db_server", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + server: Server = Server.from_config(config={"type":"server", "hostname":"db_server", "ip_address":"192.168.1.2", "subnet_mask":"255.255.255.0", "start_up_duration":0}) server.software_manager.install(DatabaseService) assert server.software_manager.software.get("FTPClient") From 4fb4c5e0f9e26b2d0c797beec562b8f35344fe74 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 28 Jan 2025 17:18:07 +0000 Subject: [PATCH 159/224] #3075: Fix error in config file. --- src/primaite/config/_package_data/data_manipulation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index b0d5d087..fc48c8e6 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -81,7 +81,7 @@ agents: action: node_application_execute options: node_name: client_1 - application_name: WebBrowser + application_name: DatabaseClient reward_function: reward_components: From f85aace31b1a994c81db70bb92333ed3cf44fc3d Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 28 Jan 2025 19:35:27 +0000 Subject: [PATCH 160/224] #2887 - Correct networking troubles causing test failures --- src/primaite/game/game.py | 16 +++++------ src/primaite/simulator/network/container.py | 2 +- .../simulator/network/hardware/base.py | 17 +++++++----- .../network/hardware/nodes/host/host_node.py | 27 +++++++++---------- .../hardware/nodes/network/firewall.py | 1 + .../network/hardware/nodes/network/router.py | 23 +++++----------- src/primaite/simulator/network/networks.py | 14 +++++----- .../network/test_frame_transmission.py | 2 +- .../system/test_database_on_node.py | 2 +- .../system/test_ftp_client_server.py | 2 +- .../test_simulation/test_request_response.py | 4 +-- 11 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f4d118ac..b1ff1f9d 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -278,11 +278,11 @@ class PrimaiteGame: # TODO: handle simulation defaults more cleanly if "node_start_up_duration" in defaults_config: - new_node.start_up_duration = defaults_config["node_startup_duration"] + new_node.config.start_up_duration = defaults_config["node_startup_duration"] if "node_shut_down_duration" in defaults_config: - new_node.shut_down_duration = defaults_config["node_shut_down_duration"] + new_node.config.shut_down_duration = defaults_config["node_shut_down_duration"] if "node_scan_duration" in defaults_config: - new_node.node_scan_duration = defaults_config["node_scan_duration"] + new_node.config.node_scan_duration = defaults_config["node_scan_duration"] if "folder_scan_duration" in defaults_config: new_node.file_system._default_folder_scan_duration = defaults_config["folder_scan_duration"] if "folder_restore_duration" in defaults_config: @@ -337,7 +337,7 @@ class PrimaiteGame: # TODO: handle simulation defaults more cleanly if "service_fix_duration" in defaults_config: - new_service.fixing_duration = defaults_config["service_fix_duration"] + new_service.config.fixing_duration = defaults_config["service_fix_duration"] if "service_restart_duration" in defaults_config: new_service.restart_duration = defaults_config["service_restart_duration"] if "service_install_duration" in defaults_config: @@ -394,8 +394,8 @@ class PrimaiteGame: new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) # temporarily set to 0 so all nodes are initially on - new_node.start_up_duration = 0 - new_node.shut_down_duration = 0 + new_node.config.start_up_duration = 0 + new_node.config.shut_down_duration = 0 net.add_node(new_node) # run through the power on step if the node is to be turned on at the start @@ -403,8 +403,8 @@ class PrimaiteGame: new_node.power_on() # set start up and shut down duration - new_node.start_up_duration = int(node_cfg.get("start_up_duration", 3)) - new_node.shut_down_duration = int(node_cfg.get("shut_down_duration", 3)) + new_node.config.start_up_duration = int(node_cfg.get("start_up_duration", 3)) + new_node.config.shut_down_duration = int(node_cfg.get("shut_down_duration", 3)) # 1.1 Create Node Sets for node_set_cfg in node_sets_cfg: diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 982495a4..247c06bb 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -201,7 +201,7 @@ class Network(SimComponent): port_str, port.ip_address, port.subnet_mask, - node.default_gateway, + node.config.default_gateway, ] ) print(table) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 21f2946b..08e100d2 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1493,17 +1493,12 @@ class Node(SimComponent, ABC): :param hostname: The node hostname on the network. :param operating_state: The node operating state, either ON or OFF. """ - - default_gateway: Optional[IPV4Address] = None - "The default gateway IP address for forwarding network traffic to other networks." operating_state: NodeOperatingState = NodeOperatingState.OFF "The hardware state of the node." network_interfaces: Dict[str, NetworkInterface] = {} "The Network Interfaces on the node." network_interface: Dict[int, NetworkInterface] = {} "The Network Interfaces on the node by port id." - dns_server: Optional[IPv4Address] = None - "List of IP addresses of DNS servers used for name resolution." accounts: Dict[str, Account] = {} "All accounts on the node." applications: Dict[str, Application] = {} @@ -1567,6 +1562,16 @@ class Node(SimComponent, ABC): red_scan_countdown: int = 0 "Time steps until reveal to red scan is complete." + dns_server: Optional[IPv4Address] = None + "List of IP addresses of DNS servers used for name resolution." + + default_gateway: Optional[IPV4Address] = None + "The default gateway IP address for forwarding network traffic to other networks." + + @property + def dns_server(self) -> Optional[IPv4Address]: + return self.config.dns_server + @classmethod def from_config(cls, config: Dict) -> "Node": """Create Node object from a given configuration dictionary.""" @@ -1615,7 +1620,7 @@ class Node(SimComponent, ABC): sys_log=kwargs.get("sys_log"), session_manager=kwargs.get("session_manager"), file_system=kwargs.get("file_system"), - dns_server=kwargs.get("dns_server"), + dns_server=kwargs["config"].dns_server, ) super().__init__(**kwargs) self._install_system_software() diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 23db025d..3b1d8e48 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -46,8 +46,8 @@ class HostARP(ARP): :return: The MAC address of the default gateway if present in the ARP cache; otherwise, None. """ - if self.software_manager.node.default_gateway: - return self.get_arp_cache_mac_address(self.software_manager.node.default_gateway) + if self.software_manager.node.config.default_gateway: + return self.get_arp_cache_mac_address(self.software_manager.node.config.default_gateway) def get_default_gateway_network_interface(self) -> Optional[NIC]: """ @@ -55,8 +55,8 @@ class HostARP(ARP): :return: The NIC associated with the default gateway if it exists in the ARP cache; otherwise, None. """ - if self.software_manager.node.default_gateway and self.software_manager.node.has_enabled_network_interface: - return self.get_arp_cache_network_interface(self.software_manager.node.default_gateway) + if self.software_manager.node.config.default_gateway and self.software_manager.node.has_enabled_network_interface: + return self.get_arp_cache_network_interface(self.software_manager.node.config.default_gateway) def _get_arp_cache_mac_address( self, ip_address: IPV4Address, is_reattempt: bool = False, is_default_gateway_attempt: bool = False @@ -75,7 +75,7 @@ class HostARP(ARP): if arp_entry: return arp_entry.mac_address - if ip_address == self.software_manager.node.default_gateway: + if ip_address == self.software_manager.node.config.default_gateway: is_reattempt = True if not is_reattempt: self.send_arp_request(ip_address) @@ -83,11 +83,11 @@ class HostARP(ARP): ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt ) else: - if self.software_manager.node.default_gateway: + if self.software_manager.node.config.default_gateway: if not is_default_gateway_attempt: - self.send_arp_request(self.software_manager.node.default_gateway) + self.send_arp_request(self.software_manager.node.config.default_gateway) return self._get_arp_cache_mac_address( - ip_address=self.software_manager.node.default_gateway, + ip_address=self.software_manager.node.config.default_gateway, is_reattempt=True, is_default_gateway_attempt=True, ) @@ -118,7 +118,7 @@ class HostARP(ARP): if arp_entry: return self.software_manager.node.network_interfaces[arp_entry.network_interface_uuid] else: - if ip_address == self.software_manager.node.default_gateway: + if ip_address == self.software_manager.node.config.default_gateway: is_reattempt = True if not is_reattempt: self.send_arp_request(ip_address) @@ -126,11 +126,11 @@ class HostARP(ARP): ip_address=ip_address, is_reattempt=True, is_default_gateway_attempt=is_default_gateway_attempt ) else: - if self.software_manager.node.default_gateway: + if self.software_manager.node.config.default_gateway: if not is_default_gateway_attempt: - self.send_arp_request(self.software_manager.node.default_gateway) + self.send_arp_request(self.software_manager.node.config.default_gateway) return self._get_arp_cache_network_interface( - ip_address=self.software_manager.node.default_gateway, + ip_address=self.software_manager.node.config.default_gateway, is_reattempt=True, is_default_gateway_attempt=True, ) @@ -333,9 +333,8 @@ class HostNode(Node, identifier="HostNode"): """Configuration Schema for HostNode class.""" hostname: str = "HostNode" - ip_address: IPV4Address = "192.168.0.1" subnet_mask: IPV4Address = "255.255.255.0" - default_gateway: IPV4Address = "192.168.10.1" + ip_address: IPV4Address def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 01c5159b..99dd48c4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -141,6 +141,7 @@ class Firewall(Router, identifier="firewall"): self.external_outbound_acl.sys_log = kwargs["sys_log"] self.external_outbound_acl.name = f"{kwargs['config'].hostname} - External Outbound" + self.power_on() def _init_request_manager(self) -> RequestManager: """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 3ecb761b..ebb35cf3 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1211,32 +1211,22 @@ class Router(NetworkNode, identifier="router"): "The Router Interfaces on the node." network_interface: Dict[int, RouterInterface] = {} "The Router Interfaces on the node by port id." - - sys_log: SysLog - acl: AccessControlList - route_table: RouteTable - config: "Router.ConfigSchema" = Field(default_factory=lambda: Router.ConfigSchema()) + config: "Router.ConfigSchema" class ConfigSchema(NetworkNode.ConfigSchema): - """Configuration Schema for Router Objects.""" + + hostname: str = "router" + num_ports: int - num_ports: int = 5 - """Number of ports available for this Router. Default is 5""" - - hostname: str = "Router" - - ports: Dict[Union[int, str], Dict] = {} def __init__(self, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(kwargs["config"].hostname) if not kwargs.get("acl"): - kwargs["acl"] = AccessControlList( - sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname - ) + kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname) if not kwargs.get("route_table"): kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"]) super().__init__(**kwargs) @@ -1562,7 +1552,7 @@ class Router(NetworkNode, identifier="router"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Network Interfaces" + table.title = f"{self.config.hostname} Network Interfaces" for port, network_interface in self.network_interface.items(): table.add_row( [ @@ -1666,4 +1656,5 @@ class Router(NetworkNode, identifier="router"): next_hop_ip_address = config["default_route"].get("next_hop_ip_address", None) if next_hop_ip_address: router.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) + router.operating_state = NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] return router diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 0579f137..644b2a4a 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -40,42 +40,42 @@ def client_server_routed() -> Network: network = Network() # Router 1 - router_1 = Router(hostname="router_1", num_ports=3) + router_1 = Router(config=dict(hostname="router_1", num_ports=3)) router_1.power_on() router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") router_1.configure_port(port=2, ip_address="192.168.2.1", subnet_mask="255.255.255.0") # Switch 1 - switch_1 = Switch(hostname="switch_1", num_ports=6) + switch_1 = Switch(config=dict(hostname="switch_1", num_ports=6)) switch_1.power_on() network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[6]) router_1.enable_port(1) # Switch 2 - switch_2 = Switch(hostname="switch_2", num_ports=6) + switch_2 = Switch(config=dict(hostname="switch_2", num_ports=6)) switch_2.power_on() network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[6]) router_1.enable_port(2) # Client 1 - client_1 = Computer( + client_1 = Computer(config=dict( hostname="client_1", ip_address="192.168.2.2", subnet_mask="255.255.255.0", default_gateway="192.168.2.1", start_up_duration=0, - ) + )) client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) # Server 1 - server_1 = Server( + server_1 = Server(config=dict( hostname="server_1", ip_address="192.168.1.2", subnet_mask="255.255.255.0", default_gateway="192.168.1.1", start_up_duration=0, - ) + )) server_1.power_on() network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1]) diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index 327c87e5..cff99e07 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -41,7 +41,7 @@ def test_multi_nic(): """Tests that Computers with multiple NICs can ping each other and the data go across the correct links.""" network = Network() - node_a = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) + node_a = Computer(config=dict(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0)) node_a.power_on() node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 87aca129..64b6ddbc 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -338,7 +338,7 @@ def test_database_client_cannot_query_offline_database_server(uc2_network): assert db_connection.query("INSERT") is True db_server.power_off() - for i in range(db_server.shut_down_duration + 1): + for i in range(db_server.config.shut_down_duration + 1): uc2_network.apply_timestep(timestep=i) assert db_server.operating_state is NodeOperatingState.OFF diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index fa4df0a9..57e42457 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -87,7 +87,7 @@ def test_ftp_client_tries_to_connect_to_offline_server(ftp_client_and_ftp_server server.power_off() - for i in range(server.shut_down_duration + 1): + for i in range(server.config.shut_down_duration + 1): server.apply_timestep(timestep=i) assert ftp_client.operating_state == ServiceOperatingState.RUNNING diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index 21152199..efc97ce6 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -140,9 +140,9 @@ class TestDataManipulationGreenRequests: client_1 = net.get_node_by_hostname("client_1") client_2 = net.get_node_by_hostname("client_2") - client_1.shut_down_duration = 0 + client_1.config.shut_down_duration = 0 client_1.power_off() - client_2.shut_down_duration = 0 + client_2.config.shut_down_duration = 0 client_2.power_off() client_1_browser_execute_off = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) From 51f1c91e154d01874cce2bc87eb00d6c2a70858e Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 29 Jan 2025 11:55:10 +0000 Subject: [PATCH 161/224] #2887 - Fixed Node unit test failures --- src/primaite/game/game.py | 3 +- src/primaite/simulator/network/airspace.py | 2 +- .../network/hardware/nodes/network/router.py | 16 ++- .../hardware/nodes/network/wireless_router.py | 8 +- .../simulator/system/applications/nmap.py | 4 +- .../test_agents_use_action_masks.py | 1 + .../extensions/nodes/super_computer.py | 4 +- ...ndwidth_load_checks_before_transmission.py | 2 +- .../network/test_firewall.py | 63 +++++---- .../network/test_frame_transmission.py | 62 ++++++--- ...test_multi_lan_internet_example_network.py | 1 + .../network/test_network_creation.py | 84 +++++++++-- .../integration_tests/network/test_routing.py | 70 ++++++---- .../network/test_wireless_router.py | 46 +++--- .../system/test_application_on_node.py | 34 +++-- .../system/test_database_on_node.py | 62 +++++++-- .../test_user_session_manager_logins.py | 30 ++-- .../test_simulation/test_request_response.py | 2 +- .../_network/_hardware/test_node_actions.py | 2 + .../_system/_services/test_dns_client.py | 10 ++ .../_system/_services/test_dns_server.py | 2 +- .../_system/_services/test_terminal.py | 131 ++++++++++-------- 22 files changed, 427 insertions(+), 212 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index b1ff1f9d..05c13c2a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -269,7 +269,8 @@ class PrimaiteGame: new_node = None if n_type in Node._registry: - # simplify down Node creation: + if n_type == "wireless_router": + node_cfg["airspace"] = net.airspace new_node = Node._registry[n_type].from_config(config=node_cfg) else: msg = f"invalid node type {n_type} in config" diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 5549eb78..7ede0bb0 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -178,7 +178,7 @@ class AirSpace(BaseModel): status = "Enabled" if interface.enabled else "Disabled" table.add_row( [ - interface._connected_node.hostname, # noqa + interface._connected_node.config.hostname, # noqa interface.mac_address, interface.ip_address if hasattr(interface, "ip_address") else None, interface.subnet_mask if hasattr(interface, "subnet_mask") else None, diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index ebb35cf3..dd32fa31 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import Field, validate_call +from pydantic import validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -1217,16 +1217,18 @@ class Router(NetworkNode, identifier="router"): config: "Router.ConfigSchema" class ConfigSchema(NetworkNode.ConfigSchema): - - hostname: str = "router" - num_ports: int + """Configuration Schema for Routers.""" + hostname: str = "router" + num_ports: int = 5 def __init__(self, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(kwargs["config"].hostname) if not kwargs.get("acl"): - kwargs["acl"] = AccessControlList(sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname) + kwargs["acl"] = AccessControlList( + sys_log=kwargs["sys_log"], implicit_action=ACLAction.DENY, name=kwargs["config"].hostname + ) if not kwargs.get("route_table"): kwargs["route_table"] = RouteTable(sys_log=kwargs["sys_log"]) super().__init__(**kwargs) @@ -1656,5 +1658,7 @@ class Router(NetworkNode, identifier="router"): next_hop_ip_address = config["default_route"].get("next_hop_ip_address", None) if next_hop_ip_address: router.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) - router.operating_state = NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] + router.operating_state = ( + NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] + ) return router diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 70e655ac..2ca854d4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -129,7 +129,7 @@ class WirelessRouter(Router, identifier="wireless_router"): """Configuration Schema for WirelessRouter nodes within PrimAITE.""" hostname: str = "WirelessRouter" - airspace: Optional[AirSpace] = None + airspace: AirSpace def __init__(self, **kwargs): super().__init__(**kwargs) @@ -262,9 +262,6 @@ class WirelessRouter(Router, identifier="wireless_router"): :return: WirelessRouter instance. :rtype: WirelessRouter """ - operating_state = ( - NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] - ) router = cls(config=cls.ConfigSchema(**config)) if "router_interface" in config: ip_address = config["router_interface"]["ip_address"] @@ -297,4 +294,7 @@ class WirelessRouter(Router, identifier="wireless_router"): next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric", 0)), ) + router.operating_state = ( + NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] + ) return router diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 3eeda4b6..46fb66a6 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -208,7 +208,7 @@ class NMAP(Application, identifier="NMAP"): if show: table = PrettyTable(["IP Address", "Can Ping"]) table.align = "l" - table.title = f"{self.software_manager.node.hostname} NMAP Ping Scan" + table.title = f"{self.software_manager.node.config.hostname} NMAP Ping Scan" ip_addresses = self._explode_ip_address_network_array(target_ip_address) @@ -367,7 +367,7 @@ class NMAP(Application, identifier="NMAP"): if show: table = PrettyTable(["IP Address", "Port", "Protocol"]) table.align = "l" - table.title = f"{self.software_manager.node.hostname} NMAP Port Scan ({scan_type})" + table.title = f"{self.software_manager.node.config.hostname} NMAP Port Scan ({scan_type})" self.sys_log.info(f"{self.name}: Starting port scan") for ip_address in ip_addresses: # Prevent port scan on this node diff --git a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py index a34d430b..6da801d4 100644 --- a/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py +++ b/tests/e2e_integration_tests/action_masking/test_agents_use_action_masks.py @@ -12,6 +12,7 @@ from sb3_contrib import MaskablePPO from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.session.ray_envs import PrimaiteRayEnv, PrimaiteRayMARLEnv +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from tests import TEST_ASSETS_ROOT CFG_PATH = TEST_ASSETS_ROOT / "configs/test_primaite_session.yaml" diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 4af1b748..4cf45706 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -36,8 +36,8 @@ class SuperComputer(HostNode, identifier="supercomputer"): SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} - def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): + def __init__(self, **kwargs): print("--- Extended Component: SuperComputer ---") - super().__init__(ip_address=ip_address, subnet_mask=subnet_mask, **kwargs) + super().__init__(**kwargs) pass diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py index 36c77fe1..32193946 100644 --- a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -16,7 +16,7 @@ def test_wireless_link_loading(wireless_wan_network): # Configure Router 2 ACLs router_2.acl.add_rule(action=ACLAction.PERMIT, position=1) - airspace = router_1.airspace + airspace = router_1.config.airspace client.software_manager.install(FTPClient) ftp_client: FTPClient = client.software_manager.software.get("FTPClient") diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 69f3e5ab..131abe78 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -83,12 +83,15 @@ def dmz_external_internal_network() -> Network: ) # external node - external_node = Computer( - hostname="external_node", - ip_address="192.168.10.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.10.1", - start_up_duration=0, + external_node: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "external_node", + "ip_address": "192.168.10.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.10.1", + "start_up_duration": 0, + } ) external_node.power_on() external_node.software_manager.install(NTPServer) @@ -98,12 +101,15 @@ def dmz_external_internal_network() -> Network: network.connect(endpoint_b=external_node.network_interface[1], endpoint_a=firewall_node.external_port) # internal node - internal_node = Computer( - hostname="internal_node", - ip_address="192.168.0.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.0.1", - start_up_duration=0, + internal_node: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "internal_node", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.0.1", + "start_up_duration": 0, + } ) internal_node.power_on() internal_node.software_manager.install(NTPClient) @@ -114,12 +120,15 @@ def dmz_external_internal_network() -> Network: network.connect(endpoint_b=internal_node.network_interface[1], endpoint_a=firewall_node.internal_port) # dmz node - dmz_node = Computer( - hostname="dmz_node", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, + dmz_node: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "dmz_node", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } ) dmz_node.power_on() dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"] @@ -157,9 +166,9 @@ def test_nodes_can_ping_default_gateway(dmz_external_internal_network): internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node") dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node") - assert internal_node.ping(internal_node.default_gateway) # default gateway internal - assert dmz_node.ping(dmz_node.default_gateway) # default gateway dmz - assert external_node.ping(external_node.default_gateway) # default gateway external + assert internal_node.ping(internal_node.config.default_gateway) # default gateway internal + assert dmz_node.ping(dmz_node.config.default_gateway) # default gateway dmz + assert external_node.ping(external_node.config.default_gateway) # default gateway external def test_nodes_can_ping_default_gateway_on_another_subnet(dmz_external_internal_network): @@ -173,14 +182,14 @@ def test_nodes_can_ping_default_gateway_on_another_subnet(dmz_external_internal_ internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node") dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node") - assert internal_node.ping(external_node.default_gateway) # internal node to external default gateway - assert internal_node.ping(dmz_node.default_gateway) # internal node to dmz default gateway + assert internal_node.ping(external_node.config.default_gateway) # internal node to external default gateway + assert internal_node.ping(dmz_node.config.default_gateway) # internal node to dmz default gateway - assert dmz_node.ping(internal_node.default_gateway) # dmz node to internal default gateway - assert dmz_node.ping(external_node.default_gateway) # dmz node to external default gateway + assert dmz_node.ping(internal_node.config.default_gateway) # dmz node to internal default gateway + assert dmz_node.ping(external_node.config.default_gateway) # dmz node to external default gateway - assert external_node.ping(external_node.default_gateway) # external node to internal default gateway - assert external_node.ping(dmz_node.default_gateway) # external node to dmz default gateway + assert external_node.ping(external_node.config.default_gateway) # external node to internal default gateway + assert external_node.ping(dmz_node.config.default_gateway) # external node to dmz default gateway def test_nodes_can_ping_each_other(dmz_external_internal_network): diff --git a/tests/integration_tests/network/test_frame_transmission.py b/tests/integration_tests/network/test_frame_transmission.py index cff99e07..6a514bdc 100644 --- a/tests/integration_tests/network/test_frame_transmission.py +++ b/tests/integration_tests/network/test_frame_transmission.py @@ -10,25 +10,31 @@ def test_node_to_node_ping(): """Tests two Computers are able to ping each other.""" network = Network() - client_1 = Computer( - hostname="client_1", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, + client_1: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "client_1", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } ) client_1.power_on() - server_1 = Server( - hostname="server_1", - ip_address="192.168.1.11", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, + server_1: Server = Server.from_config( + config={ + "type": "server", + "hostname": "server_1", + "ip_address": "192.168.1.11", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } ) server_1.power_on() - switch_1 = Switch(hostname="switch_1", start_up_duration=0) + switch_1: Switch = Switch.from_config(config={"type": "switch", "hostname": "switch_1", "start_up_duration": 0}) switch_1.power_on() network.connect(endpoint_a=client_1.network_interface[1], endpoint_b=switch_1.network_interface[1]) @@ -41,14 +47,38 @@ def test_multi_nic(): """Tests that Computers with multiple NICs can ping each other and the data go across the correct links.""" network = Network() - node_a = Computer(config=dict(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0)) + node_a: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.10", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node_a.power_on() - node_b = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node_b: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_b", + "ip_address": "192.168.0.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node_b.power_on() node_b.connect_nic(NIC(ip_address="10.0.0.12", subnet_mask="255.0.0.0")) - node_c = Computer(hostname="node_c", ip_address="10.0.0.13", subnet_mask="255.0.0.0", start_up_duration=0) + node_c: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_c", + "ip_address": "10.0.0.13", + "subnet_mask": "255.0.0.0", + "start_up_duration": 0, + } + ) node_c.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) diff --git a/tests/integration_tests/network/test_multi_lan_internet_example_network.py b/tests/integration_tests/network/test_multi_lan_internet_example_network.py index ea7e1c45..897b4008 100644 --- a/tests/integration_tests/network/test_multi_lan_internet_example_network.py +++ b/tests/integration_tests/network/test_multi_lan_internet_example_network.py @@ -1,6 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.networks import multi_lan_internet_network_example from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser diff --git a/tests/integration_tests/network/test_network_creation.py b/tests/integration_tests/network/test_network_creation.py index 1ee3ccc2..4d88eac3 100644 --- a/tests/integration_tests/network/test_network_creation.py +++ b/tests/integration_tests/network/test_network_creation.py @@ -27,7 +27,15 @@ def test_network(example_network): def test_adding_removing_nodes(): """Check that we can create and add a node to a network.""" net = Network() - n1 = Computer(hostname="computer", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + n1 = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) net.add_node(n1) assert n1.parent is net assert n1 in net @@ -37,10 +45,18 @@ def test_adding_removing_nodes(): assert n1 not in net -def test_readding_node(): - """Check that warning is raised when readding a node.""" +def test_reading_node(): + """Check that warning is raised when reading a node.""" net = Network() - n1 = Computer(hostname="computer", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + n1 = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) net.add_node(n1) net.add_node(n1) assert n1.parent is net @@ -50,7 +66,15 @@ def test_readding_node(): def test_removing_nonexistent_node(): """Check that warning is raised when trying to remove a node that is not in the network.""" net = Network() - n1 = Computer(hostname="computer1", ip_address="192.168.1.1", subnet_mask="255.255.255.0", start_up_duration=0) + n1 = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer1", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) net.remove_node(n1) assert n1.parent is None assert n1 not in net @@ -59,8 +83,24 @@ def test_removing_nonexistent_node(): def test_connecting_nodes(): """Check that two nodes on the network can be connected.""" net = Network() - n1 = Computer(hostname="computer1", ip_address="192.168.1.1", subnet_mask="255.255.255.0", start_up_duration=0) - n2 = Computer(hostname="computer2", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + n1: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer1", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) + n2: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer2", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) net.add_node(n1) net.add_node(n2) @@ -75,7 +115,15 @@ def test_connecting_nodes(): def test_connecting_node_to_itself_fails(): net = Network() - node = Computer(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0) + node = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_b", + "ip_address": "192.168.0.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node.power_on() node.connect_nic(NIC(ip_address="10.0.0.12", subnet_mask="255.0.0.0")) @@ -92,8 +140,24 @@ def test_connecting_node_to_itself_fails(): def test_disconnecting_nodes(): net = Network() - n1 = Computer(hostname="computer1", ip_address="192.168.1.1", subnet_mask="255.255.255.0", start_up_duration=0) - n2 = Computer(hostname="computer2", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) + n1 = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer1", + "ip_address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) + n2 = Computer.from_config( + config={ + "type": "computer", + "hostname": "computer2", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) net.connect(n1.network_interface[1], n2.network_interface[1]) assert len(net.links) == 1 diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 948b409f..b60f3f6b 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -15,25 +15,31 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def pc_a_pc_b_router_1() -> Tuple[Computer, Computer, Router]: network = Network() - pc_a = Computer( - hostname="pc_a", - ip_address="192.168.0.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.0.1", - start_up_duration=0, + pc_a = Computer.from_config( + config={ + "type": "computer", + "hostname": "pc_a", + "ip_address": "192.168.0.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.0.1", + "start_up_duration": 0, + } ) pc_a.power_on() - pc_b = Computer( - hostname="pc_b", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, + pc_b = Computer.from_config( + config={ + "type": "computer", + "hostname": "pc_b", + "ip_address": "192.168.1.10", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } ) pc_b.power_on() - router_1 = Router(hostname="router_1", start_up_duration=0) + router_1 = Router.from_config(config={"type": "router", "hostname": "router_1", "start_up_duration": 0}) router_1.power_on() router_1.configure_port(1, "192.168.0.1", "255.255.255.0") @@ -52,18 +58,21 @@ def multi_hop_network() -> Network: network = Network() # Configure PC A - pc_a = Computer( - hostname="pc_a", - ip_address="192.168.0.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.0.1", - start_up_duration=0, + pc_a: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "pc_a", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.0.1", + "start_up_duration": 0, + } ) pc_a.power_on() network.add_node(pc_a) # Configure Router 1 - router_1 = Router(hostname="router_1", start_up_duration=0) + router_1: Router = Router.from_config(config={"type": "router", "hostname": "router_1", "start_up_duration": 0}) router_1.power_on() network.add_node(router_1) @@ -79,18 +88,21 @@ def multi_hop_network() -> Network: router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Configure PC B - pc_b = Computer( - hostname="pc_b", - ip_address="192.168.2.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.2.1", - start_up_duration=0, + pc_b: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "pc_b", + "ip_address": "192.168.2.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.2.1", + "start_up_duration": 0, + } ) pc_b.power_on() network.add_node(pc_b) # Configure Router 2 - router_2 = Router(hostname="router_2", start_up_duration=0) + router_2: Router = Router.from_config(config={"type": "router", "hostname": "router_2", "start_up_duration": 0}) router_2.power_on() network.add_node(router_2) @@ -113,13 +125,13 @@ def multi_hop_network() -> Network: def test_ping_default_gateway(pc_a_pc_b_router_1): pc_a, pc_b, router_1 = pc_a_pc_b_router_1 - assert pc_a.ping(pc_a.default_gateway) + assert pc_a.ping(pc_a.config.default_gateway) def test_ping_other_router_port(pc_a_pc_b_router_1): pc_a, pc_b, router_1 = pc_a_pc_b_router_1 - assert pc_a.ping(pc_b.default_gateway) + assert pc_a.ping(pc_b.config.default_gateway) def test_host_on_other_subnet(pc_a_pc_b_router_1): diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index 26e50f4a..487736e7 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -17,18 +17,23 @@ def wireless_wan_network(): network = Network() # Configure PC A - pc_a = Computer( - hostname="pc_a", - ip_address="192.168.0.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.0.1", - start_up_duration=0, + pc_a = Computer.from_config( + config={ + "type": "computer", + "hostname": "pc_a", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.0.1", + "start_up_duration": 0, + } ) pc_a.power_on() network.add_node(pc_a) # Configure Router 1 - router_1 = WirelessRouter(hostname="router_1", start_up_duration=0, airspace=network.airspace) + router_1 = WirelessRouter.from_config( + config={"type": "wireless_router", "hostname": "router_1", "start_up_duration": 0, "airspace": network.airspace} + ) router_1.power_on() network.add_node(router_1) @@ -43,18 +48,23 @@ def wireless_wan_network(): router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Configure PC B - pc_b = Computer( - hostname="pc_b", - ip_address="192.168.2.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.2.1", - start_up_duration=0, + pc_b: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "pc_b", + "ip_address": "192.168.2.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.2.1", + "start_up_duration": 0, + } ) pc_b.power_on() network.add_node(pc_b) # Configure Router 2 - router_2 = WirelessRouter(hostname="router_2", start_up_duration=0, airspace=network.airspace) + router_2: WirelessRouter = WirelessRouter.from_config( + config={"type": "wireless_router", "hostname": "router_2", "start_up_duration": 0, "airspace": network.airspace} + ) router_2.power_on() network.add_node(router_2) @@ -98,8 +108,8 @@ def wireless_wan_network_from_config_yaml(): def test_cross_wireless_wan_connectivity(wireless_wan_network): pc_a, pc_b, router_1, router_2 = wireless_wan_network # Ensure that PCs can ping across routers before any frequency change - assert pc_a.ping(pc_a.default_gateway), "PC A should ping its default gateway successfully." - assert pc_b.ping(pc_b.default_gateway), "PC B should ping its default gateway successfully." + assert pc_a.ping(pc_a.config.default_gateway), "PC A should ping its default gateway successfully." + assert pc_b.ping(pc_b.config.default_gateway), "PC B should ping its default gateway successfully." assert pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should ping PC B across routers successfully." assert pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should ping PC A across routers successfully." @@ -109,8 +119,8 @@ def test_cross_wireless_wan_connectivity_from_yaml(wireless_wan_network_from_con pc_a = wireless_wan_network_from_config_yaml.get_node_by_hostname("pc_a") pc_b = wireless_wan_network_from_config_yaml.get_node_by_hostname("pc_b") - assert pc_a.ping(pc_a.default_gateway), "PC A should ping its default gateway successfully." - assert pc_b.ping(pc_b.default_gateway), "PC B should ping its default gateway successfully." + assert pc_a.ping(pc_a.config.default_gateway), "PC A should ping its default gateway successfully." + assert pc_b.ping(pc_b.config.default_gateway), "PC B should ping its default gateway successfully." assert pc_a.ping(pc_b.network_interface[1].ip_address), "PC A should ping PC B across routers successfully." assert pc_b.ping(pc_a.network_interface[1].ip_address), "PC B should ping PC A across routers successfully." diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index fc7aa69c..e1795a36 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -10,13 +10,16 @@ from primaite.simulator.system.applications.application import Application, Appl @pytest.fixture(scope="function") def populated_node(application_class) -> Tuple[Application, Computer]: - computer: Computer = Computer( - hostname="test_computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - shut_down_duration=0, + computer: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "test_computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + "shut_down_duration": 0, + } ) computer.power_on() computer.software_manager.install(application_class) @@ -29,13 +32,16 @@ def populated_node(application_class) -> Tuple[Application, Computer]: def test_application_on_offline_node(application_class): """Test to check that the application cannot be interacted with when node it is on is off.""" - computer: Computer = Computer( - hostname="test_computer", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - shut_down_duration=0, + computer: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "test_computer", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + "shut_down_duration": 0, + } ) computer.software_manager.install(application_class) diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 64b6ddbc..8ad292b2 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -20,11 +20,27 @@ from primaite.simulator.system.software import SoftwareHealthState @pytest.fixture(scope="function") def peer_to_peer() -> Tuple[Computer, Computer]: network = Network() - node_a: Computer = Computer.from_config(config={"type":"computer", "hostname":"node_a", "ip_address":"192.168.0.10", "subnet_mask":"255.255.255.0", "start_up_duration":0}) + node_a: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.10", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node_a.power_on() node_a.software_manager.get_open_ports() - node_b: Computer = Computer.from_config(config={"type":"computer", "hostname":"node_b", "ip_address":"192.168.0.11", "subnet_mask":"255.255.255.0", "start_up_duration":0}) + node_b: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_b", + "ip_address": "192.168.0.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) @@ -412,8 +428,14 @@ def test_database_service_can_terminate_connection(peer_to_peer): def test_client_connection_terminate_does_not_terminate_another_clients_connection(): network = Network() - db_server: Server = Server.from_config(config={"type":"server", - "hostname":"db_client", "ip_address":"192.168.0.11", "subnet_mask":"255.255.255.0", "start_up_duration":0} + db_server: Server = Server.from_config( + config={ + "type": "server", + "hostname": "db_client", + "ip_address": "192.168.0.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } ) db_server.power_on() @@ -421,8 +443,14 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] # noqa db_service.start() - client_a = Computer( - hostname="client_a", ip_address="192.168.0.12", subnet_mask="255.255.255.0", start_up_duration=0 + client_a = Computer.from_config( + config={ + "type": "computer", + "hostname": "client_a", + "ip_address": "192.168.0.12", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } ) client_a.power_on() @@ -430,8 +458,14 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti client_a.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11")) client_a.software_manager.software["DatabaseClient"].run() - client_b = Computer( - hostname="client_b", ip_address="192.168.0.13", subnet_mask="255.255.255.0", start_up_duration=0 + client_b = Computer.from_config( + config={ + "type": "computer", + "hostname": "client_b", + "ip_address": "192.168.0.13", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } ) client_b.power_on() @@ -439,7 +473,7 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti client_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11")) client_b.software_manager.software["DatabaseClient"].run() - switch = Switch(hostname="switch", start_up_duration=0, num_ports=3) + switch = Switch.from_config(config={"type": "switch", "hostname": "switch", "start_up_duration": 0, "num_ports": 3}) switch.power_on() network.connect(endpoint_a=switch.network_interface[1], endpoint_b=db_server.network_interface[1]) @@ -465,6 +499,14 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti def test_database_server_install_ftp_client(): - server: Server = Server.from_config(config={"type":"server", "hostname":"db_server", "ip_address":"192.168.1.2", "subnet_mask":"255.255.255.0", "start_up_duration":0}) + server: Server = Server.from_config( + config={ + "type": "server", + "hostname": "db_server", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) server.software_manager.install(DatabaseService) assert server.software_manager.software.get("FTPClient") diff --git a/tests/integration_tests/system/test_user_session_manager_logins.py b/tests/integration_tests/system/test_user_session_manager_logins.py index 0c591a4b..9736232b 100644 --- a/tests/integration_tests/system/test_user_session_manager_logins.py +++ b/tests/integration_tests/system/test_user_session_manager_logins.py @@ -14,21 +14,27 @@ from primaite.simulator.network.hardware.nodes.host.server import Server def client_server_network() -> Tuple[Computer, Server, Network]: network = Network() - client = Computer( - hostname="client", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, + client = Computer.from_config( + config={ + "type": "computer", + "hostname": "client", + "ip_address": "192.168.1.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } ) client.power_on() - server = Server( - hostname="server", - ip_address="192.168.1.3", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, + server = Server.from_config( + config={ + "type": "server", + "hostname": "server", + "ip_address": "192.168.1.3", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.1.1", + "start_up_duration": 0, + } ) server.power_on() diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index efc97ce6..960ef50f 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -111,7 +111,7 @@ def test_request_fails_if_node_off(example_network, node_request): """Test that requests succeed when the node is on, and fail if the node is off.""" net = example_network client_1: HostNode = net.get_node_by_hostname("client_1") - client_1.shut_down_duration = 0 + client_1.config.shut_down_duration = 0 assert client_1.operating_state == NodeOperatingState.ON resp_1 = net.apply_request(node_request) diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index f6308a21..0b307fe5 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -17,11 +17,13 @@ def node() -> Node: "hostname": "test", "ip_address": "192.168.1.2", "subnet_mask": "255.255.255.0", + "operating_state": "OFF", } computer = Computer.from_config(config=computer_cfg) return computer + def test_node_startup(node): assert node.operating_state == NodeOperatingState.OFF node.apply_request(["startup"]) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 430e3835..914d64e1 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -36,6 +36,16 @@ def test_create_dns_client(dns_client): def test_dns_client_add_domain_to_cache_when_not_running(dns_client): dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") + + # shutdown the dns_client + dns_client.power_off() + + # wait for dns_client to turn off + idx = 0 + while dns_client.operating_state == NodeOperatingState.SHUTTING_DOWN: + dns_client.apply_timestep(idx) + idx += 1 + assert dns_client.operating_state is NodeOperatingState.OFF assert dns_client_service.operating_state is ServiceOperatingState.STOPPED diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index fd193415..6f8664b2 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -66,7 +66,7 @@ def test_dns_server_receive(dns_server): } client = Computer.from_config(config=client_cfg) client.power_on() - client.dns_server = IPv4Address("192.168.1.10") + client.config.dns_server = IPv4Address("192.168.1.10") network = Network() network.connect(dns_server.network_interface[1], client.network_interface[1]) dns_client: DNSClient = client.software_manager.software["DNSClient"] # noqa diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 1666f008..e5fe2013 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -12,6 +12,7 @@ from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter +from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.protocols.ssh import ( SSHConnectionMessage, SSHPacket, @@ -29,8 +30,14 @@ from primaite.utils.validation.port import PORT_LOOKUP @pytest.fixture(scope="function") def terminal_on_computer() -> Tuple[Terminal, Computer]: - computer: Computer = Computer.from_config(config={"type":"computer", - "hostname":"node_a", "ip_address":"192.168.0.10", "subnet_mask":"255.255.255.0", "start_up_duration":0} + computer: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.10", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } ) computer.power_on() terminal: Terminal = computer.software_manager.software.get("Terminal") @@ -41,19 +48,27 @@ def terminal_on_computer() -> Tuple[Terminal, Computer]: @pytest.fixture(scope="function") def basic_network() -> Network: network = Network() - node_a = Computer.from_config(config={"type":"computer", - "hostname":"node_a", - "ip_address":"192.168.0.10", - "subnet_mask":"255.255.255.0", - "start_up_duration":0}) + node_a = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.10", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node_a.power_on() node_a.software_manager.get_open_ports() - node_b = Computer.from_config(config={"type":"computer", - "hostname":"node_b", - "ip_address":"192.168.0.11", - "subnet_mask":"255.255.255.0", - "start_up_duration":0}) + node_b = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_b", + "ip_address": "192.168.0.11", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) @@ -65,20 +80,23 @@ def wireless_wan_network(): network = Network() # Configure PC A - pc_a_cfg = {"type": "computer", - "hostname":"pc_a", - "ip_address":"192.168.0.2", - "subnet_mask":"255.255.255.0", - "default_gateway":"192.168.0.1", - "start_up_duration":0, - } + pc_a_cfg = { + "type": "computer", + "hostname": "pc_a", + "ip_address": "192.168.0.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.0.1", + "start_up_duration": 0, + } pc_a = Computer.from_config(config=pc_a_cfg) pc_a.power_on() network.add_node(pc_a) # Configure Router 1 - router_1 = WirelessRouter.from_config(config={"type":"wireless_router", "hostname":"router_1", "start_up_duration":0, "airspace":network.airspace}) + router_1 = WirelessRouter.from_config( + config={"type": "wireless_router", "hostname": "router_1", "start_up_duration": 0, "airspace": network.airspace} + ) router_1.power_on() network.add_node(router_1) @@ -99,43 +117,29 @@ def wireless_wan_network(): # Configure PC B - pc_b_cfg = {"type": "computer", - "hostname":"pc_b", - "ip_address":"192.168.2.2", - "subnet_mask":"255.255.255.0", - "default_gateway":"192.168.2.1", - "start_up_duration":0, - } + pc_b_cfg = { + "type": "computer", + "hostname": "pc_b", + "ip_address": "192.168.2.2", + "subnet_mask": "255.255.255.0", + "default_gateway": "192.168.2.1", + "start_up_duration": 0, + } pc_b = Computer.from_config(config=pc_b_cfg) pc_b.power_on() network.add_node(pc_b) - # Configure Router 2 - router_2 = WirelessRouter.from_config(config={"type":"wireless_router", "hostname":"router_2", "start_up_duration":0, "airspace":network.airspace}) - router_2.power_on() - network.add_node(router_2) - - # Configure the connection between PC B and Router 2 port 2 - router_2.configure_router_interface("192.168.2.1", "255.255.255.0") - network.connect(pc_b.network_interface[1], router_2.network_interface[2]) - # Configure Router 2 ACLs # Configure the wireless connection between Router 1 port 1 and Router 2 port 1 router_1.configure_wireless_access_point("192.168.1.1", "255.255.255.0") - router_2.configure_wireless_access_point("192.168.1.2", "255.255.255.0") router_1.route_table.add_route( address="192.168.2.0", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.2" ) - # Configure Route from Router 2 to PC A subnet - router_2.route_table.add_route( - address="192.168.0.2", subnet_mask="255.255.255.0", next_hop_ip_address="192.168.1.1" - ) - - return pc_a, pc_b, router_1, router_2 + return network @pytest.fixture @@ -156,11 +160,15 @@ def test_terminal_creation(terminal_on_computer): def test_terminal_install_default(): """Terminal should be auto installed onto Nodes""" - computer: Computer = Computer.from_config(config={"type":"computer", - "hostname":"node_a", - "ip_address":"192.168.0.10", - "subnet_mask":"255.255.255.0", - "start_up_duration":0}) + computer: Computer = Computer.from_config( + config={ + "type": "computer", + "hostname": "node_a", + "ip_address": "192.168.0.10", + "subnet_mask": "255.255.255.0", + "start_up_duration": 0, + } + ) computer.power_on() assert computer.software_manager.software.get("Terminal") @@ -168,7 +176,7 @@ def test_terminal_install_default(): def test_terminal_not_on_switch(): """Ensure terminal does not auto-install to switch""" - test_switch = Switch.from_config(config={"type":"switch", "hostname":"Test"}) + test_switch = Switch.from_config(config={"type": "switch", "hostname": "Test"}) assert not test_switch.software_manager.software.get("Terminal") @@ -291,7 +299,10 @@ def test_terminal_ignores_when_off(basic_network): def test_computer_remote_login_to_router(wireless_wan_network): """Test to confirm that a computer can SSH into a router.""" - pc_a, _, router_1, _ = wireless_wan_network + + pc_a = wireless_wan_network.get_node_by_hostname("pc_a") + + router_1 = wireless_wan_network.get_node_by_hostname("router_1") pc_a_terminal: Terminal = pc_a.software_manager.software.get("Terminal") @@ -310,7 +321,9 @@ def test_computer_remote_login_to_router(wireless_wan_network): def test_router_remote_login_to_computer(wireless_wan_network): """Test to confirm that a router can ssh into a computer.""" - pc_a, _, router_1, _ = wireless_wan_network + pc_a = wireless_wan_network.get_node_by_hostname("pc_a") + + router_1 = wireless_wan_network.get_node_by_hostname("router_1") router_1_terminal: Terminal = router_1.software_manager.software.get("Terminal") @@ -329,7 +342,9 @@ def test_router_remote_login_to_computer(wireless_wan_network): def test_router_blocks_SSH_traffic(wireless_wan_network): """Test to check that router will block SSH traffic if no ACL rule.""" - pc_a, _, router_1, _ = wireless_wan_network + pc_a = wireless_wan_network.get_node_by_hostname("pc_a") + + router_1 = wireless_wan_network.get_node_by_hostname("router_1") # Remove rule that allows SSH traffic. router_1.acl.remove_rule(position=21) @@ -343,20 +358,22 @@ def test_router_blocks_SSH_traffic(wireless_wan_network): assert len(pc_a_terminal._connections) == 0 -def test_SSH_across_network(wireless_wan_network): +def test_SSH_across_network(): """Test to show ability to SSH across a network.""" - pc_a, pc_b, router_1, router_2 = wireless_wan_network + network: Network = arcd_uc2_network() + pc_a = network.get_node_by_hostname("client_1") + router_1 = network.get_node_by_hostname("router_1") terminal_a: Terminal = pc_a.software_manager.software.get("Terminal") - terminal_b: Terminal = pc_b.software_manager.software.get("Terminal") - router_2.acl.add_rule( + router_1.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=21 ) assert len(terminal_a._connections) == 0 - terminal_b_on_terminal_a = terminal_b.login(username="admin", password="admin", ip_address="192.168.0.2") + # Login to the Domain Controller + terminal_a.login(username="admin", password="admin", ip_address="192.168.1.10") assert len(terminal_a._connections) == 1 From 4b42a74ac89fb5f425a86545cc934e54b970da5b Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 29 Jan 2025 16:57:25 +0000 Subject: [PATCH 162/224] #2887 - Corrected failures seen when generating services from config & syntax issues. Wireless Router tests currently fail due to port 1 being disabled on startup --- .../hardware/nodes/network/wireless_router.py | 6 +++--- .../system/services/database/database_service.py | 7 +++---- .../simulator/system/services/dns/dns_client.py | 6 ++++-- .../simulator/system/services/ftp/ftp_server.py | 7 +++++-- .../simulator/system/services/ntp/ntp_client.py | 14 ++++++++------ tests/conftest.py | 1 - .../extensions/services/extended_service.py | 6 +++--- 7 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 2ca854d4..5e52de7e 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -263,6 +263,9 @@ class WirelessRouter(Router, identifier="wireless_router"): :rtype: WirelessRouter """ router = cls(config=cls.ConfigSchema(**config)) + router.operating_state = ( + NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] + ) if "router_interface" in config: ip_address = config["router_interface"]["ip_address"] subnet_mask = config["router_interface"]["subnet_mask"] @@ -294,7 +297,4 @@ class WirelessRouter(Router, identifier="wireless_router"): next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric", 0)), ) - router.operating_state = ( - NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] - ) return router diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 4ba4c4d4..fc56483d 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -31,12 +31,11 @@ class DatabaseService(Service, identifier="DatabaseService"): type: str = "DatabaseService" backup_server_ip: Optional[IPv4Address] = None + db_password: Optional[str] = None + """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" config: "DatabaseService.ConfigSchema" = Field(default_factory=lambda: DatabaseService.ConfigSchema()) - password: Optional[str] = None - """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" - backup_server_ip: IPv4Address = None """IP address of the backup server.""" @@ -217,7 +216,7 @@ class DatabaseService(Service, identifier="DatabaseService"): SoftwareHealthState.FIXING, SoftwareHealthState.COMPROMISED, ]: - if self.password == password: + if self.config.db_password == password: status_code = 200 # ok connection_id = self._generate_connection_id() # try to create connection diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 0756eb05..3ff5b930 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -22,11 +22,13 @@ class DNSClient(Service, identifier="DNSClient"): type: str = "DNSClient" + dns_server: Optional[IPv4Address] = None + "The DNS Server the client sends requests to." + config: "DNSClient.ConfigSchema" = Field(default_factory=lambda: DNSClient.ConfigSchema()) dns_cache: Dict[str, IPv4Address] = {} "A dict of known mappings between domain/URLs names and IPv4 addresses." - dns_server: Optional[IPv4Address] = None - "The DNS Server the client sends requests to." + def __init__(self, **kwargs): kwargs["name"] = "DNSClient" diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 054bfe15..a5b59ec9 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -23,14 +23,17 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): config: "FTPServer.ConfigSchema" = Field(default_factory=lambda: FTPServer.ConfigSchema()) - server_password: Optional[str] = None - """Password needed to connect to FTP server. Default is None.""" + class ConfigSchema(Service.ConfigSchema): """ConfigSchema for FTPServer.""" type: str = "FTPServer" + server_password: Optional[str] = None + """Password needed to connect to FTP server. Default is None.""" + + def __init__(self, **kwargs): kwargs["name"] = "FTPServer" kwargs["port"] = PORT_LOOKUP["FTP"] diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index fb470faf..b27d1241 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -22,10 +22,12 @@ class NTPClient(Service, identifier="NTPClient"): type: str = "NTPClient" + ntp_server_ip: Optional[IPv4Address] = None + "The NTP server the client sends requests to." + config: "NTPClient.ConfigSchema" = Field(default_factory=lambda: NTPClient.ConfigSchema()) - ntp_server: Optional[IPv4Address] = None - "The NTP server the client sends requests to." + time: Optional[datetime] = None def __init__(self, **kwargs): @@ -42,8 +44,8 @@ class NTPClient(Service, identifier="NTPClient"): :param ntp_server_ip_address: IPv4 address of NTP server. :param ntp_client_ip_Address: IPv4 address of NTP client. """ - self.ntp_server = ntp_server_ip_address - self.sys_log.info(f"{self.name}: ntp_server: {self.ntp_server}") + self.config.ntp_server_ip = ntp_server_ip_address + self.sys_log.info(f"{self.name}: ntp_server: {self.config.ntp_server_ip}") def describe_state(self) -> Dict: """ @@ -105,10 +107,10 @@ class NTPClient(Service, identifier="NTPClient"): def request_time(self) -> None: """Send request to ntp_server.""" - if self.ntp_server: + if self.config.ntp_server_ip: self.software_manager.session_manager.receive_payload_from_software_manager( payload=NTPPacket(), - dst_ip_address=self.ntp_server, + dst_ip_address=self.config.ntp_server_ip, src_port=self.port, dst_port=self.port, ip_protocol=self.protocol, diff --git a/tests/conftest.py b/tests/conftest.py index 6ac227ef..765ed8dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -387,7 +387,6 @@ def install_stuff_to_sim(sim: Simulation): "ip_address": "10.0.1.2", "subnet_mask": "255.255.255.0", "default_gateway": "10.0.1.1", - "start_up_duration": 0, } client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index ba247369..11adc53b 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -31,14 +31,14 @@ class ExtendedService(Service, identifier="ExtendedService"): type: str = "ExtendedService" + backup_server_ip: IPv4Address = None + """IP address of the backup server.""" + config: "ExtendedService.ConfigSchema" = Field(default_factory=lambda: ExtendedService.ConfigSchema()) password: Optional[str] = None """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" - backup_server_ip: IPv4Address = None - """IP address of the backup server.""" - latest_backup_directory: str = None """Directory of latest backup.""" From 3d47b9c8638322b537d6346612f71a7a69b309ad Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 30 Jan 2025 17:33:00 +0000 Subject: [PATCH 163/224] #2887 - Further fixes to unit tests --- src/primaite/simulator/network/hardware/base.py | 5 ++++- src/primaite/simulator/system/core/software_manager.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 08e100d2..8cbe2b87 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1532,6 +1532,7 @@ class Node(SimComponent, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) """Configure pydantic to allow arbitrary types, let the instance have attributes not present in the model.""" + hostname: str = "default" "The node hostname on the network." @@ -1568,6 +1569,8 @@ class Node(SimComponent, ABC): default_gateway: Optional[IPV4Address] = None "The default gateway IP address for forwarding network traffic to other networks." + operating_state: Any = None + @property def dns_server(self) -> Optional[IPv4Address]: return self.config.dns_server @@ -1579,7 +1582,6 @@ class Node(SimComponent, ABC): msg = f"Configuration contains an invalid Node type: {config['type']}" return ValueError(msg) obj = cls(config=cls.ConfigSchema(**config)) - obj.operating_state = NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] return obj def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: @@ -1623,6 +1625,7 @@ class Node(SimComponent, ABC): dns_server=kwargs["config"].dns_server, ) super().__init__(**kwargs) + self.operating_state = NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else NodeOperatingState[p.upper()] self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index ddb30a3b..0f7aa936 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -140,6 +140,7 @@ class SoftwareManager: elif isinstance(software, Service): self.node.services[software.uuid] = software self.node._service_request_manager.add_request(software.name, RequestType(func=software._request_manager)) + software.start() software.install() software.software_manager = self self.software[software.name] = software From 4fb54c9492d79b6dfe6b76784e3914a084bcb7ec Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 Jan 2025 12:18:52 +0000 Subject: [PATCH 164/224] #3029 - Add string-ip validator, improve validation, fix minor bugs in pulling schema data --- src/primaite/game/agent/actions/software.py | 21 ++++--- .../agent/observations/acl_observation.py | 22 +++---- .../observations/firewall_observation.py | 21 ++++--- .../agent/observations/node_observations.py | 7 ++- .../agent/observations/router_observation.py | 9 ++- .../agent/scripted_agents/random_agent.py | 6 ++ src/primaite/simulator/network/creation.py | 3 +- .../red_applications/c2/c2_beacon.py | 4 +- .../applications/red_applications/dos_bot.py | 8 +-- .../services/database/database_service.py | 2 + .../system/services/dns/dns_client.py | 60 +++++++++++-------- .../system/services/dns/dns_server.py | 1 + .../system/services/ftp/ftp_server.py | 7 +-- .../system/services/ntp/ntp_client.py | 3 + src/primaite/utils/validation/ipv4_address.py | 9 +++ .../configs/basic_switched_network.yaml | 2 - tests/assets/configs/extended_config.yaml | 2 - ...software_installation_and_configuration.py | 1 - .../test_c2_suite_integration.py | 4 +- 19 files changed, 116 insertions(+), 76 deletions(-) diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index e0d602ed..81a3a315 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -6,6 +6,9 @@ from pydantic import ConfigDict, Field from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.ipv4_address import StrIP +from primaite.utils.validation.port import Port __all__ = ( "ConfigureRansomwareScriptAction", @@ -64,8 +67,8 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): model_config = ConfigDict(extra="forbid") node_name: str - target_ip_address: Optional[str] = None - target_port: Optional[str] = None + target_ip_address: Optional[StrIP] = None + target_port: Optional[Port] = None payload: Optional[str] = None repeat: Optional[bool] = None port_scan_p_of_success: Optional[float] = None @@ -95,10 +98,10 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): """Configuration schema for ConfigureC2BeaconAction.""" node_name: str - c2_server_ip_address: str + c2_server_ip_address: StrIP keep_alive_frequency: int = Field(default=5, ge=1) - masquerade_protocol: str = Field(default="TCP") - masquerade_port: str = Field(default="HTTP") + masquerade_protocol: IPProtocol = Field(default="tcp") + masquerade_port: Port = Field(default=80) @classmethod def form_request(self, config: ConfigSchema) -> RequestFormat: @@ -121,7 +124,7 @@ class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_c """Configuration schema for NodeSendRemoteCommandAction.""" node_name: str - remote_ip: str + remote_ip: StrIP command: RequestFormat @classmethod @@ -149,7 +152,7 @@ class TerminalC2ServerAction(AbstractAction, identifier="c2_server_terminal_comm node_name: str commands: Union[List[RequestFormat], RequestFormat] - ip_address: Optional[str] + ip_address: Optional[StrIP] username: Optional[str] password: Optional[str] @@ -198,7 +201,7 @@ class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfi node_name: str username: Optional[str] password: Optional[str] - target_ip_address: str + target_ip_address: StrIP target_file_name: str target_folder_name: str exfiltration_folder_name: Optional[str] @@ -229,7 +232,7 @@ class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_databa """Schema for options that can be passed to this action.""" node_name: str - server_ip_address: Optional[str] = None + server_ip_address: Optional[StrIP] = None server_password: Optional[str] = None @classmethod diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index cb2cb38e..fde49a6b 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -1,7 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from ipaddress import IPv4Address from typing import Dict, List, Optional from gymnasium import spaces @@ -10,6 +9,9 @@ from gymnasium.core import ObsType from primaite import getLogger from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.ipv4_address import StrIP +from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) @@ -20,13 +22,13 @@ class ACLObservation(AbstractObservation, identifier="ACL"): class ConfigSchema(AbstractObservation.ConfigSchema): """Configuration schema for ACLObservation.""" - ip_list: Optional[List[IPv4Address]] = None + ip_list: Optional[List[StrIP]] = None """List of IP addresses.""" wildcard_list: Optional[List[str]] = None """List of wildcard strings.""" - port_list: Optional[List[str]] = None + port_list: Optional[List[Port]] = None """List of port names.""" - protocol_list: Optional[List[str]] = None + protocol_list: Optional[List[IPProtocol]] = None """List of protocol names.""" num_rules: Optional[int] = None """Number of ACL rules.""" @@ -35,10 +37,10 @@ class ACLObservation(AbstractObservation, identifier="ACL"): self, where: WhereType, num_rules: int, - ip_list: List[IPv4Address], + ip_list: List[StrIP], wildcard_list: List[str], - port_list: List[str], - protocol_list: List[str], + port_list: List[Port], + protocol_list: List[IPProtocol], ) -> None: """ Initialise an ACL observation instance. @@ -48,13 +50,13 @@ class ACLObservation(AbstractObservation, identifier="ACL"): :param num_rules: Number of ACL rules. :type num_rules: int :param ip_list: List of IP addresses. - :type ip_list: List[IPv4Address] + :type ip_list: List[StrIP] :param wildcard_list: List of wildcard strings. :type wildcard_list: List[str] :param port_list: List of port names. - :type port_list: List[str] + :type port_list: List[Port] :param protocol_list: List of protocol names. - :type protocol_list: List[str] + :type protocol_list: List[IPProtocol] """ self.where = where self.num_rules: int = num_rules diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index 44541f24..6e5fffb9 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -11,6 +11,9 @@ from primaite.game.agent.observations.acl_observation import ACLObservation from primaite.game.agent.observations.nic_observations import PortObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.ipv4_address import StrIP +from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) @@ -23,13 +26,13 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): hostname: str """Hostname of the firewall node, used for querying simulation state dictionary.""" - ip_list: Optional[List[str]] = None + ip_list: Optional[List[StrIP]] = None """List of IP addresses for encoding ACLs.""" wildcard_list: Optional[List[str]] = None """List of IP wildcards for encoding ACLs.""" - port_list: Optional[List[str]] = None + port_list: Optional[List[Port]] = None """List of ports for encoding ACLs.""" - protocol_list: Optional[List[str]] = None + protocol_list: Optional[List[IPProtocol]] = None """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" @@ -39,10 +42,10 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): def __init__( self, where: WhereType, - ip_list: List[str], + ip_list: List[StrIP], wildcard_list: List[str], - port_list: List[str], - protocol_list: List[str], + port_list: List[Port], + protocol_list: List[IPProtocol], num_rules: int, include_users: bool, ) -> None: @@ -53,13 +56,13 @@ class FirewallObservation(AbstractObservation, identifier="FIREWALL"): A typical location for a firewall might be ['network', 'nodes', ]. :type where: WhereType :param ip_list: List of IP addresses. - :type ip_list: List[str] + :type ip_list: List[StrIP] :param wildcard_list: List of wildcard rules. :type wildcard_list: List[str] :param port_list: List of port names. - :type port_list: List[str] + :type port_list: List[Port] :param protocol_list: List of protocol types. - :type protocol_list: List[str] + :type protocol_list: List[IPProtocol] :param num_rules: Number of rules configured in the firewall. :type num_rules: int :param include_users: If True, report user session information. diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 0c5d11da..1a0f48b4 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -13,6 +13,7 @@ from primaite.game.agent.observations.host_observations import HostObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.observations.router_observation import RouterObservation from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.ipv4_address import StrIP from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) @@ -52,13 +53,13 @@ class NodesObservation(AbstractObservation, identifier="NODES"): """If True, report user session information.""" num_ports: Optional[int] = None """Number of ports.""" - ip_list: Optional[List[str]] = None + ip_list: Optional[List[StrIP]] = None """List of IP addresses for encoding ACLs.""" wildcard_list: Optional[List[str]] = None """List of IP wildcards for encoding ACLs.""" - port_list: Optional[List[str]] = None + port_list: Optional[List[Port]] = None """List of ports for encoding ACLs.""" - protocol_list: Optional[List[str]] = None + protocol_list: Optional[List[IPProtocol]] = None """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index 9687d083..ab759779 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -11,6 +11,9 @@ from primaite.game.agent.observations.acl_observation import ACLObservation from primaite.game.agent.observations.nic_observations import PortObservation from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.utils.validation.ip_protocol import IPProtocol +from primaite.utils.validation.ipv4_address import StrIP +from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) @@ -29,13 +32,13 @@ class RouterObservation(AbstractObservation, identifier="ROUTER"): """Number of port observations configured for this router.""" acl: Optional[ACLObservation.ConfigSchema] = None """Configuration of ACL observation on this router.""" - ip_list: Optional[List[str]] = None + ip_list: Optional[List[StrIP]] = None """List of IP addresses for encoding ACLs.""" wildcard_list: Optional[List[str]] = None """List of IP wildcards for encoding ACLs.""" - port_list: Optional[List[str]] = None + port_list: Optional[List[Port]] = None """List of ports for encoding ACLs.""" - protocol_list: Optional[List[str]] = None + protocol_list: Optional[List[IPProtocol]] = None """List of protocols for encoding ACLs.""" num_rules: Optional[int] = None """Number of rules ACL rules to show.""" diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 9d82a063..9cf8e798 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -83,6 +83,12 @@ class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): next_execution_timestep: int = 0 """Timestep of the next action execution by the agent.""" + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._set_next_execution_timestep( + timestep=self.config.agent_settings.start_step, variance=self.config.agent_settings.start_variance + ) + @computed_field @cached_property def start_node(self) -> str: diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index ebd17638..2cf8774e 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Literal, Optional, Type -from pydantic import BaseModel, model_validator +from pydantic import BaseModel, ConfigDict, model_validator from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -44,6 +44,7 @@ class NetworkNodeAdder(BaseModel): by the from_config method to select the correct node adder at runtime. """ + model_config = ConfigDict(extra="forbid") type: str """Uniquely identifies the node adder class to use for adding nodes to network.""" diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 13918cd7..b989671e 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -127,8 +127,8 @@ class C2Beacon(AbstractC2, identifier="C2Beacon"): self.configure( c2_server_ip_address=c2_remote_ip, keep_alive_frequency=frequency, - masquerade_protocol=PROTOCOL_LOOKUP[protocol], - masquerade_port=PORT_LOOKUP[port], + masquerade_protocol=protocol, + masquerade_port=port, ) ) diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index ea7a4d8d..a6cb2b75 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -10,8 +10,8 @@ from primaite.game.science import simulate_trial from primaite.interface.request import RequestFormat, RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.utils.validation.ipv4_address import IPV4Address -from primaite.utils.validation.port import Port, PORT_LOOKUP +from primaite.utils.validation.ipv4_address import ipv4_validator, IPV4Address +from primaite.utils.validation.port import Port, PORT_LOOKUP, port_validator _LOGGER = getLogger(__name__) @@ -106,9 +106,9 @@ class DoSBot(DatabaseClient, identifier="DoSBot"): :rtype: RequestResponse """ if "target_ip_address" in request[-1]: - request[-1]["target_ip_address"] = IPv4Address(request[-1]["target_ip_address"]) + request[-1]["target_ip_address"] = ipv4_validator(request[-1]["target_ip_address"]) if "target_port" in request[-1]: - request[-1]["target_port"] = PORT_LOOKUP[request[-1]["target_port"]] + request[-1]["target_port"] = port_validator(request[-1]["target_port"]) return RequestResponse.from_bool(self.configure(**request[-1])) rm.add_request("configure", request_type=RequestType(func=_configure)) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 4ba4c4d4..91f71302 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -31,6 +31,7 @@ class DatabaseService(Service, identifier="DatabaseService"): type: str = "DatabaseService" backup_server_ip: Optional[IPv4Address] = None + db_password: Optional[str] = None config: "DatabaseService.ConfigSchema" = Field(default_factory=lambda: DatabaseService.ConfigSchema()) @@ -53,6 +54,7 @@ class DatabaseService(Service, identifier="DatabaseService"): super().__init__(**kwargs) self._create_db_file() self.backup_server_ip = self.config.backup_server_ip + self.password = self.config.db_password def install(self): """ diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 0756eb05..6e6f7729 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Dict, Optional, TYPE_CHECKING from pydantic import Field @@ -9,8 +9,12 @@ from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.service import Service from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import Port, PORT_LOOKUP +if TYPE_CHECKING: + from primaite.simulator.network.hardware.base import Node + _LOGGER = getLogger(__name__) @@ -21,6 +25,7 @@ class DNSClient(Service, identifier="DNSClient"): """ConfigSchema for DNSClient.""" type: str = "DNSClient" + dns_server: Optional[IPV4Address] = None config: "DNSClient.ConfigSchema" = Field(default_factory=lambda: DNSClient.ConfigSchema()) dns_cache: Dict[str, IPv4Address] = {} @@ -36,6 +41,7 @@ class DNSClient(Service, identifier="DNSClient"): # TCP for now kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) + self.dns_server = self.config.dns_server self.start() def describe_state(self) -> Dict: @@ -79,6 +85,14 @@ class DNSClient(Service, identifier="DNSClient"): if not self._can_perform_action(): return False + # check if the domain is already in the DNS cache + if target_domain in self.dns_cache: + self.sys_log.info( + f"{self.name}: Domain lookup for {target_domain} successful," + f"resolves to {self.dns_cache[target_domain]}" + ) + return True + # check if DNS server is configured if self.dns_server is None: self.sys_log.warning(f"{self.name}: DNS Server is not configured") @@ -87,31 +101,23 @@ class DNSClient(Service, identifier="DNSClient"): # check if the target domain is in the client's DNS cache payload = DNSPacket(dns_request=DNSRequest(domain_name_request=target_domain)) - # check if the domain is already in the DNS cache - if target_domain in self.dns_cache: - self.sys_log.info( - f"{self.name}: Domain lookup for {target_domain} successful," - f"resolves to {self.dns_cache[target_domain]}" - ) - return True + # return False if already reattempted + if is_reattempt: + self.sys_log.warning(f"{self.name}: Domain lookup for {target_domain} failed") + return False else: - # return False if already reattempted - if is_reattempt: - self.sys_log.warning(f"{self.name}: Domain lookup for {target_domain} failed") - return False - else: - # send a request to check if domain name exists in the DNS Server - software_manager: SoftwareManager = self.software_manager - software_manager.send_payload_to_session_manager( - payload=payload, dest_ip_address=self.dns_server, dest_port=PORT_LOOKUP["DNS"] - ) + # send a request to check if domain name exists in the DNS Server + software_manager: SoftwareManager = self.software_manager + software_manager.send_payload_to_session_manager( + payload=payload, dest_ip_address=self.dns_server, dest_port=PORT_LOOKUP["DNS"] + ) - # recursively re-call the function passing is_reattempt=True - return self.check_domain_exists( - target_domain=target_domain, - session_id=session_id, - is_reattempt=True, - ) + # recursively re-call the function passing is_reattempt=True + return self.check_domain_exists( + target_domain=target_domain, + session_id=session_id, + is_reattempt=True, + ) def send( self, @@ -168,3 +174,9 @@ class DNSClient(Service, identifier="DNSClient"): self.sys_log.warning(f"Failed to resolve domain name {payload.dns_request.domain_name_request}") return False + + def install(self) -> None: + """Set the DNS server to be the node's DNS server unless a different one was already provided.""" + self.parent: Node + if self.parent and not self.dns_server: + self.dns_server = self.parent.dns_server diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 3a1c0e18..41a5b25f 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -36,6 +36,7 @@ class DNSServer(Service, identifier="DNSServer"): # TCP for now kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) + self.dns_table = self.config.domain_mapping self.start() def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 054bfe15..5f4ac846 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -6,7 +6,6 @@ from pydantic import Field from primaite import getLogger from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -from primaite.simulator.system.services.service import Service from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP @@ -22,14 +21,13 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): """ config: "FTPServer.ConfigSchema" = Field(default_factory=lambda: FTPServer.ConfigSchema()) - server_password: Optional[str] = None - """Password needed to connect to FTP server. Default is None.""" - class ConfigSchema(Service.ConfigSchema): + class ConfigSchema(FTPServiceABC.ConfigSchema): """ConfigSchema for FTPServer.""" type: str = "FTPServer" + server_password: Optional[str] = None def __init__(self, **kwargs): kwargs["name"] = "FTPServer" @@ -37,6 +35,7 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) self.start() + self.server_password = self.config.server_password def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index fb470faf..b5f921c9 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -9,6 +9,7 @@ from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP +from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -21,6 +22,7 @@ class NTPClient(Service, identifier="NTPClient"): """ConfigSchema for NTPClient.""" type: str = "NTPClient" + ntp_server_ip: Optional[IPV4Address] = None config: "NTPClient.ConfigSchema" = Field(default_factory=lambda: NTPClient.ConfigSchema()) @@ -33,6 +35,7 @@ class NTPClient(Service, identifier="NTPClient"): kwargs["port"] = PORT_LOOKUP["NTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) + self.ntp_server = self.config.ntp_server_ip self.start() def configure(self, ntp_server_ip_address: IPv4Address) -> None: diff --git a/src/primaite/utils/validation/ipv4_address.py b/src/primaite/utils/validation/ipv4_address.py index b2b8b72e..1dc6c74e 100644 --- a/src/primaite/utils/validation/ipv4_address.py +++ b/src/primaite/utils/validation/ipv4_address.py @@ -39,3 +39,12 @@ will automatically check and convert the input value to an instance of IPv4Addre any Pydantic model uses it. This ensures that any field marked with this type is not just an IPv4Address in form, but also valid according to the rules defined in ipv4_validator. """ + + +def str_ip(value: Any) -> str: + """Make sure it's a valid IP, but represent it as a string.""" + # TODO: this is a bit of a hack, we should change RequestResponse to be able to handle IPV4Address objects + return str(IPV4Address(value)) + + +StrIP: Final[Annotated] = Annotated[str, BeforeValidator(str_ip)] diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index a39bf876..b0591da6 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -178,8 +178,6 @@ simulation: backup_server_ip: 192.168.1.10 - type: WebServer - type: FTPServer - options: - server_password: arcd - type: NTPClient options: ntp_server_ip: 192.168.1.10 diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index bff58ebd..fcfc93ef 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -771,8 +771,6 @@ simulation: options: backup_server_ip: 192.168.1.16 - type: ExtendedService - options: - backup_server_ip: 192.168.1.16 - hostname: client_2 type: computer diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index 0ff6754d..2a3691ae 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -201,7 +201,6 @@ def test_ftp_server_install(): ftp_server_service: FTPServer = client_1.software_manager.software.get("FTPServer") assert ftp_server_service is not None - assert ftp_server_service.server_password == "arcd" def test_ntp_client_install(): diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index 40226be6..faf0466f 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -173,8 +173,8 @@ def test_c2_suite_configure_request(basic_network): c2_beacon_config = { "c2_server_ip_address": "192.168.0.2", "keep_alive_frequency": 5, - "masquerade_protocol": "TCP", - "masquerade_port": "HTTP", + "masquerade_protocol": "tcp", + "masquerade_port": 80, } network.apply_request(["node", "node_b", "application", "C2Beacon", "configure", c2_beacon_config]) From 037dd8278bc092f2a7979264256d775df6d29690 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 Jan 2025 12:30:08 +0000 Subject: [PATCH 165/224] #3029 - update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315579d5..c91bf4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed action space options which were previously used for assigning meaning to action space IDs - Updated tests that don't use YAMLs to still use the new action and agent schemas +### Fixed +- DNS client no longer fails to check its cache if a DNS server address is missing. +- DNS client now correctly inherits the node's DNS address configuration setting. + + ## [3.3.0] - 2024-09-04 ### Added From 3260e1f30b06d453c6c4115754d464409c7585f0 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 Jan 2025 14:41:49 +0000 Subject: [PATCH 166/224] #3029 - make new config items properties as per PR comments --- .../simulator/system/services/database/database_service.py | 6 +++++- src/primaite/simulator/system/services/dns/dns_client.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 91f71302..d65f05bd 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -54,7 +54,11 @@ class DatabaseService(Service, identifier="DatabaseService"): super().__init__(**kwargs) self._create_db_file() self.backup_server_ip = self.config.backup_server_ip - self.password = self.config.db_password + + @property + def password(self) -> Optional[str]: + """Convenience property for accessing the password.""" + return self.config.db_password def install(self): """ diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 6e6f7729..f4b427cd 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -44,6 +44,11 @@ class DNSClient(Service, identifier="DNSClient"): self.dns_server = self.config.dns_server self.start() + @property + def dns_server(self) -> Optional[IPV4Address]: + """Convenience property for accessing the dns server configuration.""" + return self.config.dns_server + def describe_state(self) -> Dict: """ Describes the current state of the software. @@ -179,4 +184,4 @@ class DNSClient(Service, identifier="DNSClient"): """Set the DNS server to be the node's DNS server unless a different one was already provided.""" self.parent: Node if self.parent and not self.dns_server: - self.dns_server = self.parent.dns_server + self.config.dns_server = self.parent.dns_server From a77fa65c39de68c62bc1264d771b21a7b1b9e519 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 Jan 2025 14:46:43 +0000 Subject: [PATCH 167/224] #3029 - Remove old initialisation of dns server attr that caused a bug --- src/primaite/simulator/system/services/dns/dns_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index f4b427cd..1c64c9a9 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -41,7 +41,6 @@ class DNSClient(Service, identifier="DNSClient"): # TCP for now kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) - self.dns_server = self.config.dns_server self.start() @property From 8feb2db954bcb153a4912d2096e8e34f0771b395 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 Jan 2025 15:29:10 +0000 Subject: [PATCH 168/224] Fix properties --- src/primaite/game/agent/agent_log.py | 5 ++-- src/primaite/game/game.py | 26 ------------------- .../services/database/database_service.py | 7 ++--- .../system/services/dns/dns_client.py | 16 +++++++----- 4 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/primaite/game/agent/agent_log.py b/src/primaite/game/agent/agent_log.py index 5d9dc848..ddf14489 100644 --- a/src/primaite/game/agent/agent_log.py +++ b/src/primaite/game/agent/agent_log.py @@ -65,8 +65,9 @@ class AgentLog: The logger is set to the DEBUG level, and is equipped with a handler that writes to a file and filters out JSON-like messages. """ - if not SIM_OUTPUT.save_agent_logs: - return + # TODO: uncomment this once we figure out why it's broken + # if not SIM_OUTPUT.save_agent_logs: + # return log_path = self._get_log_path() file_handler = logging.FileHandler(filename=log_path) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index b869cfd4..a8e23d56 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -406,32 +406,6 @@ class PrimaiteGame: if "service_install_duration" in defaults_config: new_service.install_duration = defaults_config["service_install_duration"] - # service-dependent options - if service_type == "DNSClient": - if "options" in service_cfg: - opt = service_cfg["options"] - if "dns_server" in opt: - new_service.dns_server = IPv4Address(opt["dns_server"]) - if service_type == "DNSServer": - if "options" in service_cfg: - opt = service_cfg["options"] - if "domain_mapping" in opt: - for domain, ip in opt["domain_mapping"].items(): - new_service.dns_register(domain, IPv4Address(ip)) - if service_type == "DatabaseService": - if "options" in service_cfg: - opt = service_cfg["options"] - new_service.password = opt.get("db_password", None) - if "backup_server_ip" in opt: - new_service.configure_backup(backup_server=IPv4Address(opt.get("backup_server_ip"))) - if service_type == "FTPServer": - if "options" in service_cfg: - opt = service_cfg["options"] - new_service.server_password = opt.get("server_password") - if service_type == "NTPClient": - if "options" in service_cfg: - opt = service_cfg["options"] - new_service.ntp_server = IPv4Address(opt.get("ntp_server_ip")) if "applications" in node_cfg: for application_cfg in node_cfg["applications"]: new_application = None diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index d65f05bd..1745b9d1 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -35,9 +35,6 @@ class DatabaseService(Service, identifier="DatabaseService"): config: "DatabaseService.ConfigSchema" = Field(default_factory=lambda: DatabaseService.ConfigSchema()) - password: Optional[str] = None - """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" - backup_server_ip: IPv4Address = None """IP address of the backup server.""" @@ -60,6 +57,10 @@ class DatabaseService(Service, identifier="DatabaseService"): """Convenience property for accessing the password.""" return self.config.db_password + @password.setter + def password(self, val: str) -> None: + self.config.db_password = val + def install(self): """ Perform first-time setup of the DatabaseService. diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 1c64c9a9..825896e0 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -30,8 +30,6 @@ class DNSClient(Service, identifier="DNSClient"): config: "DNSClient.ConfigSchema" = Field(default_factory=lambda: DNSClient.ConfigSchema()) dns_cache: Dict[str, IPv4Address] = {} "A dict of known mappings between domain/URLs names and IPv4 addresses." - dns_server: Optional[IPv4Address] = None - "The DNS Server the client sends requests to." def __init__(self, **kwargs): kwargs["name"] = "DNSClient" @@ -43,11 +41,6 @@ class DNSClient(Service, identifier="DNSClient"): super().__init__(**kwargs) self.start() - @property - def dns_server(self) -> Optional[IPV4Address]: - """Convenience property for accessing the dns server configuration.""" - return self.config.dns_server - def describe_state(self) -> Dict: """ Describes the current state of the software. @@ -61,6 +54,15 @@ class DNSClient(Service, identifier="DNSClient"): state = super().describe_state() return state + @property + def dns_server(self) -> Optional[IPV4Address]: + """Convenience property for accessing the dns server configuration.""" + return self.config.dns_server + + @dns_server.setter + def dns_server(self, val: IPV4Address) -> None: + self.config.dns_server = val + def add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address) -> bool: """ Adds a domain name to the DNS Client cache. From 055c853b0fd19be88a1d36ae6381b663a3a93714 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 31 Jan 2025 16:00:32 +0000 Subject: [PATCH 169/224] #3062 - rename identifier to discriminator --- docs/source/configuration/agents.rst | 2 +- docs/source/configuration/simulation.rst | 2 +- .../how_to_guides/extensible_actions.rst | 10 +++---- .../how_to_guides/extensible_agents.rst | 6 ++-- .../how_to_guides/extensible_rewards.rst | 4 +-- docs/source/node_sets.rst | 2 +- src/primaite/game/agent/actions/abstract.py | 17 +++++++---- src/primaite/game/agent/actions/acl.py | 10 +++---- .../game/agent/actions/application.py | 12 ++++---- src/primaite/game/agent/actions/file.py | 16 +++++----- src/primaite/game/agent/actions/folder.py | 10 +++---- src/primaite/game/agent/actions/host_nic.py | 4 +-- src/primaite/game/agent/actions/manager.py | 2 +- src/primaite/game/agent/actions/network.py | 6 ++-- src/primaite/game/agent/actions/node.py | 18 ++++++------ src/primaite/game/agent/actions/service.py | 20 ++++++------- src/primaite/game/agent/actions/session.py | 8 ++--- src/primaite/game/agent/actions/software.py | 20 +++++++------ src/primaite/game/agent/interface.py | 14 ++++----- .../agent/observations/acl_observation.py | 2 +- .../observations/file_system_observations.py | 4 +-- .../observations/firewall_observation.py | 2 +- .../agent/observations/host_observations.py | 2 +- .../agent/observations/link_observation.py | 4 +-- .../agent/observations/nic_observations.py | 4 +-- .../agent/observations/node_observations.py | 2 +- .../agent/observations/observation_manager.py | 10 +++---- .../game/agent/observations/observations.py | 14 ++++----- .../agent/observations/router_observation.py | 2 +- .../observations/software_observation.py | 4 +-- src/primaite/game/agent/rewards.py | 24 +++++++-------- .../agent/scripted_agents/abstract_tap.py | 2 +- .../scripted_agents/data_manipulation_bot.py | 2 +- .../scripted_agents/probabilistic_agent.py | 2 +- .../agent/scripted_agents/random_agent.py | 4 +-- .../create-simulation_demo.ipynb | 2 +- src/primaite/simulator/core.py | 2 +- src/primaite/simulator/network/container.py | 2 +- src/primaite/simulator/network/creation.py | 22 +++++++------- .../simulator/network/hardware/base.py | 29 ++++++++++--------- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../network/hardware/nodes/host/server.py | 4 +-- .../hardware/nodes/network/firewall.py | 2 +- .../hardware/nodes/network/network_node.py | 2 +- .../network/hardware/nodes/network/router.py | 2 +- .../network/hardware/nodes/network/switch.py | 2 +- .../hardware/nodes/network/wireless_router.py | 2 +- .../system/applications/application.py | 14 ++++----- .../system/applications/database_client.py | 2 +- .../simulator/system/applications/nmap.py | 2 +- .../red_applications/c2/c2_beacon.py | 2 +- .../red_applications/c2/c2_server.py | 2 +- .../red_applications/data_manipulation_bot.py | 2 +- .../applications/red_applications/dos_bot.py | 2 +- .../red_applications/ransomware_script.py | 2 +- .../system/applications/web_browser.py | 2 +- .../simulator/system/services/arp/arp.py | 2 +- .../services/database/database_service.py | 2 +- .../system/services/dns/dns_client.py | 2 +- .../system/services/dns/dns_server.py | 2 +- .../system/services/ftp/ftp_client.py | 2 +- .../system/services/ftp/ftp_server.py | 2 +- .../simulator/system/services/icmp/icmp.py | 2 +- .../system/services/ntp/ntp_client.py | 2 +- .../system/services/ntp/ntp_server.py | 2 +- .../simulator/system/services/service.py | 16 +++++----- .../system/services/terminal/terminal.py | 2 +- .../system/services/web_server/web_server.py | 2 +- tests/conftest.py | 6 ++-- .../applications/extended_application.py | 2 +- .../extensions/nodes/giga_switch.py | 2 +- .../extensions/nodes/super_computer.py | 2 +- .../extensions/services/extended_service.py | 2 +- .../network/test_broadcast.py | 4 +-- .../system/test_service_listening_on_ports.py | 2 +- .../test_application_registry.py | 4 +-- .../_simulator/_system/test_software.py | 2 +- 78 files changed, 223 insertions(+), 213 deletions(-) diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index cf2b618f..0194bd72 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -103,7 +103,7 @@ Similar to action space, this is defined as a list of components from the :py:mo ``reward_components`` ^^^^^^^^^^^^^^^^^^^^^ - +TODO: update description A list of reward types from :py:mod:`primaite.game.agent.rewards.RewardFunction.rew_class_identifiers` e.g. diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index 0b2067d8..47ff6832 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -6,7 +6,7 @@ ``simulation`` ============== In this section the network layout is defined. This part of the config follows a hierarchical structure. Almost every component defines a ``ref`` field which acts as a human-readable unique identifier, used by other parts of the config, such as agents. - +# TODO: ref field is no longer real At the top level of the network are ``nodes``, ``links`` and ``airspace``. e.g. diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 93a6cf21..4deede53 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -20,7 +20,7 @@ Custom actions within PrimAITE must be a sub-class of `AbstractAction`, and cont #. ConfigSchema class -#. Unique Identifier +#. Unique discriminator #. `form_request` method. @@ -31,14 +31,14 @@ ConfigSchema The ConfigSchema sub-class of the action must contain all `configurable` variables within the action, that would be specified within the environments configuration YAML file. -Unique Identifier +Unique discriminator ################# -When declaring a custom class, it must have a unique identifier string, that allows PrimAITE to generate the correct action when needed. +When declaring a custom class, it must have a unique discriminator string, that allows PrimAITE to generate the correct action when needed. .. code:: Python - class CreateDirectoryAction(AbstractAction, identifier="node_folder_create") + class CreateDirectoryAction(AbstractAction, discriminator="node_folder_create") config: CreateDirectoryAction.ConfigSchema @@ -58,7 +58,7 @@ When declaring a custom class, it must have a unique identifier string, that all config.directory_name, ] -The above action would fail pydantic validation as the identifier "node_folder_create" is already used by the `NodeFolderCreateAction`, and would create a duplicate listing within `AbstractAction._registry`. +The above action would fail pydantic validation as the discriminator "node_folder_create" is already used by the `NodeFolderCreateAction`, and would create a duplicate listing within `AbstractAction._registry`. form_request method diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 256e96ca..d83f83d6 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -25,7 +25,7 @@ The core features that should be implemented in any new agent are detailed below .. code-block:: python - class ExampleAgent(AbstractAgent, identifier = "ExampleAgent"): + class ExampleAgent(AbstractAgent, discriminator = "ExampleAgent"): """An example agent for demonstration purposes.""" config: "ExampleAgent.ConfigSchema" = Field(default_factory= lambda: ExampleAgent.ConfigSchema()) @@ -64,9 +64,9 @@ The core features that should be implemented in any new agent are detailed below starting_host: "Server_1" -#. **Identifiers**: +#. **discriminators**: - All agent classes should have an ``identifier`` attribute, a unique kebab-case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. + All agent classes should have an ``discriminator`` attribute, a unique kebab-case string, for when they are added to the base ``AbstractAgent`` registry. This is then specified in your configuration YAML, and used by PrimAITE to generate the correct Agent. Changes to YAML file ==================== diff --git a/docs/source/how_to_guides/extensible_rewards.rst b/docs/source/how_to_guides/extensible_rewards.rst index a01b9d8f..9068f1bb 100644 --- a/docs/source/how_to_guides/extensible_rewards.rst +++ b/docs/source/how_to_guides/extensible_rewards.rst @@ -17,7 +17,7 @@ Reward classes are inherited from AbstractReward (a sub-class of Pydantic's Base Within the reward class there is a ConfigSchema class responsible for ensuring the config file data is in the correct format. This also means there is little (if no) requirement for and `__init__` method. The `.from_config` method is no longer required as it's inherited from `AbstractReward`. -Each class requires an identifier string which is used by the ConfigSchema class to verify that it +Each class requires an discriminator string which is used by the ConfigSchema class to verify that it hasn't previously been added to the registry. Inheriting from `BaseModel` removes the need for an `__init__` method but means that object @@ -28,7 +28,7 @@ To add a new reward class follow the example below. Note that the type attribute .. code-block:: Python -class DatabaseFileIntegrity(AbstractReward, identifier="DATABASE_FILE_INTEGRITY"): +class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRITY"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" config: "DatabaseFileIntegrity.ConfigSchema" diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst index 3c247478..1ac6a54c 100644 --- a/docs/source/node_sets.rst +++ b/docs/source/node_sets.rst @@ -82,7 +82,7 @@ Here is an example of creating a custom node adder, DataCenterAdder: .. code-block:: python - class DataCenterAdder(NetworkNodeAdder, identifier="data_center"): + class DataCenterAdder(NetworkNodeAdder, discriminator="data_center"): class ConfigSchema(NetworkNodeAdder.ConfigSchema): type: Literal["data_center"] = "data_center" num_servers: int diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 1cda4360..9e55cf09 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -22,13 +22,20 @@ class AbstractAction(BaseModel, ABC): _registry: ClassVar[Dict[str, Type[AbstractAction]]] = {} - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: + """ + Register an action type. + + :param discriminator: discriminator used to uniquely specify action types. + :type discriminator: str + :raises ValueError: When attempting to create an action with a name that is already in use. + """ super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - if identifier in cls._registry: - raise ValueError(f"Cannot create new action under reserved name {identifier}") - cls._registry[identifier] = cls + if discriminator in cls._registry: + raise ValueError(f"Cannot create new action under reserved name {discriminator}") + cls._registry[discriminator] = cls @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 7b70d10d..4ef4b506 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -37,7 +37,7 @@ class ACLAddRuleAbstractAction(AbstractAction, ABC): dst_wildcard: Union[IPV4Address, Literal["NONE"]] -class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_abstract_action"): +class ACLRemoveRuleAbstractAction(AbstractAction, discriminator="acl_remove_rule_abstract_action"): """Base abstract class for ACL remove rule actions.""" config: ConfigSchema = "ACLRemoveRuleAbstractAction.ConfigSchema" @@ -48,7 +48,7 @@ class ACLRemoveRuleAbstractAction(AbstractAction, identifier="acl_remove_rule_ab position: int -class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_add_rule"): +class RouterACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="router_acl_add_rule"): """Action which adds a rule to a router's ACL.""" config: "RouterACLAddRuleAction.ConfigSchema" @@ -79,7 +79,7 @@ class RouterACLAddRuleAction(ACLAddRuleAbstractAction, identifier="router_acl_ad ] -class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, identifier="router_acl_remove_rule"): +class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="router_acl_remove_rule"): """Action which removes a rule from a router's ACL.""" config: "RouterACLRemoveRuleAction.ConfigSchema" @@ -95,7 +95,7 @@ class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, identifier="router_ return ["network", "node", config.target_router, "acl", "remove_rule", config.position] -class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_acl_add_rule"): +class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="firewall_acl_add_rule"): """Action which adds a rule to a firewall port's ACL.""" config: "FirewallACLAddRuleAction.ConfigSchema" @@ -130,7 +130,7 @@ class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, identifier="firewall_ac ] -class FirewallACLRemoveRuleAction(ACLRemoveRuleAbstractAction, identifier="firewall_acl_remove_rule"): +class FirewallACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="firewall_acl_remove_rule"): """Action which removes a rule from a firewall port's ACL.""" config: "FirewallACLRemoveRuleAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index f6ce0624..36d2e0b4 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -45,7 +45,7 @@ class NodeApplicationAbstractAction(AbstractAction, ABC): ] -class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="node_application_execute"): +class NodeApplicationExecuteAction(NodeApplicationAbstractAction, discriminator="node_application_execute"): """Action which executes an application.""" config: "NodeApplicationExecuteAction.ConfigSchema" @@ -56,7 +56,7 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction, identifier="no verb: str = "execute" -class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_application_scan"): +class NodeApplicationScanAction(NodeApplicationAbstractAction, discriminator="node_application_scan"): """Action which scans an application.""" config: "NodeApplicationScanAction.ConfigSchema" @@ -67,7 +67,7 @@ class NodeApplicationScanAction(NodeApplicationAbstractAction, identifier="node_ verb: str = "scan" -class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node_application_close"): +class NodeApplicationCloseAction(NodeApplicationAbstractAction, discriminator="node_application_close"): """Action which closes an application.""" config: "NodeApplicationCloseAction.ConfigSchema" @@ -78,7 +78,7 @@ class NodeApplicationCloseAction(NodeApplicationAbstractAction, identifier="node verb: str = "close" -class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_application_fix"): +class NodeApplicationFixAction(NodeApplicationAbstractAction, discriminator="node_application_fix"): """Action which fixes an application.""" config: "NodeApplicationFixAction.ConfigSchema" @@ -89,7 +89,7 @@ class NodeApplicationFixAction(NodeApplicationAbstractAction, identifier="node_a verb: str = "fix" -class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="node_application_install"): +class NodeApplicationInstallAction(NodeApplicationAbstractAction, discriminator="node_application_install"): """Action which installs an application.""" config: "NodeApplicationInstallAction.ConfigSchema" @@ -113,7 +113,7 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction, identifier="no ] -class NodeApplicationRemoveAction(NodeApplicationAbstractAction, identifier="node_application_remove"): +class NodeApplicationRemoveAction(NodeApplicationAbstractAction, discriminator="node_application_remove"): """Action which removes/uninstalls an application.""" config: "NodeApplicationRemoveAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index ed666773..2aa3b85c 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -52,7 +52,7 @@ class NodeFileAbstractAction(AbstractAction, ABC): ] -class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create"): +class NodeFileCreateAction(NodeFileAbstractAction, discriminator="node_file_create"): """Action which creates a new file in a given folder.""" config: "NodeFileCreateAction.ConfigSchema" @@ -81,7 +81,7 @@ class NodeFileCreateAction(NodeFileAbstractAction, identifier="node_file_create" ] -class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): +class NodeFileScanAction(NodeFileAbstractAction, discriminator="node_file_scan"): """Action which scans a file.""" config: "NodeFileScanAction.ConfigSchema" @@ -92,7 +92,7 @@ class NodeFileScanAction(NodeFileAbstractAction, identifier="node_file_scan"): verb: ClassVar[str] = "scan" -class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete"): +class NodeFileDeleteAction(NodeFileAbstractAction, discriminator="node_file_delete"): """Action which deletes a file.""" config: "NodeFileDeleteAction.ConfigSchema" @@ -119,7 +119,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction, identifier="node_file_delete" ] -class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restore"): +class NodeFileRestoreAction(NodeFileAbstractAction, discriminator="node_file_restore"): """Action which restores a file.""" config: "NodeFileRestoreAction.ConfigSchema" @@ -130,7 +130,7 @@ class NodeFileRestoreAction(NodeFileAbstractAction, identifier="node_file_restor verb: ClassVar[str] = "restore" -class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrupt"): +class NodeFileCorruptAction(NodeFileAbstractAction, discriminator="node_file_corrupt"): """Action which corrupts a file.""" config: "NodeFileCorruptAction.ConfigSchema" @@ -141,7 +141,7 @@ class NodeFileCorruptAction(NodeFileAbstractAction, identifier="node_file_corrup verb: ClassVar[str] = "corrupt" -class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access"): +class NodeFileAccessAction(NodeFileAbstractAction, discriminator="node_file_access"): """Action which increases a file's access count.""" config: "NodeFileAccessAction.ConfigSchema" @@ -167,7 +167,7 @@ class NodeFileAccessAction(NodeFileAbstractAction, identifier="node_file_access" ] -class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_checkhash"): +class NodeFileCheckhashAction(NodeFileAbstractAction, discriminator="node_file_checkhash"): """Action which checks the hash of a file.""" config: "NodeFileCheckhashAction.ConfigSchema" @@ -178,7 +178,7 @@ class NodeFileCheckhashAction(NodeFileAbstractAction, identifier="node_file_chec verb: ClassVar[str] = "checkhash" -class NodeFileRepairAction(NodeFileAbstractAction, identifier="node_file_repair"): +class NodeFileRepairAction(NodeFileAbstractAction, discriminator="node_file_repair"): """Action which repairs a file.""" config: "NodeFileRepairAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index 3e1136ac..c0a03398 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -47,7 +47,7 @@ class NodeFolderAbstractAction(AbstractAction, ABC): ] -class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_scan"): +class NodeFolderScanAction(NodeFolderAbstractAction, discriminator="node_folder_scan"): """Action which scans a folder.""" config: "NodeFolderScanAction.ConfigSchema" @@ -58,7 +58,7 @@ class NodeFolderScanAction(NodeFolderAbstractAction, identifier="node_folder_sca verb: ClassVar[str] = "scan" -class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folder_checkhash"): +class NodeFolderCheckhashAction(NodeFolderAbstractAction, discriminator="node_folder_checkhash"): """Action which checks the hash of a folder.""" config: "NodeFolderCheckhashAction.ConfigSchema" @@ -69,7 +69,7 @@ class NodeFolderCheckhashAction(NodeFolderAbstractAction, identifier="node_folde verb: ClassVar[str] = "checkhash" -class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_repair"): +class NodeFolderRepairAction(NodeFolderAbstractAction, discriminator="node_folder_repair"): """Action which repairs a folder.""" config: "NodeFolderRepairAction.ConfigSchema" @@ -80,7 +80,7 @@ class NodeFolderRepairAction(NodeFolderAbstractAction, identifier="node_folder_r verb: ClassVar[str] = "repair" -class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_restore"): +class NodeFolderRestoreAction(NodeFolderAbstractAction, discriminator="node_folder_restore"): """Action which restores a folder.""" config: "NodeFolderRestoreAction.ConfigSchema" @@ -91,7 +91,7 @@ class NodeFolderRestoreAction(NodeFolderAbstractAction, identifier="node_folder_ verb: ClassVar[str] = "restore" -class NodeFolderCreateAction(NodeFolderAbstractAction, identifier="node_folder_create"): +class NodeFolderCreateAction(NodeFolderAbstractAction, discriminator="node_folder_create"): """Action which creates a new folder.""" config: "NodeFolderCreateAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 0ca816f3..35599325 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -40,7 +40,7 @@ class HostNICAbstractAction(AbstractAction, ABC): ] -class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): +class HostNICEnableAction(HostNICAbstractAction, discriminator="host_nic_enable"): """Action which enables a NIC.""" config: "HostNICEnableAction.ConfigSchema" @@ -51,7 +51,7 @@ class HostNICEnableAction(HostNICAbstractAction, identifier="host_nic_enable"): verb: ClassVar[str] = "enable" -class HostNICDisableAction(HostNICAbstractAction, identifier="host_nic_disable"): +class HostNICDisableAction(HostNICAbstractAction, discriminator="host_nic_disable"): """Action which disables a NIC.""" config: "HostNICDisableAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 3e5b21b1..8332368e 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -24,7 +24,7 @@ from primaite.interface.request import RequestFormat __all__ = ("DoNothingAction", "ActionManager") -class DoNothingAction(AbstractAction, identifier="do_nothing"): +class DoNothingAction(AbstractAction, discriminator="do_nothing"): """Do Nothing Action.""" class ConfigSchema(AbstractAction.ConfigSchema): diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index d244fb74..afa22861 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -8,7 +8,7 @@ from primaite.interface.request import RequestFormat __all__ = ("NetworkPortEnableAction", "NetworkPortDisableAction") -class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstract"): +class NetworkPortAbstractAction(AbstractAction, discriminator="network_port_abstract"): """Base class for Network port actions.""" config: "NetworkPortAbstractAction.ConfigSchema" @@ -35,7 +35,7 @@ class NetworkPortAbstractAction(AbstractAction, identifier="network_port_abstrac ] -class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_port_enable"): +class NetworkPortEnableAction(NetworkPortAbstractAction, discriminator="network_port_enable"): """Action which enables are port on a router or a firewall.""" config: "NetworkPortEnableAction.ConfigSchema" @@ -46,7 +46,7 @@ class NetworkPortEnableAction(NetworkPortAbstractAction, identifier="network_por verb: ClassVar[str] = "enable" -class NetworkPortDisableAction(NetworkPortAbstractAction, identifier="network_port_disable"): +class NetworkPortDisableAction(NetworkPortAbstractAction, discriminator="network_port_disable"): """Action which disables are port on a router or a firewall.""" config: "NetworkPortDisableAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 5e1b6725..7f7b01a2 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -18,7 +18,7 @@ __all__ = ( ) -class NodeAbstractAction(AbstractAction, identifier="node_abstract"): +class NodeAbstractAction(AbstractAction, discriminator="node_abstract"): """ Abstract base class for node actions. @@ -39,7 +39,7 @@ class NodeAbstractAction(AbstractAction, identifier="node_abstract"): return ["network", "node", config.node_name, config.verb] -class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): +class NodeOSScanAction(NodeAbstractAction, discriminator="node_os_scan"): """Action which scans a node's OS.""" config: "NodeOSScanAction.ConfigSchema" @@ -50,7 +50,7 @@ class NodeOSScanAction(NodeAbstractAction, identifier="node_os_scan"): verb: ClassVar[str] = "scan" -class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): +class NodeShutdownAction(NodeAbstractAction, discriminator="node_shutdown"): """Action which shuts down a node.""" config: "NodeShutdownAction.ConfigSchema" @@ -61,7 +61,7 @@ class NodeShutdownAction(NodeAbstractAction, identifier="node_shutdown"): verb: ClassVar[str] = "shutdown" -class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): +class NodeStartupAction(NodeAbstractAction, discriminator="node_startup"): """Action which starts up a node.""" config: "NodeStartupAction.ConfigSchema" @@ -72,7 +72,7 @@ class NodeStartupAction(NodeAbstractAction, identifier="node_startup"): verb: ClassVar[str] = "startup" -class NodeResetAction(NodeAbstractAction, identifier="node_reset"): +class NodeResetAction(NodeAbstractAction, discriminator="node_reset"): """Action which resets a node.""" config: "NodeResetAction.ConfigSchema" @@ -83,7 +83,7 @@ class NodeResetAction(NodeAbstractAction, identifier="node_reset"): verb: ClassVar[str] = "reset" -class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_action"): +class NodeNMAPAbstractAction(AbstractAction, discriminator="node_nmap_abstract_action"): """Base class for NodeNMAP actions.""" config: "NodeNMAPAbstractAction.ConfigSchema" @@ -103,7 +103,7 @@ class NodeNMAPAbstractAction(AbstractAction, identifier="node_nmap_abstract_acti pass -class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_scan"): +class NodeNMAPPingScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_ping_scan"): """Action which performs an NMAP ping scan.""" config: "NodeNMAPPingScanAction.ConfigSchema" @@ -122,7 +122,7 @@ class NodeNMAPPingScanAction(NodeNMAPAbstractAction, identifier="node_nmap_ping_ ] -class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_scan"): +class NodeNMAPPortScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_port_scan"): """Action which performs an NMAP port scan.""" config: "NodeNMAPPortScanAction.ConfigSchema" @@ -154,7 +154,7 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, identifier="node_nmap_port_ ] -class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, identifier="node_network_service_recon"): +class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, discriminator="node_network_service_recon"): """Action which performs an NMAP network service recon (ping scan followed by port scan).""" config: "NodeNetworkServiceReconAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index 4a483f28..4adbe139 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -17,7 +17,7 @@ __all__ = ( ) -class NodeServiceAbstractAction(AbstractAction, identifier="node_service_abstract"): +class NodeServiceAbstractAction(AbstractAction, discriminator="node_service_abstract"): """Abstract Action for Node Service related actions. Any actions which use node_name and service_name can inherit from this class. @@ -36,7 +36,7 @@ class NodeServiceAbstractAction(AbstractAction, identifier="node_service_abstrac return ["network", "node", config.node_name, "service", config.service_name, config.verb] -class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_scan"): +class NodeServiceScanAction(NodeServiceAbstractAction, discriminator="node_service_scan"): """Action which scans a service.""" config: "NodeServiceScanAction.ConfigSchema" @@ -47,7 +47,7 @@ class NodeServiceScanAction(NodeServiceAbstractAction, identifier="node_service_ verb: ClassVar[str] = "scan" -class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_stop"): +class NodeServiceStopAction(NodeServiceAbstractAction, discriminator="node_service_stop"): """Action which stops a service.""" config: "NodeServiceStopAction.ConfigSchema" @@ -58,7 +58,7 @@ class NodeServiceStopAction(NodeServiceAbstractAction, identifier="node_service_ verb: ClassVar[str] = "stop" -class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service_start"): +class NodeServiceStartAction(NodeServiceAbstractAction, discriminator="node_service_start"): """Action which starts a service.""" config: "NodeServiceStartAction.ConfigSchema" @@ -69,7 +69,7 @@ class NodeServiceStartAction(NodeServiceAbstractAction, identifier="node_service verb: ClassVar[str] = "start" -class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service_pause"): +class NodeServicePauseAction(NodeServiceAbstractAction, discriminator="node_service_pause"): """Action which pauses a service.""" config: "NodeServicePauseAction.ConfigSchema" @@ -80,7 +80,7 @@ class NodeServicePauseAction(NodeServiceAbstractAction, identifier="node_service verb: ClassVar[str] = "pause" -class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_service_resume"): +class NodeServiceResumeAction(NodeServiceAbstractAction, discriminator="node_service_resume"): """Action which resumes a service.""" config: "NodeServiceResumeAction.ConfigSchema" @@ -91,7 +91,7 @@ class NodeServiceResumeAction(NodeServiceAbstractAction, identifier="node_servic verb: ClassVar[str] = "resume" -class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_service_restart"): +class NodeServiceRestartAction(NodeServiceAbstractAction, discriminator="node_service_restart"): """Action which restarts a service.""" config: "NodeServiceRestartAction.ConfigSchema" @@ -102,7 +102,7 @@ class NodeServiceRestartAction(NodeServiceAbstractAction, identifier="node_servi verb: ClassVar[str] = "restart" -class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_service_disable"): +class NodeServiceDisableAction(NodeServiceAbstractAction, discriminator="node_service_disable"): """Action which disables a service.""" config: "NodeServiceDisableAction.ConfigSchema" @@ -113,7 +113,7 @@ class NodeServiceDisableAction(NodeServiceAbstractAction, identifier="node_servi verb: ClassVar[str] = "disable" -class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_service_enable"): +class NodeServiceEnableAction(NodeServiceAbstractAction, discriminator="node_service_enable"): """Action which enables a service.""" config: "NodeServiceEnableAction.ConfigSchema" @@ -124,7 +124,7 @@ class NodeServiceEnableAction(NodeServiceAbstractAction, identifier="node_servic verb: ClassVar[str] = "enable" -class NodeServiceFixAction(NodeServiceAbstractAction, identifier="node_service_fix"): +class NodeServiceFixAction(NodeServiceAbstractAction, discriminator="node_service_fix"): """Action which fixes a service.""" config: "NodeServiceFixAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 9720d371..4bed1943 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -11,7 +11,7 @@ __all__ = ( ) -class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstract"): +class NodeSessionAbstractAction(AbstractAction, discriminator="node_session_abstract"): """Base class for NodeSession actions.""" config: "NodeSessionAbstractAction.ConfigSchema" @@ -33,7 +33,7 @@ class NodeSessionAbstractAction(AbstractAction, identifier="node_session_abstrac pass -class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_session_remote_login"): +class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="node_session_remote_login"): """Action which performs a remote session login.""" config: "NodeSessionsRemoteLoginAction.ConfigSchema" @@ -62,7 +62,7 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, identifier="node_ ] -class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node_session_remote_logoff"): +class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="node_session_remote_logoff"): """Action which performs a remote session logout.""" config: "NodeSessionsRemoteLogoutAction.ConfigSchema" @@ -80,7 +80,7 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, identifier="node return ["network", "node", config.node_name, "service", "Terminal", config.verb, config.remote_ip] -class NodeAccountChangePasswordAction(NodeSessionAbstractAction, identifier="node_account_change_password"): +class NodeAccountChangePasswordAction(NodeSessionAbstractAction, discriminator="node_account_change_password"): """Action which changes the password for a user.""" config: "NodeAccountChangePasswordAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index 81a3a315..49edd7c5 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -22,7 +22,7 @@ __all__ = ( ) -class ConfigureRansomwareScriptAction(AbstractAction, identifier="configure_ransomware_script"): +class ConfigureRansomwareScriptAction(AbstractAction, discriminator="configure_ransomware_script"): """Action which sets config parameters for a ransomware script on a node.""" config: "ConfigureRansomwareScriptAction.ConfigSchema" @@ -48,7 +48,9 @@ class ConfigureRansomwareScriptAction(AbstractAction, identifier="configure_rans return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", data] -class RansomwareConfigureC2ServerAction(ConfigureRansomwareScriptAction, identifier="c2_server_ransomware_configure"): +class RansomwareConfigureC2ServerAction( + ConfigureRansomwareScriptAction, discriminator="c2_server_ransomware_configure" +): """Action which causes a C2 server to send a command to set options on a ransomware script remotely.""" @classmethod @@ -59,7 +61,7 @@ class RansomwareConfigureC2ServerAction(ConfigureRansomwareScriptAction, identif return ["network", "node", config.node_name, "application", "C2Server", "ransomware_configure", data] -class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): +class ConfigureDoSBotAction(AbstractAction, discriminator="configure_dos_bot"): """Action which sets config parameters for a DoS bot on a node.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -91,7 +93,7 @@ class ConfigureDoSBotAction(AbstractAction, identifier="configure_dos_bot"): return ["network", "node", config.node_name, "application", "DoSBot", "configure", data] -class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): +class ConfigureC2BeaconAction(AbstractAction, discriminator="configure_c2_beacon"): """Action which configures a C2 Beacon based on the parameters given.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -115,7 +117,7 @@ class ConfigureC2BeaconAction(AbstractAction, identifier="configure_c2_beacon"): return ["network", "node", config.node_name, "application", "C2Beacon", "configure", data] -class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_command"): +class NodeSendRemoteCommandAction(AbstractAction, discriminator="node_send_remote_command"): """Action which sends a terminal command to a remote node via SSH.""" config: "NodeSendRemoteCommandAction.ConfigSchema" @@ -142,7 +144,7 @@ class NodeSendRemoteCommandAction(AbstractAction, identifier="node_send_remote_c ] -class TerminalC2ServerAction(AbstractAction, identifier="c2_server_terminal_command"): +class TerminalC2ServerAction(AbstractAction, discriminator="c2_server_terminal_command"): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" config: "TerminalC2ServerAction.ConfigSchema" @@ -171,7 +173,7 @@ class TerminalC2ServerAction(AbstractAction, identifier="c2_server_terminal_comm return ["network", "node", config.node_name, "application", "C2Server", "terminal_command", command_model] -class RansomwareLaunchC2ServerAction(AbstractAction, identifier="c2_server_ransomware_launch"): +class RansomwareLaunchC2ServerAction(AbstractAction, discriminator="c2_server_ransomware_launch"): """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" config: "RansomwareLaunchC2ServerAction.ConfigSchema" @@ -190,7 +192,7 @@ class RansomwareLaunchC2ServerAction(AbstractAction, identifier="c2_server_ranso return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] -class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfiltrate"): +class ExfiltrationC2ServerAction(AbstractAction, discriminator="c2_server_data_exfiltrate"): """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" config: "ExfiltrationC2ServerAction.ConfigSchema" @@ -223,7 +225,7 @@ class ExfiltrationC2ServerAction(AbstractAction, identifier="c2_server_data_exfi return ["network", "node", config.node_name, "application", "C2Server", "exfiltrate", command_model] -class ConfigureDatabaseClientAction(AbstractAction, identifier="configure_database_client"): +class ConfigureDatabaseClientAction(AbstractAction, discriminator="configure_database_client"): """Action which sets config parameters for a database client on a node.""" config: "ConfigureDatabaseClientAction.ConfigSchema" diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index aac898e1..cb1c15dd 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -79,13 +79,13 @@ class AbstractAgent(BaseModel, ABC): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - if identifier in cls._registry: - raise ValueError(f"Cannot create a new agent under reserved name {identifier}") - cls._registry[identifier] = cls + if discriminator in cls._registry: + raise ValueError(f"Cannot create a new agent under reserved name {discriminator}") + cls._registry[discriminator] = cls def model_post_init(self, __context: Any) -> None: """Overwrite the default empty action, observation, and rewards with ones defined through the config.""" @@ -161,7 +161,7 @@ class AbstractAgent(BaseModel, ABC): return agent_class(config=config) -class AbstractScriptedAgent(AbstractAgent, identifier="AbstractScriptedAgent"): +class AbstractScriptedAgent(AbstractAgent, discriminator="AbstractScriptedAgent"): """Base class for actors which generate their own behaviour.""" config: "AbstractScriptedAgent.ConfigSchema" = Field(default_factory=lambda: AbstractScriptedAgent.ConfigSchema()) @@ -177,7 +177,7 @@ class AbstractScriptedAgent(AbstractAgent, identifier="AbstractScriptedAgent"): return super().get_action(obs=obs, timestep=timestep) -class ProxyAgent(AbstractAgent, identifier="ProxyAgent"): +class ProxyAgent(AbstractAgent, discriminator="ProxyAgent"): """Agent that sends observations to an RL model and receives actions from that model.""" config: "ProxyAgent.ConfigSchema" = Field(default_factory=lambda: ProxyAgent.ConfigSchema()) diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index fde49a6b..ef171431 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -16,7 +16,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class ACLObservation(AbstractObservation, identifier="ACL"): +class ACLObservation(AbstractObservation, discriminator="ACL"): """ACL observation, provides information about access control lists within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 784eaa7f..82ae9acc 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -13,7 +13,7 @@ from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_ST _LOGGER = getLogger(__name__) -class FileObservation(AbstractObservation, identifier="FILE"): +class FileObservation(AbstractObservation, discriminator="FILE"): """File observation, provides status information about a file within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -125,7 +125,7 @@ class FileObservation(AbstractObservation, identifier="FILE"): ) -class FolderObservation(AbstractObservation, identifier="FOLDER"): +class FolderObservation(AbstractObservation, discriminator="FOLDER"): """Folder observation, provides status information about a folder within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index 6e5fffb9..f0390697 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class FirewallObservation(AbstractObservation, identifier="FIREWALL"): +class FirewallObservation(AbstractObservation, discriminator="FIREWALL"): """Firewall observation, provides status information about a firewall within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index e46cc805..ed4dd21c 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class HostObservation(AbstractObservation, identifier="HOST"): +class HostObservation(AbstractObservation, discriminator="HOST"): """Host observation, provides status information about a host within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/link_observation.py b/src/primaite/game/agent/observations/link_observation.py index 851e9557..303e421c 100644 --- a/src/primaite/game/agent/observations/link_observation.py +++ b/src/primaite/game/agent/observations/link_observation.py @@ -13,7 +13,7 @@ from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_ST _LOGGER = getLogger(__name__) -class LinkObservation(AbstractObservation, identifier="LINK"): +class LinkObservation(AbstractObservation, discriminator="LINK"): """Link observation, providing information about a specific link within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -90,7 +90,7 @@ class LinkObservation(AbstractObservation, identifier="LINK"): return cls(where=where) -class LinksObservation(AbstractObservation, identifier="LINKS"): +class LinksObservation(AbstractObservation, discriminator="LINKS"): """Collection of link observations representing multiple links within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index f87d2d76..4c8fbaf5 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -12,7 +12,7 @@ from primaite.utils.validation.ip_protocol import IPProtocol from primaite.utils.validation.port import Port -class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): +class NICObservation(AbstractObservation, discriminator="NETWORK_INTERFACE"): """Status information about a network interface within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -227,7 +227,7 @@ class NICObservation(AbstractObservation, identifier="NETWORK_INTERFACE"): ) -class PortObservation(AbstractObservation, identifier="PORT"): +class PortObservation(AbstractObservation, discriminator="PORT"): """Port observation, provides status information about a network port within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 1a0f48b4..2937aa7c 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class NodesObservation(AbstractObservation, identifier="NODES"): +class NodesObservation(AbstractObservation, discriminator="NODES"): """Nodes observation, provides status information about nodes within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 83d4a076..0d28aa98 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, computed_field, ConfigDict, Field, model_validat from primaite.game.agent.observations.observations import AbstractObservation, WhereType -class NestedObservation(AbstractObservation, identifier="CUSTOM"): +class NestedObservation(AbstractObservation, discriminator="CUSTOM"): """Observation type that allows combining other observations into a gymnasium.spaces.Dict space.""" class NestedObservationItem(BaseModel): @@ -19,7 +19,7 @@ class NestedObservation(AbstractObservation, identifier="CUSTOM"): model_config = ConfigDict(extra="forbid") type: str - """Select observation class. It maps to the identifier of the obs class by checking the registry.""" + """Select observation class. It maps to the discriminator of the obs class by checking the registry.""" label: str """Dict key in the final observation space.""" options: Dict @@ -119,7 +119,7 @@ class NestedObservation(AbstractObservation, identifier="CUSTOM"): return cls(components=instances) -class NullObservation(AbstractObservation, identifier="NONE"): +class NullObservation(AbstractObservation, discriminator="NONE"): """Empty observation that acts as a placeholder.""" def __init__(self) -> None: @@ -158,7 +158,7 @@ class ObservationManager(BaseModel): model_config = ConfigDict(extra="forbid") type: str = "NONE" - """Identifier name for the top-level observation.""" + """discriminator name for the top-level observation.""" options: AbstractObservation.ConfigSchema = Field( default_factory=lambda: NullObservation.ConfigSchema(), validate_default=True ) @@ -235,7 +235,7 @@ class ObservationManager(BaseModel): :param config: Dictionary containing the configuration for this observation space. If None, a blank observation space is created. Otherwise, this must be a Dict with a type field and options field. - type: string that corresponds to one of the observation identifiers that are provided when subclassing + type: string that corresponds to one of the observation discriminators that are provided when subclassing AbstractObservation options: this must adhere to the chosen observation type's ConfigSchema nested class. :type config: Dict diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index 89c45b37..da81d2ad 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -31,20 +31,20 @@ class AbstractObservation(ABC): """Initialise an observation. This method must be overwritten.""" self.default_observation: ObsType - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: """ Register an observation type. - :param identifier: Identifier used to uniquely specify observation component types. - :type identifier: str + :param discriminator: discriminator used to uniquely specify observation component types. + :type discriminator: str :raises ValueError: When attempting to create a component with a name that is already in use. """ super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - if identifier in cls._registry: - raise ValueError(f"Duplicate observation component type {identifier}") - cls._registry[identifier] = cls + if discriminator in cls._registry: + raise ValueError(f"Duplicate observation component type {discriminator}") + cls._registry[discriminator] = cls @abstractmethod def observe(self, state: Dict) -> Any: diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index ab759779..8eaad1b1 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class RouterObservation(AbstractObservation, identifier="ROUTER"): +class RouterObservation(AbstractObservation, discriminator="ROUTER"): """Router observation, provides status information about a router within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 37810c6e..6e2fbb73 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -10,7 +10,7 @@ from primaite.game.agent.observations.observations import AbstractObservation, W from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -class ServiceObservation(AbstractObservation, identifier="SERVICE"): +class ServiceObservation(AbstractObservation, discriminator="SERVICE"): """Service observation, shows status of a service in the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -73,7 +73,7 @@ class ServiceObservation(AbstractObservation, identifier="SERVICE"): return cls(where=parent_where + ["services", config.service_name]) -class ApplicationObservation(AbstractObservation, identifier="APPLICATION"): +class ApplicationObservation(AbstractObservation, discriminator="APPLICATION"): """Application observation, shows the status of an application within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 80be14ef..3e961bdf 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -55,13 +55,13 @@ class AbstractReward(BaseModel): _registry: ClassVar[Dict[str, Type["AbstractReward"]]] = {} - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - if identifier in cls._registry: - raise ValueError(f"Duplicate reward {identifier}") - cls._registry[identifier] = cls + if discriminator in cls._registry: + raise ValueError(f"Duplicate reward {discriminator}") + cls._registry[discriminator] = cls @classmethod def from_config(cls, config: Dict) -> "AbstractReward": @@ -92,7 +92,7 @@ class AbstractReward(BaseModel): return 0.0 -class DummyReward(AbstractReward, identifier="DUMMY"): +class DummyReward(AbstractReward, discriminator="DUMMY"): """Dummy reward function component which always returns 0.0.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: @@ -108,7 +108,7 @@ class DummyReward(AbstractReward, identifier="DUMMY"): return 0.0 -class DatabaseFileIntegrity(AbstractReward, identifier="DATABASE_FILE_INTEGRITY"): +class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRITY"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" config: "DatabaseFileIntegrity.ConfigSchema" @@ -161,7 +161,7 @@ class DatabaseFileIntegrity(AbstractReward, identifier="DATABASE_FILE_INTEGRITY" return 0 -class WebServer404Penalty(AbstractReward, identifier="WEB_SERVER_404_PENALTY"): +class WebServer404Penalty(AbstractReward, discriminator="WEB_SERVER_404_PENALTY"): """Reward function component which penalises the agent when the web server returns a 404 error.""" config: "WebServer404Penalty.ConfigSchema" @@ -215,7 +215,7 @@ class WebServer404Penalty(AbstractReward, identifier="WEB_SERVER_404_PENALTY"): return self.reward -class WebpageUnavailablePenalty(AbstractReward, identifier="WEBPAGE_UNAVAILABLE_PENALTY"): +class WebpageUnavailablePenalty(AbstractReward, discriminator="WEBPAGE_UNAVAILABLE_PENALTY"): """Penalises the agent when the web browser fails to fetch a webpage.""" config: "WebpageUnavailablePenalty.ConfigSchema" @@ -289,7 +289,7 @@ class WebpageUnavailablePenalty(AbstractReward, identifier="WEBPAGE_UNAVAILABLE_ return self.reward -class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY"): +class GreenAdminDatabaseUnreachablePenalty(AbstractReward, discriminator="GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY"): """Penalises the agent when the green db clients fail to connect to the database.""" config: "GreenAdminDatabaseUnreachablePenalty.ConfigSchema" @@ -339,7 +339,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, identifier="GREEN_ADM return self.reward -class SharedReward(AbstractReward, identifier="SHARED_REWARD"): +class SharedReward(AbstractReward, discriminator="SHARED_REWARD"): """Adds another agent's reward to the overall reward.""" config: "SharedReward.ConfigSchema" @@ -376,7 +376,7 @@ class SharedReward(AbstractReward, identifier="SHARED_REWARD"): return self.callback(self.config.agent_name) -class ActionPenalty(AbstractReward, identifier="ACTION_PENALTY"): +class ActionPenalty(AbstractReward, discriminator="ACTION_PENALTY"): """Apply a negative reward when taking any action except do_nothing.""" config: "ActionPenalty.ConfigSchema" diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index e6ddd546..f36c93de 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -13,7 +13,7 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "AbstractTAPAgent" -class AbstractTAPAgent(PeriodicAgent, identifier="AbstractTAP"): +class AbstractTAPAgent(PeriodicAgent, discriminator="AbstractTAP"): """Base class for TAP agents to inherit from.""" config: "AbstractTAPAgent.ConfigSchema" = Field(default_factory=lambda: AbstractTAPAgent.ConfigSchema()) diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index a7558d42..b32df428 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -9,7 +9,7 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "DataManipulationAgent" -class DataManipulationAgent(PeriodicAgent, identifier="RedDatabaseCorruptingAgent"): +class DataManipulationAgent(PeriodicAgent, discriminator="RedDatabaseCorruptingAgent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" class AgentSettingsSchema(PeriodicAgent.AgentSettingsSchema): diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index de643ed8..2ddc39b7 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -13,7 +13,7 @@ from primaite.game.agent.interface import AbstractScriptedAgent __all__ = "ProbabilisticAgent" -class ProbabilisticAgent(AbstractScriptedAgent, identifier="ProbabilisticAgent"): +class ProbabilisticAgent(AbstractScriptedAgent, discriminator="ProbabilisticAgent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" rng: Generator = Field(default_factory=lambda: np.random.default_rng(np.random.randint(0, 65535))) diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 9cf8e798..3d652dfc 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -11,7 +11,7 @@ from primaite.game.agent.interface import AbstractScriptedAgent __all__ = ("RandomAgent", "PeriodicAgent") -class RandomAgent(AbstractScriptedAgent, identifier="RandomAgent"): +class RandomAgent(AbstractScriptedAgent, discriminator="RandomAgent"): """Agent that ignores its observation and acts completely at random.""" config: "RandomAgent.ConfigSchema" = Field(default_factory=lambda: RandomAgent.ConfigSchema()) @@ -34,7 +34,7 @@ class RandomAgent(AbstractScriptedAgent, identifier="RandomAgent"): return self.action_manager.get_action(self.action_manager.space.sample()) -class PeriodicAgent(AbstractScriptedAgent, identifier="PeriodicAgent"): +class PeriodicAgent(AbstractScriptedAgent, discriminator="PeriodicAgent"): """Agent that does nothing most of the time, but executes application at regular intervals (with variance).""" config: "PeriodicAgent.ConfigSchema" = Field(default_factory=lambda: PeriodicAgent.ConfigSchema()) diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb index 7af8b98e..690e7856 100644 --- a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb +++ b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb @@ -172,7 +172,7 @@ "\n", "\n", "# no applications exist yet so we will create our own.\n", - "class MSPaint(Application, identifier=\"MSPaint\"):\n", + "class MSPaint(Application, discriminator=\"MSPaint\"):\n", " def describe_state(self):\n", " return super().describe_state()" ] diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 567a0493..7ccd202e 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -244,7 +244,7 @@ class SimComponent(BaseModel): ..code::python - class WebBrowser(Application, identifier="WebBrowser"): + class WebBrowser(Application, discriminator="WebBrowser"): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() # all requests generic to any Application get initialised rm.add_request(...) # initialise any requests specific to the web browser diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index bf677d5c..f5ae0232 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -180,7 +180,7 @@ class Network(SimComponent): table.align = "l" table.title = "Nodes" for node in self.nodes.values(): - table.add_row((node.hostname, type(node)._identifier, node.operating_state.name)) + table.add_row((node.hostname, type(node)._discriminator, node.operating_state.name)) print(table) if ip_addresses: diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 2cf8774e..e16a7fcc 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -22,7 +22,7 @@ class NetworkNodeAdder(BaseModel): Here is a template that users can use to define custom node adders: ``` - class YourNodeAdder(NetworkNodeAdder, identifier="your_name"): + class YourNodeAdder(NetworkNodeAdder, discriminator="your_name"): class ConfigSchema(NetworkNodeAdder.ConfigSchema): property_1 : str property_2 : int @@ -40,8 +40,8 @@ class NetworkNodeAdder(BaseModel): """ Base schema for node adders. - Child classes of NetworkNodeAdder must define a schema which inherits from this schema. The identifier is used - by the from_config method to select the correct node adder at runtime. + Child classes of NetworkNodeAdder must define a schema which inherits from this schema. The discriminator is + used by the from_config method to select the correct node adder at runtime. """ model_config = ConfigDict(extra="forbid") @@ -50,20 +50,20 @@ class NetworkNodeAdder(BaseModel): _registry: ClassVar[Dict[str, Type["NetworkNodeAdder"]]] = {} - def __init_subclass__(cls, identifier: Optional[str], **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str], **kwargs: Any) -> None: """ Register a network node adder class. - :param identifier: Unique name for the node adder to use for matching against primaite config entries. - :type identifier: str + :param discriminator: Unique name for the node adder to use for matching against primaite config entries. + :type discriminator: str :raises ValueError: When attempting to register a name that is already reserved. """ super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - if identifier in cls._registry: - raise ValueError(f"Duplicate node adder {identifier}") - cls._registry[identifier] = cls + if discriminator in cls._registry: + raise ValueError(f"Duplicate node adder {discriminator}") + cls._registry[discriminator] = cls @classmethod @abstractmethod @@ -99,7 +99,7 @@ class NetworkNodeAdder(BaseModel): adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config), network=network) -class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): +class OfficeLANAdder(NetworkNodeAdder, discriminator="office_lan"): """Creates an office LAN.""" class ConfigSchema(NetworkNodeAdder.ConfigSchema): diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ecbd0629..bacba15b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -824,7 +824,7 @@ class User(SimComponent): return self.model_dump() -class UserManager(Service, identifier="UserManager"): +class UserManager(Service, discriminator="UserManager"): """ Manages users within the PrimAITE system, handling creation, authentication, and administration. @@ -1137,7 +1137,7 @@ class RemoteUserSession(UserSession): return state -class UserSessionManager(Service, identifier="UserSessionManager"): +class UserSessionManager(Service, discriminator="UserSessionManager"): """ Manages user sessions on a Node, including local and remote sessions. @@ -1483,7 +1483,7 @@ class UserSessionManager(Service, identifier="UserSessionManager"): return self.local_session is not None -class Node(SimComponent): +class Node(SimComponent, ABC): """ A basic Node class that represents a node on the network. @@ -1556,25 +1556,26 @@ class Node(SimComponent): _registry: ClassVar[Dict[str, Type["Node"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" - _identifier: ClassVar[str] = "unknown" - """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" + # TODO: this should not be set for abstract classes. + _discriminator: ClassVar[str] + """discriminator for this particular class, used for printing and logging. Each subclass redefines this.""" - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: """ Register a node type. - :param identifier: Uniquely specifies an node class by name. Used for finding items by config. - :type identifier: str + :param discriminator: Uniquely specifies an node class by name. Used for finding items by config. + :type discriminator: str :raises ValueError: When attempting to register an node with a name that is already allocated. """ super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - identifier = identifier.lower() - if identifier in cls._registry: - raise ValueError(f"Tried to define new node {identifier}, but this name is already reserved.") - cls._registry[identifier] = cls - cls._identifier = identifier + discriminator = discriminator.lower() + if discriminator in cls._registry: + raise ValueError(f"Tried to define new node {discriminator}, but this name is already reserved.") + cls._registry[discriminator] = cls + cls._discriminator = discriminator def __init__(self, **kwargs): """ diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 11b925b9..a47af2ad 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -5,7 +5,7 @@ from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.system.services.ftp.ftp_client import FTPClient -class Computer(HostNode, identifier="computer"): +class Computer(HostNode, discriminator="computer"): """ A basic Computer class. diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index c51afbca..f8786a08 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -262,7 +262,7 @@ class NIC(IPWiredNetworkInterface): return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" -class HostNode(Node, identifier="HostNode"): +class HostNode(Node, discriminator="HostNode"): """ Represents a host node in the network. diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index e16cfd8f..50b82122 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -2,7 +2,7 @@ from primaite.simulator.network.hardware.nodes.host.host_node import HostNode -class Server(HostNode, identifier="server"): +class Server(HostNode, discriminator="server"): """ A basic Server class. @@ -31,7 +31,7 @@ class Server(HostNode, identifier="server"): """ -class Printer(HostNode, identifier="printer"): +class Printer(HostNode, discriminator="printer"): """Printer? I don't even know her!.""" # TODO: Implement printer-specific behaviour diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index ac7c12e3..4da9e24c 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -27,7 +27,7 @@ DMZ_PORT_ID: Final[int] = 3 """The Firewall port ID of the DMZ port.""" -class Firewall(Router, identifier="firewall"): +class Firewall(Router, discriminator="firewall"): """ A Firewall class that extends the functionality of a Router. diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 22ff2b28..185b6bae 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -7,7 +7,7 @@ from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.services.arp.arp import ARP -class NetworkNode(Node, identifier="NetworkNode"): +class NetworkNode(Node, discriminator="NetworkNode"): """ Represents an abstract base class for a network node that can receive and process network frames. diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 4a049f99..b6004e8e 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1184,7 +1184,7 @@ class RouterSessionManager(SessionManager): return outbound_network_interface, dst_mac_address, dst_ip_address, src_port, dst_port, protocol, is_broadcast -class Router(NetworkNode, identifier="router"): +class Router(NetworkNode, discriminator="router"): """ Represents a network router, managing routing and forwarding of IP packets across network interfaces. diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index db923f1a..f06337aa 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -87,7 +87,7 @@ class SwitchPort(WiredNetworkInterface): return False -class Switch(NetworkNode, identifier="switch"): +class Switch(NetworkNode, discriminator="switch"): """ A class representing a Layer 2 network switch. diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 804a570e..87408670 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -91,7 +91,7 @@ class WirelessAccessPoint(IPWirelessNetworkInterface): ) -class WirelessRouter(Router, identifier="wireless_router"): +class WirelessRouter(Router, discriminator="wireless_router"): """ A WirelessRouter class that extends the functionality of a standard Router to include wireless capabilities. diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 05a47d7a..1de29c33 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -53,20 +53,20 @@ class Application(IOSoftware, ABC): _registry: ClassVar[Dict[str, Type["Application"]]] = {} """Registry of application types. Automatically populated when subclasses are defined.""" - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: """ Register an application type. - :param identifier: Uniquely specifies an application class by name. Used for finding items by config. - :type identifier: Optional[str] + :param discriminator: Uniquely specifies an application class by name. Used for finding items by config. + :type discriminator: Optional[str] :raises ValueError: When attempting to register an application with a name that is already allocated. """ super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return - if identifier in cls._registry: - raise ValueError(f"Tried to define new application {identifier}, but this name is already reserved.") - cls._registry[identifier] = cls + if discriminator in cls._registry: + raise ValueError(f"Tried to define new application {discriminator}, but this name is already reserved.") + cls._registry[discriminator] = cls @classmethod def from_config(cls, config: Dict) -> "Application": diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 96130e16..67749e21 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -61,7 +61,7 @@ class DatabaseClientConnection(BaseModel): return str(self) -class DatabaseClient(Application, identifier="DatabaseClient"): +class DatabaseClient(Application, discriminator="DatabaseClient"): """ A DatabaseClient application. diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 3eeda4b6..6a29aedf 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -44,7 +44,7 @@ class PortScanPayload(SimComponent): return state -class NMAP(Application, identifier="NMAP"): +class NMAP(Application, discriminator="NMAP"): """ A class representing the NMAP application for network scanning. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index b989671e..14e446a4 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -17,7 +17,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import Port, PORT_LOOKUP -class C2Beacon(AbstractC2, identifier="C2Beacon"): +class C2Beacon(AbstractC2, discriminator="C2Beacon"): """ C2 Beacon Application. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 9d2097e9..df4c34a8 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -16,7 +16,7 @@ from primaite.simulator.system.applications.red_applications.c2 import ( from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload -class C2Server(AbstractC2, identifier="C2Server"): +class C2Server(AbstractC2, discriminator="C2Server"): """ C2 Server Application. diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 392cdfba..7ad31e3b 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -40,7 +40,7 @@ class DataManipulationAttackStage(IntEnum): "Signifies that the attack has failed." -class DataManipulationBot(Application, identifier="DataManipulationBot"): +class DataManipulationBot(Application, discriminator="DataManipulationBot"): """A bot that simulates a script which performs a SQL injection attack.""" class ConfigSchema(Application.ConfigSchema): diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index a6cb2b75..6153c2a5 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -32,7 +32,7 @@ class DoSAttackStage(IntEnum): "Attack is completed." -class DoSBot(DatabaseClient, identifier="DoSBot"): +class DoSBot(DatabaseClient, discriminator="DoSBot"): """A bot that simulates a Denial of Service attack.""" class ConfigSchema(DatabaseClient.ConfigSchema): diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 114d5716..0a818a85 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -14,7 +14,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP -class RansomwareScript(Application, identifier="RansomwareScript"): +class RansomwareScript(Application, discriminator="RansomwareScript"): """Ransomware Kill Chain - Designed to be used by the TAP001 Agent on the example layout Network. :ivar payload: The attack stage query payload. (Default ENCRYPT) diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 49f303b5..3eb18f7f 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -23,7 +23,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class WebBrowser(Application, identifier="WebBrowser"): +class WebBrowser(Application, discriminator="WebBrowser"): """ Represents a web browser in the simulation environment. diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index bbeec301..311f7e25 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -15,7 +15,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP -class ARP(Service, identifier="ARP"): +class ARP(Service, discriminator="ARP"): """ The ARP (Address Resolution Protocol) Service. diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 1745b9d1..369905db 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class DatabaseService(Service, identifier="DatabaseService"): +class DatabaseService(Service, discriminator="DatabaseService"): """ A class for simulating a generic SQL Server service. diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 825896e0..83a14033 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: _LOGGER = getLogger(__name__) -class DNSClient(Service, identifier="DNSClient"): +class DNSClient(Service, discriminator="DNSClient"): """Represents a DNS Client as a Service.""" class ConfigSchema(Service.ConfigSchema): diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 41a5b25f..ef19a13e 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -14,7 +14,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class DNSServer(Service, identifier="DNSServer"): +class DNSServer(Service, discriminator="DNSServer"): """Represents a DNS Server as a Service.""" class ConfigSchema(Service.ConfigSchema): diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 82875b97..23b55330 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class FTPClient(FTPServiceABC, identifier="FTPClient"): +class FTPClient(FTPServiceABC, discriminator="FTPClient"): """ A class for simulating an FTP client service. diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 5f4ac846..43184684 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -12,7 +12,7 @@ from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class FTPServer(FTPServiceABC, identifier="FTPServer"): +class FTPServer(FTPServiceABC, discriminator="FTPServer"): """ A class for simulating an FTP server service. diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 7f626945..77dbd5be 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -16,7 +16,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ICMP(Service, identifier="ICMP"): +class ICMP(Service, discriminator="ICMP"): """ The Internet Control Message Protocol (ICMP) service. diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index b5f921c9..e3af43f7 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -15,7 +15,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class NTPClient(Service, identifier="NTPClient"): +class NTPClient(Service, discriminator="NTPClient"): """Represents a NTP client as a service.""" class ConfigSchema(Service.ConfigSchema): diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index 7af33893..b2d8356c 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -13,7 +13,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class NTPServer(Service, identifier="NTPServer"): +class NTPServer(Service, discriminator="NTPServer"): """Represents a NTP server as a service.""" class ConfigSchema(Service.ConfigSchema): diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index c4a73301..a7b8fd09 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -61,22 +61,22 @@ class Service(IOSoftware): def __init__(self, **kwargs): super().__init__(**kwargs) - def __init_subclass__(cls, identifier: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: """ Register a hostnode type. - :param identifier: Uniquely specifies an hostnode class by name. Used for finding items by config. - :type identifier: str + :param discriminator: Uniquely specifies an hostnode class by name. Used for finding items by config. + :type discriminator: str :raises ValueError: When attempting to register an hostnode with a name that is already allocated. """ super().__init_subclass__(**kwargs) - if identifier is None: + if discriminator is None: return # Enforce lowercase registry entries because it makes comparisons everywhere else much easier. - identifier = identifier.lower() - if identifier in cls._registry: - raise ValueError(f"Tried to define new hostnode {identifier}, but this name is already reserved.") - cls._registry[identifier] = cls + discriminator = discriminator.lower() + if discriminator in cls._registry: + raise ValueError(f"Tried to define new hostnode {discriminator}, but this name is already reserved.") + cls._registry[discriminator] = cls @classmethod def from_config(cls, config: Dict) -> "Service": diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index bda8bad3..01d9095b 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -129,7 +129,7 @@ class RemoteTerminalConnection(TerminalClientConnection): return self.parent_terminal.send(payload=payload, session_id=self.ssh_session_id) -class Terminal(Service, identifier="Terminal"): +class Terminal(Service, discriminator="Terminal"): """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" class ConfigSchema(Service.ConfigSchema): diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 51724002..40a713a5 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -21,7 +21,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class WebServer(Service, identifier="WebServer"): +class WebServer(Service, discriminator="WebServer"): """Class used to represent a Web Server Service in simulation.""" class ConfigSchema(Service.ConfigSchema): diff --git a/tests/conftest.py b/tests/conftest.py index 165ab30e..70443042 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ ACTION_SPACE_NODE_ACTION_VALUES = 1 _LOGGER = getLogger(__name__) -class DummyService(Service, identifier="DummyService"): +class DummyService(Service, discriminator="DummyService"): """Test Service class""" class ConfigSchema(Service.ConfigSchema): @@ -62,7 +62,7 @@ class DummyService(Service, identifier="DummyService"): pass -class DummyApplication(Application, identifier="DummyApplication"): +class DummyApplication(Application, discriminator="DummyApplication"): """Test Application class""" class ConfigSchema(Application.ConfigSchema): @@ -280,7 +280,7 @@ def example_network() -> Network: return network -class ControlledAgent(AbstractAgent, identifier="ControlledAgent"): +class ControlledAgent(AbstractAgent, discriminator="ControlledAgent"): """Agent that can be controlled by the tests.""" config: "ControlledAgent.ConfigSchema" = Field(default_factory=lambda: ControlledAgent.ConfigSchema()) diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index 159cfd06..fd6fea3f 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -24,7 +24,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ExtendedApplication(Application, identifier="ExtendedApplication"): +class ExtendedApplication(Application, discriminator="ExtendedApplication"): """ Clone of web browser that uses the extension framework instead of being part of PrimAITE directly. diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index 37a05b6e..86da0610 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -11,7 +11,7 @@ from primaite.simulator.network.hardware.nodes.network.switch import SwitchPort from primaite.simulator.network.transmission.data_link_layer import Frame -class GigaSwitch(NetworkNode, identifier="gigaswitch"): +class GigaSwitch(NetworkNode, discriminator="gigaswitch"): """ A class representing a Layer 2 network switch. diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 4af1b748..99c5fdf5 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -6,7 +6,7 @@ from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.utils.validation.ipv4_address import IPV4Address -class SuperComputer(HostNode, identifier="supercomputer"): +class SuperComputer(HostNode, discriminator="supercomputer"): """ A basic Computer class. diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index ba247369..79821b6c 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ExtendedService(Service, identifier="ExtendedService"): +class ExtendedService(Service, discriminator="ExtendedService"): """ A copy of DatabaseService that uses the extension framework instead of being part of PrimAITE. diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index ed40334f..d2ec06ae 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -15,7 +15,7 @@ from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP -class BroadcastTestService(Service, identifier="BroadcastTestService"): +class BroadcastTestService(Service, discriminator="BroadcastTestService"): """A service for sending broadcast and unicast messages over a network.""" class ConfigSchema(Service.ConfigSchema): @@ -51,7 +51,7 @@ class BroadcastTestService(Service, identifier="BroadcastTestService"): ) -class BroadcastTestClient(Application, identifier="BroadcastTestClient"): +class BroadcastTestClient(Application, discriminator="BroadcastTestClient"): """A client application to receive broadcast and unicast messages.""" class ConfigSchema(Service.ConfigSchema): 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 84413ac9..db5381d0 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -14,7 +14,7 @@ from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT -class _DatabaseListener(Service, identifier="_DatabaseListener"): +class _DatabaseListener(Service, discriminator="_DatabaseListener"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for _DatabaseListener.""" diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py index 16a4c9ad..9e448b87 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py @@ -5,14 +5,14 @@ from primaite.simulator.system.applications.application import Application def test_adding_to_app_registry(): - class temp_application(Application, identifier="temp_app"): + class temp_application(Application, discriminator="temp_app"): pass assert Application._registry["temp_app"] is temp_application with pytest.raises(ValueError): - class another_application(Application, identifier="temp_app"): + class another_application(Application, discriminator="temp_app"): pass # This is kinda evil... diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index bdf9cfee..12cb736d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -11,7 +11,7 @@ from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP -class TestSoftware(Service, identifier="TestSoftware"): +class TestSoftware(Service, discriminator="TestSoftware"): class ConfigSchema(Service.ConfigSchema): """ConfigSChema for TestSoftware.""" From d806391625a1569eba51f1cfbbc877b420909b87 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 31 Jan 2025 18:46:02 +0000 Subject: [PATCH 170/224] #2887 - Test fixes --- src/primaite/simulator/network/hardware/base.py | 2 +- .../network/hardware/nodes/network/wireless_router.py | 1 + .../game_layer/observations/test_firewall_observation.py | 4 ++-- tests/integration_tests/system/test_database_on_node.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 8cbe2b87..732b79f5 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1564,7 +1564,7 @@ class Node(SimComponent, ABC): "Time steps until reveal to red scan is complete." dns_server: Optional[IPv4Address] = None - "List of IP addresses of DNS servers used for name resolution." + "List of IP addresses of DNS servers used for name resolution." default_gateway: Optional[IPV4Address] = None "The default gateway IP address for forwarding network traffic to other networks." diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 5e52de7e..348c2aaa 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -130,6 +130,7 @@ class WirelessRouter(Router, identifier="wireless_router"): hostname: str = "WirelessRouter" airspace: AirSpace + num_ports: int = 0 def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/tests/integration_tests/game_layer/observations/test_firewall_observation.py b/tests/integration_tests/game_layer/observations/test_firewall_observation.py index 17c7775f..874fa49e 100644 --- a/tests/integration_tests/game_layer/observations/test_firewall_observation.py +++ b/tests/integration_tests/game_layer/observations/test_firewall_observation.py @@ -25,7 +25,7 @@ def check_default_rules(acl_obs): def test_firewall_observation(): """Test adding/removing acl rules and enabling/disabling ports.""" net = Network() - firewall_cfg = {"type": "firewall", "hostname": "firewall", "opertating_state": NodeOperatingState.ON} + firewall_cfg = {"type": "firewall", "hostname": "firewall"} firewall = Firewall.from_config(config=firewall_cfg) firewall_observation = FirewallObservation( where=[], @@ -118,7 +118,7 @@ def test_firewall_observation(): # connect a switch to the firewall and check that only the correct port is updated switch: Switch = Switch.from_config( - config={"type": "switch", "hostname": "switch", "num_ports": 1, "operating_state": NodeOperatingState.ON} + config={"type": "switch", "hostname": "switch", "num_ports": 1, "operating_state": "ON"} ) link = net.connect(firewall.network_interface[1], switch.network_interface[1]) assert firewall.network_interface[1].enabled diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 8ad292b2..59e50659 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -62,7 +62,7 @@ def peer_to_peer_secure_db(peer_to_peer) -> Tuple[Computer, Computer]: database_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa database_service.stop() - database_service.password = "12345" + database_service.config.db_password = "12345" database_service.start() return node_a, node_b From 3d01f52eea304906022c426427b04f8ef0ecbc84 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 3 Feb 2025 11:18:34 +0000 Subject: [PATCH 171/224] #2887 - Merge in changes on dev to resolve conflicts. All tests should now pass --- src/primaite/game/game.py | 1 - .../simulator/network/hardware/base.py | 8 +++-- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 5 ++- src/primaite/simulator/network/networks.py | 32 +++++++++++-------- .../system/services/ftp/ftp_server.py | 1 - .../system/services/ntp/ntp_client.py | 1 - tests/conftest.py | 6 ++-- .../nodes/test_node_config.py | 2 +- .../game_layer/test_observations.py | 7 ++-- 10 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index b52d506f..e9941a12 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,6 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """PrimAITE game - Encapsulates the simulation and agents.""" -from ipaddress import IPv4Address from typing import Dict, List, Optional, Union import numpy as np diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 23ffbcf8..06b0dbe4 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1493,6 +1493,7 @@ class Node(SimComponent, ABC): :param hostname: The node hostname on the network. :param operating_state: The node operating state, either ON or OFF. """ + operating_state: NodeOperatingState = NodeOperatingState.OFF "The hardware state of the node." network_interfaces: Dict[str, NetworkInterface] = {} @@ -1564,7 +1565,7 @@ class Node(SimComponent, ABC): "Time steps until reveal to red scan is complete." dns_server: Optional[IPv4Address] = None - "List of IP addresses of DNS servers used for name resolution." + "List of IP addresses of DNS servers used for name resolution." default_gateway: Optional[IPV4Address] = None "The default gateway IP address for forwarding network traffic to other networks." @@ -1573,6 +1574,7 @@ class Node(SimComponent, ABC): @property def dns_server(self) -> Optional[IPv4Address]: + """Convenience method to access the dns_server IP.""" return self.config.dns_server @classmethod @@ -1625,7 +1627,9 @@ class Node(SimComponent, ABC): dns_server=kwargs["config"].dns_server, ) super().__init__(**kwargs) - self.operating_state = NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else NodeOperatingState[p.upper()] + self.operating_state = ( + NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else NodeOperatingState[p.upper()] + ) self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 1aebc3af..85857a44 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -37,7 +37,7 @@ class Computer(HostNode, identifier="computer"): SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} - config: "Computer.ConfigSchema" = Field(default_factory=lambda: Computer.ConfigSchema()) + config: "Computer.ConfigSchema" = Field(default_factory=lambda: Computer.ConfigSchema()) class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Computer class.""" diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 3b1d8e48..424e39f1 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -55,7 +55,10 @@ class HostARP(ARP): :return: The NIC associated with the default gateway if it exists in the ARP cache; otherwise, None. """ - if self.software_manager.node.config.default_gateway and self.software_manager.node.has_enabled_network_interface: + if ( + self.software_manager.node.config.default_gateway + and self.software_manager.node.has_enabled_network_interface + ): return self.get_arp_cache_network_interface(self.software_manager.node.config.default_gateway) def _get_arp_cache_mac_address( diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 644b2a4a..75978bee 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -58,24 +58,28 @@ def client_server_routed() -> Network: router_1.enable_port(2) # Client 1 - client_1 = Computer(config=dict( - hostname="client_1", - ip_address="192.168.2.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.2.1", - start_up_duration=0, - )) + client_1 = Computer( + config=dict( + hostname="client_1", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.2.1", + start_up_duration=0, + ) + ) client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) # Server 1 - server_1 = Server(config=dict( - hostname="server_1", - ip_address="192.168.1.2", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - start_up_duration=0, - )) + server_1 = Server( + config=dict( + hostname="server_1", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + ) server_1.power_on() network.connect(endpoint_b=server_1.network_interface[1], endpoint_a=switch_1.network_interface[1]) diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 623bdf90..ebb93a7b 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -43,7 +43,6 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): """Convenience method for accessing FTP server password.""" return self.config.server_password - def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ Process the command in the FTP Packet. diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 81947395..72e8f6c0 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -29,7 +29,6 @@ class NTPClient(Service, identifier="NTPClient"): config: "NTPClient.ConfigSchema" = Field(default_factory=lambda: NTPClient.ConfigSchema()) - time: Optional[datetime] = None def __init__(self, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py index b3cac34a..c1d44aec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -232,7 +232,7 @@ def example_network() -> Network: # Router 1 - router_1_cfg = {"hostname": "router_1", "type": "router", "start_up_duration":0} + router_1_cfg = {"hostname": "router_1", "type": "router", "start_up_duration": 0} # router_1 = Router(hostname="router_1", start_up_duration=0) router_1 = Router.from_config(config=router_1_cfg) @@ -253,7 +253,7 @@ def example_network() -> Network: router_1.enable_port(1) # Switch 2 - switch_2_config = {"hostname": "switch_2", "type": "switch", "num_ports": 8, "start_up_duration":0} + switch_2_config = {"hostname": "switch_2", "type": "switch", "num_ports": 8, "start_up_duration": 0} # switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) switch_2 = Switch.from_config(config=switch_2_config) switch_2.power_on() @@ -387,7 +387,7 @@ def install_stuff_to_sim(sim: Simulation): "ip_address": "10.0.1.2", "subnet_mask": "255.255.255.0", "default_gateway": "10.0.1.1", - "start_up_duration":0, + "start_up_duration": 0, } client_1: Computer = Computer.from_config(config=client_1_cfg) client_1.power_on() diff --git a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py index 6ccbf4e1..f3911691 100644 --- a/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py +++ b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py @@ -3,8 +3,8 @@ from primaite.config.load import data_manipulation_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from primaite.simulator.network.hardware.nodes.network.firewall import Firewall +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index 090725b5..17b9b71e 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -8,10 +8,9 @@ from primaite.simulator.sim_container import Simulation def test_file_observation(): sim = Simulation() - pc: Computer = Computer.from_config(config={"type":"computer", - "hostname":"beep", - "ip_address":"123.123.123.123", - "subnet_mask":"255.255.255.0"}) + pc: Computer = Computer.from_config( + config={"type": "computer", "hostname": "beep", "ip_address": "123.123.123.123", "subnet_mask": "255.255.255.0"} + ) sim.network.add_node(pc) f = pc.file_system.create_file(file_name="dog.png") From 0920ec5f5b47a43be2d93e6f22f8af6ae98eb0f5 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 3 Feb 2025 11:32:07 +0000 Subject: [PATCH 172/224] #2887 - Remove debug print statements --- src/primaite/simulator/network/hardware/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 06b0dbe4..0564e1f3 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -2100,17 +2100,14 @@ class Node(SimComponent, ABC): def power_on(self) -> bool: """Power on the Node, enabling its NICs if it is in the OFF state.""" - print("HI") if self.config.start_up_duration <= 0: self.operating_state = NodeOperatingState.ON - print(f"Powering On: f{self.config.hostname}") self._start_up_actions() self.sys_log.info("Power on") for network_interface in self.network_interfaces.values(): network_interface.enable() return True if self.operating_state == NodeOperatingState.OFF: - print("OOOOOOOOOOOOOOOOOOH") self.operating_state = NodeOperatingState.BOOTING self.config.start_up_countdown = self.config.start_up_duration return True From 32de95917e636c8febd08d38aa41492e7e02c606 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 3 Feb 2025 11:33:21 +0000 Subject: [PATCH 173/224] #3075: Add repeat parameter to config file. --- .../notebooks/Data-Manipulation-E2E-Demonstration.ipynb | 3 +-- .../applications/red_applications/data_manipulation_bot.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 3818bb18..4a34619f 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -405,8 +405,7 @@ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.game.agent.interface import AgentHistoryItem\n", "import yaml\n", - "from pprint import pprint\n", - "from primaite.game.agent.scripted_agents import probabilistic_agent, data_manipulation_bot\n" + "from pprint import pprint\n" ] }, { diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 392cdfba..79ef1e67 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -52,6 +52,7 @@ class DataManipulationBot(Application, identifier="DataManipulationBot"): payload: str = "DELETE" port_scan_p_of_success: float = 0.1 data_manipulation_p_of_success: float = 0.1 + repeat: bool = True config: "DataManipulationBot.ConfigSchema" = Field(default_factory=lambda: DataManipulationBot.ConfigSchema()) @@ -76,6 +77,7 @@ class DataManipulationBot(Application, identifier="DataManipulationBot"): self.payload = self.config.payload self.port_scan_p_of_success = self.config.port_scan_p_of_success self.data_manipulation_p_of_success = self.config.data_manipulation_p_of_success + self.repeat = self.config.repeat def describe_state(self) -> Dict: """ From f3bbfffe7f4503bd4d14ddbc2d7c08766962c8eb Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 3 Feb 2025 14:03:21 +0000 Subject: [PATCH 174/224] #2887 - Update CHANGELOG.md --- CHANGELOG.md | 2 ++ docs/source/how_to_guides/extensible_nodes.rst | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c91bf4f4..7f87f54e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Relabeled action parameters to match the new action config schemas, and updated the values to no longer rely on indices - Removed action space options which were previously used for assigning meaning to action space IDs - Updated tests that don't use YAMLs to still use the new action and agent schemas +- Nodes now use a config schema and are extensible, allowing for plugin support. +- Node tests have been updated to use the new node config schemas when not using YAML files. ### Fixed - DNS client no longer fails to check its cache if a DNS server address is missing. diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index f0b78b08..78ee550e 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -48,8 +48,6 @@ class Router(NetworkNode, identifier="router"): hostname: str = "Router" - ports: list = [] - Changes to YAML file. From abccf4afc5561f1bec310bb806c47658fd2289ad Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 3 Feb 2025 16:24:03 +0000 Subject: [PATCH 175/224] #3062 - First pass at unifying naming convention for discriminators (still errors) [skip ci] --- docs/source/action_masking.rst | 112 ++--- docs/source/configuration/agents.rst | 20 +- .../simulation/nodes/network_examples.rst | 32 +- docs/source/game_layer.rst | 8 +- .../how_to_guides/extensible_actions.rst | 4 +- .../how_to_guides/extensible_agents.rst | 4 +- .../how_to_guides/extensible_rewards.rst | 4 +- docs/source/node_sets.rst | 8 +- docs/source/request_system.rst | 4 +- docs/source/rewards.rst | 14 +- .../system/applications/c2_suite.rst | 4 +- .../applications/data_manipulation_bot.rst | 8 +- .../system/applications/database_client.rst | 2 +- .../system/applications/dos_bot.rst | 38 +- .../system/applications/web_browser.rst | 2 +- .../system/services/database_service.rst | 2 +- .../system/services/dns_client.rst | 2 +- .../system/services/dns_server.rst | 2 +- .../system/services/ftp_client.rst | 2 +- .../system/services/ftp_server.rst | 2 +- .../system/services/ntp_client.rst | 2 +- .../system/services/ntp_server.rst | 2 +- .../system/services/web_server.rst | 2 +- .../_package_data/data_manipulation.yaml | 260 +++++----- .../_package_data/data_manipulation_marl.yaml | 456 +++++++++--------- .../base_scenario.yaml | 20 +- .../simulation_variant_1.yaml | 4 +- .../simulation_variant_2.yaml | 4 +- .../multi_lan_internet_network_example.yaml | 40 +- .../scenario_with_placeholders/greens_1.yaml | 10 +- .../scenario_with_placeholders/greens_2.yaml | 10 +- .../scenario_with_placeholders/reds_1.yaml | 4 +- .../scenario_with_placeholders/reds_2.yaml | 4 +- .../scenario_with_placeholders/scenario.yaml | 34 +- src/primaite/game/agent/actions/acl.py | 20 +- .../game/agent/actions/application.py | 12 +- src/primaite/game/agent/actions/file.py | 24 +- src/primaite/game/agent/actions/folder.py | 14 +- src/primaite/game/agent/actions/host_nic.py | 6 +- src/primaite/game/agent/actions/manager.py | 12 +- src/primaite/game/agent/actions/network.py | 8 +- src/primaite/game/agent/actions/node.py | 30 +- src/primaite/game/agent/actions/service.py | 20 +- src/primaite/game/agent/actions/session.py | 20 +- src/primaite/game/agent/actions/software.py | 46 +- src/primaite/game/agent/interface.py | 6 +- .../agent/observations/acl_observation.py | 2 +- .../observations/file_system_observations.py | 4 +- .../observations/firewall_observation.py | 4 +- .../agent/observations/host_observations.py | 4 +- .../agent/observations/link_observation.py | 4 +- .../agent/observations/nic_observations.py | 4 +- .../agent/observations/node_observations.py | 2 +- .../agent/observations/observation_manager.py | 12 +- .../agent/observations/router_observation.py | 4 +- .../observations/software_observation.py | 4 +- src/primaite/game/agent/rewards.py | 42 +- .../agent/scripted_agents/abstract_tap.py | 4 +- .../scripted_agents/data_manipulation_bot.py | 10 +- .../scripted_agents/probabilistic_agent.py | 4 +- .../agent/scripted_agents/random_agent.py | 12 +- ...ommand-and-Control-E2E-Demonstration.ipynb | 6 +- src/primaite/simulator/core.py | 2 +- src/primaite/simulator/file_system/file.py | 4 +- src/primaite/simulator/file_system/folder.py | 4 +- src/primaite/simulator/network/creation.py | 6 +- .../simulator/network/hardware/base.py | 24 +- .../network/hardware/nodes/host/computer.py | 2 +- .../network/hardware/nodes/host/host_node.py | 26 +- .../hardware/nodes/network/network_node.py | 4 +- .../hardware/nodes/network/wireless_router.py | 2 +- src/primaite/simulator/network/networks.py | 12 +- src/primaite/simulator/sim_container.py | 4 +- .../system/applications/database_client.py | 6 +- .../simulator/system/applications/nmap.py | 4 +- .../red_applications/c2/abstract_c2.py | 8 +- .../red_applications/c2/c2_beacon.py | 26 +- .../red_applications/c2/c2_server.py | 6 +- .../red_applications/data_manipulation_bot.py | 8 +- .../applications/red_applications/dos_bot.py | 6 +- .../red_applications/ransomware_script.py | 8 +- .../system/applications/web_browser.py | 8 +- .../simulator/system/core/software_manager.py | 6 +- .../simulator/system/services/arp/arp.py | 4 +- .../services/database/database_service.py | 14 +- .../system/services/dns/dns_client.py | 8 +- .../system/services/dns/dns_server.py | 8 +- .../system/services/ftp/ftp_client.py | 10 +- .../system/services/ftp/ftp_server.py | 9 +- .../simulator/system/services/icmp/icmp.py | 6 +- .../system/services/icmp/router_icmp.py | 16 +- .../system/services/ntp/ntp_client.py | 8 +- .../system/services/ntp/ntp_server.py | 8 +- .../system/services/terminal/terminal.py | 8 +- .../system/services/web_server/web_server.py | 10 +- tests/assets/configs/action_penalty.yaml | 220 ++++----- .../assets/configs/bad_primaite_session.yaml | 170 +++---- tests/assets/configs/basic_c2_setup.yaml | 4 +- tests/assets/configs/basic_firewall.yaml | 8 +- ...ic_node_with_software_listening_ports.yaml | 4 +- .../configs/basic_switched_network.yaml | 50 +- tests/assets/configs/data_manipulation.yaml | 260 +++++----- tests/assets/configs/dmz_network.yaml | 10 +- .../configs/eval_only_primaite_session.yaml | 182 +++---- tests/assets/configs/extended_config.yaml | 266 +++++----- .../configs/firewall_actions_network.yaml | 58 +-- .../configs/fixing_duration_one_item.yaml | 54 +-- .../configs/install_and_configure_apps.yaml | 30 +- tests/assets/configs/multi_agent_session.yaml | 456 +++++++++--------- ...etwork_service_recon_red_agent_config.yaml | 4 +- .../nmap_ping_scan_red_agent_config.yaml | 4 +- .../nmap_port_scan_red_agent_config.yaml | 4 +- .../scenario_with_placeholders/greens_1.yaml | 10 +- .../scenario_with_placeholders/greens_2.yaml | 10 +- .../scenario_with_placeholders/reds_1.yaml | 4 +- .../scenario_with_placeholders/reds_2.yaml | 4 +- .../scenario_with_placeholders/scenario.yaml | 34 +- tests/assets/configs/shared_rewards.yaml | 258 +++++----- .../configs/software_fixing_duration.yaml | 56 +-- .../configs/test_application_install.yaml | 278 +++++------ .../assets/configs/test_primaite_session.yaml | 180 +++---- .../configs/wireless_wan_network_config.yaml | 4 +- ..._wan_network_config_freq_max_override.yaml | 4 +- ...work_config_freq_max_override_blocked.yaml | 4 +- tests/conftest.py | 49 +- .../test_uc2_data_manipulation_scenario.py | 14 +- ...software_installation_and_configuration.py | 26 +- .../test_software_fixing_duration.py | 26 +- .../applications/extended_application.py | 8 +- .../extensions/nodes/super_computer.py | 2 +- .../extensions/services/extended_service.py | 12 +- .../extensions/test_extendable_config.py | 4 +- .../test_application_request_permission.py | 10 +- .../actions/test_c2_suite_actions.py | 36 +- .../actions/test_configure_actions.py | 38 +- .../actions/test_file_request_permission.py | 14 +- .../actions/test_folder_request_permission.py | 8 +- .../actions/test_nic_request_permission.py | 8 +- .../actions/test_node_request_permission.py | 14 +- .../test_service_request_permission.py | 24 +- .../actions/test_terminal_actions.py | 20 +- .../observations/test_acl_observations.py | 2 +- .../observations/test_nic_observations.py | 10 +- .../test_software_observations.py | 8 +- .../game_layer/test_RNG_seed.py | 8 +- .../game_layer/test_action_mask.py | 26 +- .../game_layer/test_actions.py | 90 ++-- .../game_layer/test_rewards.py | 34 +- ...ndwidth_load_checks_before_transmission.py | 4 +- .../network/test_broadcast.py | 20 +- .../network/test_capture_nmne.py | 6 +- .../network/test_firewall.py | 14 +- ...test_multi_lan_internet_example_network.py | 36 +- .../integration_tests/network/test_routing.py | 4 +- .../test_users_creation_from_config.py | 2 +- .../test_c2_suite_integration.py | 24 +- .../test_data_manipulation_bot_and_server.py | 20 +- .../test_dos_bot_and_server.py | 16 +- .../test_ransomware_script.py | 20 +- .../system/test_application_on_node.py | 4 +- .../system/test_database_on_node.py | 74 +-- .../system/test_dns_client_server.py | 4 +- .../system/test_ftp_client_server.py | 4 +- tests/integration_tests/system/test_nmap.py | 14 +- .../system/test_ntp_client_server.py | 12 +- .../system/test_service_listening_on_ports.py | 10 +- .../system/test_service_on_node.py | 4 +- .../system/test_web_client_server.py | 8 +- .../test_web_client_server_and_database.py | 30 +- .../test_simulation/test_request_response.py | 60 ++- .../_primaite/_game/_agent/test_actions.py | 2 +- .../_primaite/_game/_agent/test_agent.py | 8 +- .../_game/_agent/test_observations.py | 10 +- .../_game/_agent/test_probabilistic_agent.py | 18 +- .../_game/_agent/test_sticky_rewards.py | 128 ++--- .../_simulator/_file_system/test_file.py | 2 +- .../_file_system/test_file_actions.py | 2 +- .../_simulator/_file_system/test_folder.py | 2 +- .../_file_system/test_folder_actions.py | 2 +- .../_network/_hardware/test_node_actions.py | 8 +- .../_simulator/_network/test_creation.py | 2 +- .../_red_applications/test_c2_suite.py | 4 +- .../test_data_manipulation_bot.py | 10 +- .../_red_applications/test_dos_bot.py | 2 +- .../test_application_registry.py | 8 +- .../_applications/test_database_client.py | 4 +- .../_system/_applications/test_web_browser.py | 6 +- .../_system/_services/test_database.py | 2 +- .../_system/_services/test_dns_client.py | 14 +- .../_system/_services/test_dns_server.py | 10 +- .../_system/_services/test_ftp_client.py | 16 +- .../_system/_services/test_ftp_server.py | 12 +- .../_system/_services/test_terminal.py | 78 +-- .../_system/_services/test_web_server.py | 10 +- .../_simulator/_system/test_software.py | 4 +- 195 files changed, 2824 insertions(+), 2802 deletions(-) diff --git a/docs/source/action_masking.rst b/docs/source/action_masking.rst index 4331b090..bee4674b 100644 --- a/docs/source/action_masking.rst +++ b/docs/source/action_masking.rst @@ -23,117 +23,117 @@ The following logic is applied: +------------------------------------------+---------------------------------------------------------------------+ | Action | Action Mask Logic | +==========================================+=====================================================================+ -| **do_nothing** | Always Possible. | +| **do-nothing** | Always Possible. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_scan** | Node is on. Service is running. | +| **node-service-scan** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_stop** | Node is on. Service is running. | +| **node-service-stop** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_start** | Node is on. Service is stopped. | +| **node-service-start** | Node is on. Service is stopped. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_pause** | Node is on. Service is running. | +| **node-service-pause** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_resume** | Node is on. Service is paused. | +| **node-service-resume** | Node is on. Service is paused. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_restart** | Node is on. Service is running. | +| **node-service-restart** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_disable** | Node is on. | +| **node-service-disable** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_enable** | Node is on. Service is disabled. | +| **node-service-enable** | Node is on. Service is disabled. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_service_fix** | Node is on. Service is running. | +| **node-service-fix** | Node is on. Service is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_application_execute** | Node is on. | +| **node-application-execute** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_application_scan** | Node is on. Application is running. | +| **node-application-scan** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_application_close** | Node is on. Application is running. | +| **node-application-close** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_application_fix** | Node is on. Application is running. | +| **node-application-fix** | Node is on. Application is running. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_application_install** | Node is on. | +| **node-application-install** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_application_remove** | Node is on. | +| **node-application-remove** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_scan** | Node is on. File exists. File not deleted. | +| **node-file-scan** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_create** | Node is on. | +| **node-file-create** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_checkhash** | Node is on. File exists. File not deleted. | +| **node-file-checkhash** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_delete** | Node is on. File exists. | +| **node-file-delete** | Node is on. File exists. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_repair** | Node is on. File exists. File not deleted. | +| **node-file-repair** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_restore** | Node is on. File exists. File is deleted. | +| **node-file-restore** | Node is on. File exists. File is deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_corrupt** | Node is on. File exists. File not deleted. | +| **node-file-corrupt** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_file_access** | Node is on. File exists. File not deleted. | +| **node-file-access** | Node is on. File exists. File not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_folder_create** | Node is on. | +| **node-folder-create** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_folder_scan** | Node is on. Folder exists. Folder not deleted. | +| **node-folder-scan** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_folder_checkhash** | Node is on. Folder exists. Folder not deleted. | +| **node-folder-checkhash** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_folder_repair** | Node is on. Folder exists. Folder not deleted. | +| **node-folder-repair** | Node is on. Folder exists. Folder not deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_folder_restore** | Node is on. Folder exists. Folder is deleted. | +| **node-folder-restore** | Node is on. Folder exists. Folder is deleted. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_os_scan** | Node is on. | +| **node-os-scan** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **host_nic_enable** | NIC is disabled. Node is on. | +| **host-nic-enable** | NIC is disabled. Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **host_nic_disable** | NIC is enabled. Node is on. | +| **host-nic-disable** | NIC is enabled. Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_shutdown** | Node is on. | +| **node-shutdown** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_startup** | Node is off. | +| **node-startup** | Node is off. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_reset** | Node is on. | +| **node-reset** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_nmap_ping_scan** | Node is on. | +| **node-nmap-ping-scan** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_nmap_port_scan** | Node is on. | +| **node-nmap-port-scan** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_network_service_recon** | Node is on. | +| **node-network-service-recon** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **network_port_enable** | Node is on. Router is on. | +| **network-port-enable** | Node is on. Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **network_port_disable** | Router is on. | +| **network-port-disable** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **router_acl_addrule** | Router is on. | +| **router-acl-add-rule** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **router_acl_removerule** | Router is on. | +| **router-acl-remove-rule** | Router is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **firewall_acl_addrule** | Firewall is on. | +| **firewall-acl-add-rule** | Firewall is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **firewall_acl_removerule** | Firewall is on. | +| **firewall-acl-remove-rule** | Firewall is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **configure_database_client** | Node is on. | +| **configure-database-client** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **configure_ransomware_script** | Node is on. | +| **configure-ransomware-script** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **c2_server_ransomware_configure** | Node is on. | +| **c2-server-ransomware-configure** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **configure_dos_bot** | Node is on. | +| **configure-dos-bot** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **configure_c2_beacon** | Node is on. | +| **configure-c2-beacon** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **c2_server_ransomware_launch** | Node is on. | +| **c2-server-ransomware-launch** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **c2_server_terminal_command** | Node is on. | +| **c2-server-terminal-command** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **c2_server_data_exfiltrate** | Node is on. | +| **c2-server-data-exfiltrate** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_account_change_password** | Node is on. | +| **node-account-change-password** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_session_remote_login** | Node is on. | +| **node-session-remote-login** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_session_remote_logoff** | Node is on. | +| **node-session-remote-logoff** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ -| **node_send_remote_command** | Node is on. | +| **node-send-remote-command** | Node is on. | +------------------------------------------+---------------------------------------------------------------------+ diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index 0194bd72..dce8da3a 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -19,13 +19,13 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo ... - ref: green_agent_example team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent observation_space: type: UC2GreenObservation action_space: reward_function: reward_components: - - type: DUMMY + - type: dummy agent_settings: start_settings: @@ -44,13 +44,13 @@ Specifies if the agent is malicious (``RED``), benign (``GREEN``), or defensive ``type`` -------- -Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``ProbabilisticAgent`` generate their own behaviour. +Specifies which class should be used for the agent. ``proxy-agent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``red-database-corrupting-agent`` and ``probabilistic-agent`` generate their own behaviour. Available agent types: -- ``ProbabilisticAgent`` -- ``ProxyAgent`` -- ``RedDatabaseCorruptingAgent`` +- ``probabilistic-agent`` +- ``proxy-agent`` +- ``red-database-corrupting-agent`` ``observation_space`` --------------------- @@ -66,10 +66,10 @@ selects which python class from the :py:mod:`primaite.game.agent.observation` mo Allows configuration of the chosen observation type. These are optional. - * ``num_services_per_node``, ``num_folders_per_node``, ``num_files_per_folder``, ``num_nics_per_node`` all define the shape of the observation space. The size and shape of the obs space must remain constant, but the number of files, folders, ACL rules, and other components can change within an episode. Therefore padding is performed and these options set the size of the obs space. + * ``num_services_per_node``, ``num_folders_per_node``, ``num_files_per_folder``, ``num_nics_per_node`` all define the shape of the observation space. The size and shape of the obs space must remain constant, but the number of files, folders, acl rules, and other components can change within an episode. Therefore padding is performed and these options set the size of the obs space. * ``nodes``: list of nodes that will be present in this agent's observation space. The ``node_ref`` relates to the human-readable unique reference defined later in the ``simulation`` part of the config. Each node can also be configured with services, and files that should be monitored. * ``links``: list of links that will be present in this agent's observation space. The ``link_ref`` relates to the human-readable unique reference defined later in the ``simulation`` part of the config. - * ``acl``: configure how the agent reads the access control list on the router in the simulation. ``router_node_ref`` is for selecting which router's ACL table should be used. ``ip_list`` sets the encoding of ip addresses as integers within the observation space. + * ``acl``: configure how the agent reads the access control list on the router in the simulation. ``router_node_ref`` is for selecting which router's acl table should be used. ``ip_list`` sets the encoding of ip addresses as integers within the observation space. For more information see :py:mod:`primaite.game.agent.observations` @@ -111,8 +111,8 @@ e.g. .. code-block:: yaml reward_components: - - type: DUMMY - - type: DATABASE_FILE_INTEGRITY + - type: dummy + - type: database-file-integrity ``agent_settings`` diff --git a/docs/source/configuration/simulation/nodes/network_examples.rst b/docs/source/configuration/simulation/nodes/network_examples.rst index 80e934e5..4616139e 100644 --- a/docs/source/configuration/simulation/nodes/network_examples.rst +++ b/docs/source/configuration/simulation/nodes/network_examples.rst @@ -617,10 +617,10 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 192.168.1.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai @@ -631,10 +631,10 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 192.168.1.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai @@ -700,7 +700,7 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 8.8.8.1 services: - ref: dns_server - type: DNSServer + type: dns-server options: domain_mapping: sometech.ai: 94.10.180.6 @@ -794,9 +794,9 @@ Each node is configured to ensure it meets the specific security and operational dns_server: 8.8.8.2 services: - ref: web_server - type: WebServer + type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 @@ -903,10 +903,10 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 10.10.1.1 dns_server: 8.8.8.2 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 10.10.1.12 # The some_tech_storage_srv server - - type: FTPClient + - type: ftp-client - hostname: some_tech_storage_srv type: server @@ -915,7 +915,7 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 10.10.1.1 dns_server: 8.8.8.2 services: - - type: FTPServer + - type: ftp-server - hostname: some_tech_hr_1 type: computer @@ -924,10 +924,10 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 10.10.3.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai @@ -938,10 +938,10 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 10.10.2.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai @@ -952,10 +952,10 @@ Each node is configured to ensure it meets the specific security and operational default_gateway: 10.10.2.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 58a274d9..36ec016d 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -57,13 +57,13 @@ An agent's reward can be based on rewards of other agents. This is particularly reward_components: # When the webpage loads, the reward goes up by 0.25 when it fails to load, it goes down to -0.25 - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 # When the database is reachable, the reward goes up by 0.05, when it is unreachable it goes down to -0.05 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 @@ -74,7 +74,7 @@ An agent's reward can be based on rewards of other agents. This is particularly reward_components: # When the database file is in a good state, blue's reward is 0.4, when it's in a corrupted state the reward is -0.4 - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server @@ -82,7 +82,7 @@ An agent's reward can be based on rewards of other agents. This is particularly file_name: database.db # The green's reward is added onto the blue's reward. - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user diff --git a/docs/source/how_to_guides/extensible_actions.rst b/docs/source/how_to_guides/extensible_actions.rst index 4deede53..c2cc07bf 100644 --- a/docs/source/how_to_guides/extensible_actions.rst +++ b/docs/source/how_to_guides/extensible_actions.rst @@ -38,7 +38,7 @@ When declaring a custom class, it must have a unique discriminator string, that .. code:: Python - class CreateDirectoryAction(AbstractAction, discriminator="node_folder_create") + class CreateDirectoryAction(AbstractAction, discriminator="node-folder-create") config: CreateDirectoryAction.ConfigSchema @@ -58,7 +58,7 @@ When declaring a custom class, it must have a unique discriminator string, that config.directory_name, ] -The above action would fail pydantic validation as the discriminator "node_folder_create" is already used by the `NodeFolderCreateAction`, and would create a duplicate listing within `AbstractAction._registry`. +The above action would fail pydantic validation as the discriminator "node-folder-create" is already used by the `NodeFolderCreateAction`, and would create a duplicate listing within `AbstractAction._registry`. form_request method diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index d83f83d6..1d765417 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -51,11 +51,11 @@ The core features that should be implemented in any new agent are detailed below action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} reward_function: reward_components: - - type: DUMMY + - type: dummy agent_settings: start_step: 25 diff --git a/docs/source/how_to_guides/extensible_rewards.rst b/docs/source/how_to_guides/extensible_rewards.rst index 9068f1bb..d3053a49 100644 --- a/docs/source/how_to_guides/extensible_rewards.rst +++ b/docs/source/how_to_guides/extensible_rewards.rst @@ -28,7 +28,7 @@ To add a new reward class follow the example below. Note that the type attribute .. code-block:: Python -class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRITY"): +class DatabaseFileIntegrity(AbstractReward, discriminator="database-file-integrity"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" config: "DatabaseFileIntegrity.ConfigSchema" @@ -38,7 +38,7 @@ class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRI class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for DatabaseFileIntegrity.""" - type: str = "DATABASE_FILE_INTEGRITY" + type: str = "database-file-integrity" node_hostname: str folder_name: str file_name: str diff --git a/docs/source/node_sets.rst b/docs/source/node_sets.rst index 1ac6a54c..75047d54 100644 --- a/docs/source/node_sets.rst +++ b/docs/source/node_sets.rst @@ -55,7 +55,7 @@ Via YAML Config nodes: # ... nodes go here node_sets: - - type: office_lan + - type: office-lan lan_name: CORP_LAN subnet_base: 2 pcs_ip_block_start: 10 @@ -82,9 +82,9 @@ Here is an example of creating a custom node adder, DataCenterAdder: .. code-block:: python - class DataCenterAdder(NetworkNodeAdder, discriminator="data_center"): + class DataCenterAdder(NetworkNodeAdder, discriminator="data-center"): class ConfigSchema(NetworkNodeAdder.ConfigSchema): - type: Literal["data_center"] = "data_center" + type: Literal["data-center"] = "data-center" num_servers: int data_center_name: str @@ -106,7 +106,7 @@ Here is an example of creating a custom node adder, DataCenterAdder: .. code-block:: python config = { - "type": "data_center", + "type": "data-center", "num_servers": 5, "data_center_name": "dc1" } diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index b89d0906..f0437705 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -51,10 +51,10 @@ Request responses When the simulator receives a request, it returns a response with a success status. The possible statuses are: * **success**: The request was received and successfully executed. - * For example, the agent tries to add an ACL rule and specifies correct parameters, and the ACL rule is added successfully. + * For example, the agent tries to add an acl rule and specifies correct parameters, and the acl rule is added successfully. * **failure**: The request was received, but it could not be executed, or it failed while executing. - * For example, the agent tries to execute the ``WebBrowser`` application, but the webpage wasn't retrieved because the DNS server is not setup on the node. + * For example, the agent tries to execute the ``web-browser`` application, but the webpage wasn't retrieved because the DNS server is not setup on the node. * **unreachable**: The request was sent to a simulation component that does not exist. * For example, the agent tries to scan a file that has not been created yet. diff --git a/docs/source/rewards.rst b/docs/source/rewards.rst index 254237ee..1f588f36 100644 --- a/docs/source/rewards.rst +++ b/docs/source/rewards.rst @@ -23,7 +23,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: DUMMY + - type: dummy weight: 1.0 @@ -36,7 +36,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 1.0 options: node_hostname: server_1 @@ -53,7 +53,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty node_hostname: web_server weight: 1.0 options: @@ -70,7 +70,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty node_hostname: computer_1 weight: 1.0 options: @@ -86,7 +86,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 1.0 options: node_hostname: admin_pc_1 @@ -104,7 +104,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: scripted_agent @@ -119,7 +119,7 @@ The following API pages describe the use of each reward component and the possib # ... reward_function: reward_components: - - type: ACTION_PENALTY + - type: action-penalty weight: 1.0 options: action_penalty: -0.3 diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 3dd2b4fc..34175fc3 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -229,7 +229,7 @@ Via Configuration type: computer ... applications: - type: C2Server + type: c2-server ... hostname: computer_b type: computer @@ -238,7 +238,7 @@ Via Configuration # Either an agent must use application_execute. # Or a if using the simulation layer - .establish(). applications: - type: C2Beacon + type: c2-beacon options: c2_server_ip_address: ... keep_alive_frequency: 5 diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 49dc3baf..8e008504 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -95,7 +95,7 @@ If not using the data manipulation bot manually, it needs to be used with a data agents: - ref: data_manipulation_red_bot team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent observation_space: type: UC2RedObservation @@ -115,7 +115,7 @@ If not using the data manipulation bot manually, it needs to be used with a data action_space: reward_function: reward_components: - - type: DUMMY + - type: dummy agent_settings: start_settings: @@ -132,14 +132,14 @@ If not using the data manipulation bot manually, it needs to be used with a data # ... additional configuration here applications: - ref: data_manipulation_bot - type: DataManipulationBot + type: data-manipulation-bot options: port_scan_p_of_success: 0.1 data_manipulation_p_of_success: 0.1 payload: "DELETE" server_ip: 192.168.1.14 - ref: web_server_database_client - type: DatabaseClient + type: database-client options: db_server_ip: 192.168.1.14 diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 75a396b5..7087dedf 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -83,7 +83,7 @@ Via Configuration ... applications: - ref: database_client - type: DatabaseClient + type: database-client options: db_server_ip: 192.168.0.1 diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index 5c0ae86a..47b72be7 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -4,10 +4,10 @@ .. _DoSBot: -DoSBot +dos-bot ###### -The ``DoSBot`` is an implementation of a Denial of Service attack within the PrimAITE simulation. +The ``dos-bot`` is an implementation of a Denial of Service attack within the PrimAITE simulation. This specifically simulates a `Slow Loris attack`_. .. _Slow Loris Attack: https://en.wikipedia.org/wiki/Slowloris_(computer_security) @@ -15,20 +15,20 @@ This specifically simulates a `Slow Loris attack`_. Key features ============ -- Connects to the :ref:`DatabaseService` via the ``SoftwareManager``. -- Makes many connections to the :ref:`DatabaseService` which ends up using up the available connections. +- Connects to the :ref:`database-service` via the ``SoftwareManager``. +- Makes many connections to the :ref:`database-service` which ends up using up the available connections. Usage ===== - Configure with target IP address and optional password. -- use ``run`` to run the application_loop of DoSBot to begin attacks -- DoSBot runs through different actions at each timestep +- use ``run`` to run the application_loop of dos-bot to begin attacks +- dos-bot runs through different actions at each timestep Implementation ============== -- Leverages :ref:`DatabaseClient` to create connections with :ref`DatabaseServer`. +- Leverages :ref:`database-client` to create connections with :ref`DatabaseServer`. - Extends base Application class. Examples @@ -42,7 +42,7 @@ Python from ipaddress import IPv4Address from primaite.simulator.network.hardware.nodes.host.computer import Computer - from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot + from primaite.simulator.system.applications.red_applications.dos_bot import dos-bot # Create Computer computer = Computer( @@ -54,11 +54,11 @@ Python ) computer.power_on() - # Install DoSBot on computer - computer.software_manager.install(DoSBot) - dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") + # Install dos-bot on computer + computer.software_manager.install(dos-bot) + dos_bot: dos-bot = computer.software_manager.software.get("dos-bot") - # Configure the DoSBot + # Configure the dos-bot dos_bot.configure( target_ip_address=IPv4Address("192.168.0.10"), payload="SPOOF DATA", @@ -68,7 +68,7 @@ Python max_sessions=1000 ) - # run DoSBot + # run dos-bot dos_bot.run() @@ -86,7 +86,7 @@ Via Configuration ... applications: - ref: dos_bot - type: DoSBot + type: dos-bot options: target_ip_address: 192.168.0.10 payload: SPOOF DATA @@ -101,7 +101,7 @@ Configuration ``target_ip_address`` """"""""""""""""""""" -IP address of the :ref:`DatabaseService` which the ``DataManipulationBot`` will try to attack. +IP address of the :ref:`database-service` which the ``data-manipulation-bot`` will try to attack. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. @@ -119,7 +119,7 @@ See :ref:`List of IPProtocols ` for a list of protocols. Optional. Default value is ``None``. -The payload that the ``DoSBot`` sends as part of its attack. +The payload that the ``dos-bot`` sends as part of its attack. .. include:: ../common/db_payload_list.rst @@ -128,14 +128,14 @@ The payload that the ``DoSBot`` sends as part of its attack. Optional. Default value is ``False``. -If ``True`` the ``DoSBot`` will maintain its attack. +If ``True`` the ``dos-bot`` will maintain its attack. ``port_scan_p_of_success`` """""""""""""""""""""""""" Optional. Default value is ``0.1``. -The chance of the ``DoSBot`` to succeed with a port scan (and therefore continue the attack). +The chance of the ``dos-bot`` to succeed with a port scan (and therefore continue the attack). This must be a float value between ``0`` and ``1``. @@ -153,7 +153,7 @@ This must be a float value between ``0`` and ``1``. Optional. Default value is ``1000``. -The maximum number of sessions the ``DoSBot`` is able to make. +The maximum number of sessions the ``dos-bot`` is able to make. This must be an integer value equal to or greater than ``0``. diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index 7062887b..c04e60af 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -85,7 +85,7 @@ Via Configuration ... applications: - ref: web_browser - type: WebBrowser + type: web-browser options: target_url: http://arcd.com/ diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index b41c1097..961f2e45 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -87,7 +87,7 @@ Via Configuration ... services: - ref: database_service - type: DatabaseService + type: database-service options: backup_server_ip: 192.168.0.10 diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index 6475b4d4..17a1ed25 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -77,7 +77,7 @@ Via Configuration ... services: - ref: dns_client - type: DNSClient + type: dns-client options: dns_server: 192.168.0.10 diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index 3d699048..633221d5 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -74,7 +74,7 @@ Via Configuration ... services: - ref: dns_server - type: DNSServer + type: dns-server options: domain_mapping: arcd.com: 192.168.0.10 diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index 47566e5f..d4375069 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -78,7 +78,7 @@ Via Configuration ... services: - ref: ftp_client - type: FTPClient + type: ftp-client Configuration ============= diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index e4cada29..a5ad32fe 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -74,7 +74,7 @@ Via Configuration ... services: - ref: ftp_server - type: FTPServer + type: ftp-server options: server_password: test diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index fb965029..8c011cad 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -73,7 +73,7 @@ Via Configuration ... services: - ref: ntp_client - type: NTPClient + type: ntp-client options: ntp_server_ip: 192.168.0.10 diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index 68fadca9..c1d16d61 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -73,7 +73,7 @@ Via Configuration ... services: - ref: ntp_server - type: NTPServer + type: ntp-server ``Common Attributes`` diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index 011aa00f..bce42791 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -73,7 +73,7 @@ Via Configuration ... services: - ref: web_server - type: WebServer + type: web-server ``Common Attributes`` diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index b0d5d087..4705c135 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -24,7 +24,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -34,33 +34,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -70,26 +70,26 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 @@ -100,31 +100,31 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -169,7 +169,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -183,222 +183,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -410,8 +410,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -424,7 +424,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -437,7 +437,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -450,7 +450,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -463,7 +463,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -476,132 +476,132 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 @@ -611,19 +611,19 @@ agents: reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -693,7 +693,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -705,9 +705,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -719,10 +719,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -731,7 +731,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -751,20 +751,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -773,20 +773,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: router_1 diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index e45f193e..0263edb0 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -20,7 +20,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -30,33 +30,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -66,26 +66,26 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 @@ -96,32 +96,32 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender_1 team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -161,7 +161,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -175,222 +175,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -402,8 +402,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -416,7 +416,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -429,7 +429,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -442,7 +442,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -455,7 +455,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -468,132 +468,132 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 @@ -601,17 +601,17 @@ agents: reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -623,20 +623,20 @@ agents: - ref: defender_2 team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -676,7 +676,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -690,222 +690,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -917,8 +917,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -931,7 +931,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -944,7 +944,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -957,7 +957,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -970,7 +970,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -983,132 +983,132 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 @@ -1117,17 +1117,17 @@ agents: reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -1197,7 +1197,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -1209,9 +1209,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -1223,10 +1223,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -1235,7 +1235,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -1255,20 +1255,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -1277,20 +1277,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml index e18de191..4c7734d8 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml @@ -5,46 +5,46 @@ game: agents: - ref: RL_Agent - type: ProxyAgent + type: proxy-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_shutdown + action: node-shutdown options: node_name: client_1 2: - action: node_shutdown + action: node-shutdown options: node_name: server 3: - action: node_startup + action: node-startup options: node_name: client_1 4: - action: node_startup + action: node-startup options: node_name: server 5: - action: host_nic_disable + action: host-nic-disable options: node_name: client_1 nic_num: 1 6: - action: host_nic_disable + action: host-nic-disable options: node_name: server nic_num: 1 7: - action: host_nic_enable + action: host-nic-enable options: node_name: client_1 nic_num: 1 8: - action: host_nic_enable + action: host-nic-enable options: node_name: server nic_num: 1 diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_1.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_1.yaml index 3e27cc27..5a976294 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_1.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_1.yaml @@ -1,5 +1,5 @@ server_services: &server_services - - type: DatabaseService + - type: database-service client_applications: &client_applications - - type: DatabaseClient + - type: database-client diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_2.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_2.yaml index 207e0c73..8b89e9f6 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_2.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/simulation_variant_2.yaml @@ -1,5 +1,5 @@ server_services: &server_services - - type: FTPServer + - type: ftp-server client_applications: &client_applications - - type: RansomwareScript + - type: ransomware-script diff --git a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml index 61562418..508018aa 100644 --- a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml +++ b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml @@ -20,10 +20,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai/users/ @@ -34,10 +34,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai/users/ @@ -103,7 +103,7 @@ simulation: default_gateway: 8.8.8.1 services: - ref: dns_server - type: DNSServer + type: dns-server options: domain_mapping: sometech.ai: 94.10.180.6 @@ -150,7 +150,7 @@ simulation: dst_ip: 94.10.180.6 dst_port: POSTGRES_SERVER dst_wildcard_mask: 0.0.0.0 - 8: # Permit SomeTech DMZ to use ARP + 8: # Permit SomeTech DMZ to use arp action: PERMIT src_port: ARP dst_port: ARP @@ -170,7 +170,7 @@ simulation: dst_ip: 10.10.1.11 dst_port: POSTGRES_SERVER dst_wildcard_mask: 0.0.0.0 - 8: # Permit SomeTech DMZ to use ARP + 8: # Permit SomeTech DMZ to use arp action: PERMIT src_port: ARP dst_port: ARP @@ -197,9 +197,9 @@ simulation: dns_server: 8.8.8.2 services: - ref: web_server - type: WebServer + type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 @@ -269,12 +269,12 @@ simulation: action: PERMIT src_port: HTTP dst_port: HTTP - 18: # Allow the SomeTech internal network to use ARP + 18: # Allow the SomeTech internal network to use arp action: PERMIT src_ip: 10.10.0.0 src_wildcard_mask: 0.0.255.255 src_port: ARP - 19: # Allow the SomeTech internal network to use ICMP + 19: # Allow the SomeTech internal network to use icmp action: PERMIT src_ip: 10.10.0.0 src_wildcard_mask: 0.0.255.255 @@ -318,10 +318,10 @@ simulation: default_gateway: 10.10.1.1 dns_server: 8.8.8.2 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 10.10.1.12 # The some_tech_storage_srv server - - type: FTPClient + - type: ftp-client - hostname: some_tech_storage_srv type: server @@ -330,7 +330,7 @@ simulation: default_gateway: 10.10.1.1 dns_server: 8.8.8.2 services: - - type: FTPServer + - type: ftp-server - hostname: some_tech_hr_1 type: computer @@ -339,10 +339,10 @@ simulation: default_gateway: 10.10.3.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai/users/ @@ -353,10 +353,10 @@ simulation: default_gateway: 10.10.2.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai/users/ @@ -367,10 +367,10 @@ simulation: default_gateway: 10.10.2.1 dns_server: 8.8.8.2 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 10.10.1.11 - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai/users/ diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml index 677cd5a5..3f9b65f4 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/greens_1.yaml @@ -1,7 +1,7 @@ agents: &greens - ref: green_A team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.2 @@ -10,17 +10,17 @@ agents: &greens action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 1.0 options: node_hostname: client diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml index eb7823f8..77a689e7 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/greens_2.yaml @@ -1,7 +1,7 @@ agents: &greens - ref: green_B team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.95 @@ -10,17 +10,17 @@ agents: &greens action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 1.0 options: node_hostname: client diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml index 0170143f..b95955b4 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_1.yaml @@ -1,11 +1,11 @@ reds: &reds - ref: red_A team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: possible_start_nodes: [client,] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 10 frequency: 10 variance: 0 diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml index 55bee3fb..653051c6 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/reds_2.yaml @@ -1,11 +1,11 @@ reds: &reds - ref: red_B team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: possible_start_nodes: [client_1] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 3 frequency: 2 variance: 1 diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index c692c725..1a96e3a0 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -26,12 +26,12 @@ agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: routers: [] @@ -46,7 +46,7 @@ agents: include_num_access: false include_nmne: true - - type: LINKS + - type: links label: LINKS options: link_references: @@ -56,48 +56,48 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_shutdown + action: node-shutdown options: node_name: client_1 2: - action: node_shutdown + action: node-shutdown options: node_name: server 3: - action: node_startup + action: node-startup options: node_name: client_1 4: - action: node_startup + action: node-startup options: node_name: server 5: - action: host_nic_disable + action: host-nic-disable options: node_name: client_1 nic_num: 1 6: - action: host_nic_disable + action: host-nic-disable options: node_name: server nic_num: 1 7: - action: host_nic_enable + action: host-nic-enable options: node_name: client_1 nic_num: 1 8: - action: host_nic_enable + action: host-nic-enable options: node_name: server nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server @@ -121,10 +121,10 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.3 - - type: DataManipulationBot + - type: data-manipulation-bot options: server_ip: 192.168.1.3 payload: "DELETE" @@ -139,7 +139,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DatabaseService + - type: database-service links: - endpoint_a_hostname: client diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 4ef4b506..7a5cc188 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -37,8 +37,8 @@ class ACLAddRuleAbstractAction(AbstractAction, ABC): dst_wildcard: Union[IPV4Address, Literal["NONE"]] -class ACLRemoveRuleAbstractAction(AbstractAction, discriminator="acl_remove_rule_abstract_action"): - """Base abstract class for ACL remove rule actions.""" +class ACLRemoveRuleAbstractAction(AbstractAction, discriminator="acl-remove-rule-abstract-action"): + """Base abstract class for acl remove rule actions.""" config: ConfigSchema = "ACLRemoveRuleAbstractAction.ConfigSchema" @@ -48,8 +48,8 @@ class ACLRemoveRuleAbstractAction(AbstractAction, discriminator="acl_remove_rule position: int -class RouterACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="router_acl_add_rule"): - """Action which adds a rule to a router's ACL.""" +class RouterACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="router-acl-add-rule"): + """Action which adds a rule to a router's acl.""" config: "RouterACLAddRuleAction.ConfigSchema" @@ -79,8 +79,8 @@ class RouterACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="router_acl ] -class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="router_acl_remove_rule"): - """Action which removes a rule from a router's ACL.""" +class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="router-acl-remove-rule"): + """Action which removes a rule from a router's acl.""" config: "RouterACLRemoveRuleAction.ConfigSchema" @@ -95,8 +95,8 @@ class RouterACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="rout return ["network", "node", config.target_router, "acl", "remove_rule", config.position] -class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="firewall_acl_add_rule"): - """Action which adds a rule to a firewall port's ACL.""" +class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="firewall-acl-add-rule"): + """Action which adds a rule to a firewall port's acl.""" config: "FirewallACLAddRuleAction.ConfigSchema" @@ -130,8 +130,8 @@ class FirewallACLAddRuleAction(ACLAddRuleAbstractAction, discriminator="firewall ] -class FirewallACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="firewall_acl_remove_rule"): - """Action which removes a rule from a firewall port's ACL.""" +class FirewallACLRemoveRuleAction(ACLRemoveRuleAbstractAction, discriminator="firewall-acl-remove-rule"): + """Action which removes a rule from a firewall port's acl.""" config: "FirewallACLRemoveRuleAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index 36d2e0b4..a7452de0 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -45,7 +45,7 @@ class NodeApplicationAbstractAction(AbstractAction, ABC): ] -class NodeApplicationExecuteAction(NodeApplicationAbstractAction, discriminator="node_application_execute"): +class NodeApplicationExecuteAction(NodeApplicationAbstractAction, discriminator="node-application-execute"): """Action which executes an application.""" config: "NodeApplicationExecuteAction.ConfigSchema" @@ -56,7 +56,7 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction, discriminator= verb: str = "execute" -class NodeApplicationScanAction(NodeApplicationAbstractAction, discriminator="node_application_scan"): +class NodeApplicationScanAction(NodeApplicationAbstractAction, discriminator="node-application-scan"): """Action which scans an application.""" config: "NodeApplicationScanAction.ConfigSchema" @@ -67,7 +67,7 @@ class NodeApplicationScanAction(NodeApplicationAbstractAction, discriminator="no verb: str = "scan" -class NodeApplicationCloseAction(NodeApplicationAbstractAction, discriminator="node_application_close"): +class NodeApplicationCloseAction(NodeApplicationAbstractAction, discriminator="node-application-close"): """Action which closes an application.""" config: "NodeApplicationCloseAction.ConfigSchema" @@ -78,7 +78,7 @@ class NodeApplicationCloseAction(NodeApplicationAbstractAction, discriminator="n verb: str = "close" -class NodeApplicationFixAction(NodeApplicationAbstractAction, discriminator="node_application_fix"): +class NodeApplicationFixAction(NodeApplicationAbstractAction, discriminator="node-application-fix"): """Action which fixes an application.""" config: "NodeApplicationFixAction.ConfigSchema" @@ -89,7 +89,7 @@ class NodeApplicationFixAction(NodeApplicationAbstractAction, discriminator="nod verb: str = "fix" -class NodeApplicationInstallAction(NodeApplicationAbstractAction, discriminator="node_application_install"): +class NodeApplicationInstallAction(NodeApplicationAbstractAction, discriminator="node-application-install"): """Action which installs an application.""" config: "NodeApplicationInstallAction.ConfigSchema" @@ -113,7 +113,7 @@ class NodeApplicationInstallAction(NodeApplicationAbstractAction, discriminator= ] -class NodeApplicationRemoveAction(NodeApplicationAbstractAction, discriminator="node_application_remove"): +class NodeApplicationRemoveAction(NodeApplicationAbstractAction, discriminator="node-application-remove"): """Action which removes/uninstalls an application.""" config: "NodeApplicationRemoveAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index 2aa3b85c..b9ebe3bd 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -38,7 +38,7 @@ class NodeFileAbstractAction(AbstractAction, ABC): def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None or config.file_name is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -52,7 +52,7 @@ class NodeFileAbstractAction(AbstractAction, ABC): ] -class NodeFileCreateAction(NodeFileAbstractAction, discriminator="node_file_create"): +class NodeFileCreateAction(NodeFileAbstractAction, discriminator="node-file-create"): """Action which creates a new file in a given folder.""" config: "NodeFileCreateAction.ConfigSchema" @@ -67,7 +67,7 @@ class NodeFileCreateAction(NodeFileAbstractAction, discriminator="node_file_crea def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None or config.file_name is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -81,7 +81,7 @@ class NodeFileCreateAction(NodeFileAbstractAction, discriminator="node_file_crea ] -class NodeFileScanAction(NodeFileAbstractAction, discriminator="node_file_scan"): +class NodeFileScanAction(NodeFileAbstractAction, discriminator="node-file-scan"): """Action which scans a file.""" config: "NodeFileScanAction.ConfigSchema" @@ -92,7 +92,7 @@ class NodeFileScanAction(NodeFileAbstractAction, discriminator="node_file_scan") verb: ClassVar[str] = "scan" -class NodeFileDeleteAction(NodeFileAbstractAction, discriminator="node_file_delete"): +class NodeFileDeleteAction(NodeFileAbstractAction, discriminator="node-file-delete"): """Action which deletes a file.""" config: "NodeFileDeleteAction.ConfigSchema" @@ -106,7 +106,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction, discriminator="node_file_dele def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None or config.file_name is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -119,7 +119,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction, discriminator="node_file_dele ] -class NodeFileRestoreAction(NodeFileAbstractAction, discriminator="node_file_restore"): +class NodeFileRestoreAction(NodeFileAbstractAction, discriminator="node-file-restore"): """Action which restores a file.""" config: "NodeFileRestoreAction.ConfigSchema" @@ -130,7 +130,7 @@ class NodeFileRestoreAction(NodeFileAbstractAction, discriminator="node_file_res verb: ClassVar[str] = "restore" -class NodeFileCorruptAction(NodeFileAbstractAction, discriminator="node_file_corrupt"): +class NodeFileCorruptAction(NodeFileAbstractAction, discriminator="node-file-corrupt"): """Action which corrupts a file.""" config: "NodeFileCorruptAction.ConfigSchema" @@ -141,7 +141,7 @@ class NodeFileCorruptAction(NodeFileAbstractAction, discriminator="node_file_cor verb: ClassVar[str] = "corrupt" -class NodeFileAccessAction(NodeFileAbstractAction, discriminator="node_file_access"): +class NodeFileAccessAction(NodeFileAbstractAction, discriminator="node-file-access"): """Action which increases a file's access count.""" config: "NodeFileAccessAction.ConfigSchema" @@ -155,7 +155,7 @@ class NodeFileAccessAction(NodeFileAbstractAction, discriminator="node_file_acce def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None or config.file_name is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -167,7 +167,7 @@ class NodeFileAccessAction(NodeFileAbstractAction, discriminator="node_file_acce ] -class NodeFileCheckhashAction(NodeFileAbstractAction, discriminator="node_file_checkhash"): +class NodeFileCheckhashAction(NodeFileAbstractAction, discriminator="node-file-checkhash"): """Action which checks the hash of a file.""" config: "NodeFileCheckhashAction.ConfigSchema" @@ -178,7 +178,7 @@ class NodeFileCheckhashAction(NodeFileAbstractAction, discriminator="node_file_c verb: ClassVar[str] = "checkhash" -class NodeFileRepairAction(NodeFileAbstractAction, discriminator="node_file_repair"): +class NodeFileRepairAction(NodeFileAbstractAction, discriminator="node-file-repair"): """Action which repairs a file.""" config: "NodeFileRepairAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index c0a03398..d5731527 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -35,7 +35,7 @@ class NodeFolderAbstractAction(AbstractAction, ABC): def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -47,7 +47,7 @@ class NodeFolderAbstractAction(AbstractAction, ABC): ] -class NodeFolderScanAction(NodeFolderAbstractAction, discriminator="node_folder_scan"): +class NodeFolderScanAction(NodeFolderAbstractAction, discriminator="node-folder-scan"): """Action which scans a folder.""" config: "NodeFolderScanAction.ConfigSchema" @@ -58,7 +58,7 @@ class NodeFolderScanAction(NodeFolderAbstractAction, discriminator="node_folder_ verb: ClassVar[str] = "scan" -class NodeFolderCheckhashAction(NodeFolderAbstractAction, discriminator="node_folder_checkhash"): +class NodeFolderCheckhashAction(NodeFolderAbstractAction, discriminator="node-folder-checkhash"): """Action which checks the hash of a folder.""" config: "NodeFolderCheckhashAction.ConfigSchema" @@ -69,7 +69,7 @@ class NodeFolderCheckhashAction(NodeFolderAbstractAction, discriminator="node_fo verb: ClassVar[str] = "checkhash" -class NodeFolderRepairAction(NodeFolderAbstractAction, discriminator="node_folder_repair"): +class NodeFolderRepairAction(NodeFolderAbstractAction, discriminator="node-folder-repair"): """Action which repairs a folder.""" config: "NodeFolderRepairAction.ConfigSchema" @@ -80,7 +80,7 @@ class NodeFolderRepairAction(NodeFolderAbstractAction, discriminator="node_folde verb: ClassVar[str] = "repair" -class NodeFolderRestoreAction(NodeFolderAbstractAction, discriminator="node_folder_restore"): +class NodeFolderRestoreAction(NodeFolderAbstractAction, discriminator="node-folder-restore"): """Action which restores a folder.""" config: "NodeFolderRestoreAction.ConfigSchema" @@ -91,7 +91,7 @@ class NodeFolderRestoreAction(NodeFolderAbstractAction, discriminator="node_fold verb: ClassVar[str] = "restore" -class NodeFolderCreateAction(NodeFolderAbstractAction, discriminator="node_folder_create"): +class NodeFolderCreateAction(NodeFolderAbstractAction, discriminator="node-folder-create"): """Action which creates a new folder.""" config: "NodeFolderCreateAction.ConfigSchema" @@ -105,7 +105,7 @@ class NodeFolderCreateAction(NodeFolderAbstractAction, discriminator="node_folde def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.folder_name is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index 35599325..f96714b2 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -29,7 +29,7 @@ class HostNICAbstractAction(AbstractAction, ABC): def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.nic_num is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -40,7 +40,7 @@ class HostNICAbstractAction(AbstractAction, ABC): ] -class HostNICEnableAction(HostNICAbstractAction, discriminator="host_nic_enable"): +class HostNICEnableAction(HostNICAbstractAction, discriminator="host-nic-enable"): """Action which enables a NIC.""" config: "HostNICEnableAction.ConfigSchema" @@ -51,7 +51,7 @@ class HostNICEnableAction(HostNICAbstractAction, discriminator="host_nic_enable" verb: ClassVar[str] = "enable" -class HostNICDisableAction(HostNICAbstractAction, discriminator="host_nic_disable"): +class HostNICDisableAction(HostNICAbstractAction, discriminator="host-nic-disable"): """Action which disables a NIC.""" config: "HostNICDisableAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/manager.py b/src/primaite/game/agent/actions/manager.py index 8332368e..0a9d3ffd 100644 --- a/src/primaite/game/agent/actions/manager.py +++ b/src/primaite/game/agent/actions/manager.py @@ -5,9 +5,9 @@ agents: - name: agent_1 action_space: actions: - - do_nothing - - node_service_start - - node_service_stop + - do-nothing + - node-service-start + - node-service-stop action_map: """ @@ -24,18 +24,18 @@ from primaite.interface.request import RequestFormat __all__ = ("DoNothingAction", "ActionManager") -class DoNothingAction(AbstractAction, discriminator="do_nothing"): +class DoNothingAction(AbstractAction, discriminator="do-nothing"): """Do Nothing Action.""" class ConfigSchema(AbstractAction.ConfigSchema): """Configuration Schema for do_nothingAction.""" - type: str = "do_nothing" + type: str = "do-nothing" @classmethod def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" - return ["do_nothing"] + return ["do-nothing"] class _ActionMapItem(BaseModel): diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index afa22861..74d427d0 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -8,7 +8,7 @@ from primaite.interface.request import RequestFormat __all__ = ("NetworkPortEnableAction", "NetworkPortDisableAction") -class NetworkPortAbstractAction(AbstractAction, discriminator="network_port_abstract"): +class NetworkPortAbstractAction(AbstractAction, discriminator="network-port-abstract"): """Base class for Network port actions.""" config: "NetworkPortAbstractAction.ConfigSchema" @@ -24,7 +24,7 @@ class NetworkPortAbstractAction(AbstractAction, discriminator="network_port_abst def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.target_nodename is None or config.port_num is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", @@ -35,7 +35,7 @@ class NetworkPortAbstractAction(AbstractAction, discriminator="network_port_abst ] -class NetworkPortEnableAction(NetworkPortAbstractAction, discriminator="network_port_enable"): +class NetworkPortEnableAction(NetworkPortAbstractAction, discriminator="network-port-enable"): """Action which enables are port on a router or a firewall.""" config: "NetworkPortEnableAction.ConfigSchema" @@ -46,7 +46,7 @@ class NetworkPortEnableAction(NetworkPortAbstractAction, discriminator="network_ verb: ClassVar[str] = "enable" -class NetworkPortDisableAction(NetworkPortAbstractAction, discriminator="network_port_disable"): +class NetworkPortDisableAction(NetworkPortAbstractAction, discriminator="network-port-disable"): """Action which disables are port on a router or a firewall.""" config: "NetworkPortDisableAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 7f7b01a2..be59986f 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -18,7 +18,7 @@ __all__ = ( ) -class NodeAbstractAction(AbstractAction, discriminator="node_abstract"): +class NodeAbstractAction(AbstractAction, discriminator="node-abstract"): """ Abstract base class for node actions. @@ -39,7 +39,7 @@ class NodeAbstractAction(AbstractAction, discriminator="node_abstract"): return ["network", "node", config.node_name, config.verb] -class NodeOSScanAction(NodeAbstractAction, discriminator="node_os_scan"): +class NodeOSScanAction(NodeAbstractAction, discriminator="node-os-scan"): """Action which scans a node's OS.""" config: "NodeOSScanAction.ConfigSchema" @@ -50,7 +50,7 @@ class NodeOSScanAction(NodeAbstractAction, discriminator="node_os_scan"): verb: ClassVar[str] = "scan" -class NodeShutdownAction(NodeAbstractAction, discriminator="node_shutdown"): +class NodeShutdownAction(NodeAbstractAction, discriminator="node-shutdown"): """Action which shuts down a node.""" config: "NodeShutdownAction.ConfigSchema" @@ -61,7 +61,7 @@ class NodeShutdownAction(NodeAbstractAction, discriminator="node_shutdown"): verb: ClassVar[str] = "shutdown" -class NodeStartupAction(NodeAbstractAction, discriminator="node_startup"): +class NodeStartupAction(NodeAbstractAction, discriminator="node-startup"): """Action which starts up a node.""" config: "NodeStartupAction.ConfigSchema" @@ -72,7 +72,7 @@ class NodeStartupAction(NodeAbstractAction, discriminator="node_startup"): verb: ClassVar[str] = "startup" -class NodeResetAction(NodeAbstractAction, discriminator="node_reset"): +class NodeResetAction(NodeAbstractAction, discriminator="node-reset"): """Action which resets a node.""" config: "NodeResetAction.ConfigSchema" @@ -83,7 +83,7 @@ class NodeResetAction(NodeAbstractAction, discriminator="node_reset"): verb: ClassVar[str] = "reset" -class NodeNMAPAbstractAction(AbstractAction, discriminator="node_nmap_abstract_action"): +class NodeNMAPAbstractAction(AbstractAction, discriminator="node-nmap-abstract-action"): """Base class for NodeNMAP actions.""" config: "NodeNMAPAbstractAction.ConfigSchema" @@ -103,8 +103,8 @@ class NodeNMAPAbstractAction(AbstractAction, discriminator="node_nmap_abstract_a pass -class NodeNMAPPingScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_ping_scan"): - """Action which performs an NMAP ping scan.""" +class NodeNMAPPingScanAction(NodeNMAPAbstractAction, discriminator="node-nmap-ping-scan"): + """Action which performs an nmap ping scan.""" config: "NodeNMAPPingScanAction.ConfigSchema" @@ -116,14 +116,14 @@ class NodeNMAPPingScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_pi "node", config.source_node, "application", - "NMAP", + "nmap", "ping_scan", {"target_ip_address": config.target_ip_address, "show": config.show}, ] -class NodeNMAPPortScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_port_scan"): - """Action which performs an NMAP port scan.""" +class NodeNMAPPortScanAction(NodeNMAPAbstractAction, discriminator="node-nmap-port-scan"): + """Action which performs an nmap port scan.""" config: "NodeNMAPPortScanAction.ConfigSchema" @@ -143,7 +143,7 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_po "node", config.source_node, "application", - "NMAP", + "nmap", "port_scan", { "target_ip_address": config.target_ip_address, @@ -154,8 +154,8 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, discriminator="node_nmap_po ] -class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, discriminator="node_network_service_recon"): - """Action which performs an NMAP network service recon (ping scan followed by port scan).""" +class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, discriminator="node-network-service-recon"): + """Action which performs an nmap network service recon (ping scan followed by port scan).""" config: "NodeNetworkServiceReconAction.ConfigSchema" @@ -174,7 +174,7 @@ class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, discriminator="node_ "node", config.source_node, "application", - "NMAP", + "nmap", "network_service_recon", { "target_ip_address": config.target_ip_address, diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index 4adbe139..bb7b689c 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -17,7 +17,7 @@ __all__ = ( ) -class NodeServiceAbstractAction(AbstractAction, discriminator="node_service_abstract"): +class NodeServiceAbstractAction(AbstractAction, discriminator="node-service-abstract"): """Abstract Action for Node Service related actions. Any actions which use node_name and service_name can inherit from this class. @@ -36,7 +36,7 @@ class NodeServiceAbstractAction(AbstractAction, discriminator="node_service_abst return ["network", "node", config.node_name, "service", config.service_name, config.verb] -class NodeServiceScanAction(NodeServiceAbstractAction, discriminator="node_service_scan"): +class NodeServiceScanAction(NodeServiceAbstractAction, discriminator="node-service-scan"): """Action which scans a service.""" config: "NodeServiceScanAction.ConfigSchema" @@ -47,7 +47,7 @@ class NodeServiceScanAction(NodeServiceAbstractAction, discriminator="node_servi verb: ClassVar[str] = "scan" -class NodeServiceStopAction(NodeServiceAbstractAction, discriminator="node_service_stop"): +class NodeServiceStopAction(NodeServiceAbstractAction, discriminator="node-service-stop"): """Action which stops a service.""" config: "NodeServiceStopAction.ConfigSchema" @@ -58,7 +58,7 @@ class NodeServiceStopAction(NodeServiceAbstractAction, discriminator="node_servi verb: ClassVar[str] = "stop" -class NodeServiceStartAction(NodeServiceAbstractAction, discriminator="node_service_start"): +class NodeServiceStartAction(NodeServiceAbstractAction, discriminator="node-service-start"): """Action which starts a service.""" config: "NodeServiceStartAction.ConfigSchema" @@ -69,7 +69,7 @@ class NodeServiceStartAction(NodeServiceAbstractAction, discriminator="node_serv verb: ClassVar[str] = "start" -class NodeServicePauseAction(NodeServiceAbstractAction, discriminator="node_service_pause"): +class NodeServicePauseAction(NodeServiceAbstractAction, discriminator="node-service-pause"): """Action which pauses a service.""" config: "NodeServicePauseAction.ConfigSchema" @@ -80,7 +80,7 @@ class NodeServicePauseAction(NodeServiceAbstractAction, discriminator="node_serv verb: ClassVar[str] = "pause" -class NodeServiceResumeAction(NodeServiceAbstractAction, discriminator="node_service_resume"): +class NodeServiceResumeAction(NodeServiceAbstractAction, discriminator="node-service-resume"): """Action which resumes a service.""" config: "NodeServiceResumeAction.ConfigSchema" @@ -91,7 +91,7 @@ class NodeServiceResumeAction(NodeServiceAbstractAction, discriminator="node_ser verb: ClassVar[str] = "resume" -class NodeServiceRestartAction(NodeServiceAbstractAction, discriminator="node_service_restart"): +class NodeServiceRestartAction(NodeServiceAbstractAction, discriminator="node-service-restart"): """Action which restarts a service.""" config: "NodeServiceRestartAction.ConfigSchema" @@ -102,7 +102,7 @@ class NodeServiceRestartAction(NodeServiceAbstractAction, discriminator="node_se verb: ClassVar[str] = "restart" -class NodeServiceDisableAction(NodeServiceAbstractAction, discriminator="node_service_disable"): +class NodeServiceDisableAction(NodeServiceAbstractAction, discriminator="node-service-disable"): """Action which disables a service.""" config: "NodeServiceDisableAction.ConfigSchema" @@ -113,7 +113,7 @@ class NodeServiceDisableAction(NodeServiceAbstractAction, discriminator="node_se verb: ClassVar[str] = "disable" -class NodeServiceEnableAction(NodeServiceAbstractAction, discriminator="node_service_enable"): +class NodeServiceEnableAction(NodeServiceAbstractAction, discriminator="node-service-enable"): """Action which enables a service.""" config: "NodeServiceEnableAction.ConfigSchema" @@ -124,7 +124,7 @@ class NodeServiceEnableAction(NodeServiceAbstractAction, discriminator="node_ser verb: ClassVar[str] = "enable" -class NodeServiceFixAction(NodeServiceAbstractAction, discriminator="node_service_fix"): +class NodeServiceFixAction(NodeServiceAbstractAction, discriminator="node-service-fix"): """Action which fixes a service.""" config: "NodeServiceFixAction.ConfigSchema" diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 4bed1943..872e5bf4 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -11,7 +11,7 @@ __all__ = ( ) -class NodeSessionAbstractAction(AbstractAction, discriminator="node_session_abstract"): +class NodeSessionAbstractAction(AbstractAction, discriminator="node-session-abstract"): """Base class for NodeSession actions.""" config: "NodeSessionAbstractAction.ConfigSchema" @@ -33,7 +33,7 @@ class NodeSessionAbstractAction(AbstractAction, discriminator="node_session_abst pass -class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="node_session_remote_login"): +class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="node-session-remote-login"): """Action which performs a remote session login.""" config: "NodeSessionsRemoteLoginAction.ConfigSchema" @@ -48,21 +48,21 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="no def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.remote_ip is None: - return ["do_nothing"] + return ["do-nothing"] return [ "network", "node", config.node_name, "service", - "Terminal", - "node_session_remote_login", + "terminal", + "node-session-remote-login", config.username, config.password, config.remote_ip, ] -class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="node_session_remote_logoff"): +class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="node-session-remote-logoff"): """Action which performs a remote session logout.""" config: "NodeSessionsRemoteLogoutAction.ConfigSchema" @@ -76,11 +76,11 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="n def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" if config.node_name is None or config.remote_ip is None: - return ["do_nothing"] - return ["network", "node", config.node_name, "service", "Terminal", config.verb, config.remote_ip] + return ["do-nothing"] + return ["network", "node", config.node_name, "service", "terminal", config.verb, config.remote_ip] -class NodeAccountChangePasswordAction(NodeSessionAbstractAction, discriminator="node_account_change_password"): +class NodeAccountChangePasswordAction(NodeSessionAbstractAction, discriminator="node-account-change-password"): """Action which changes the password for a user.""" config: "NodeAccountChangePasswordAction.ConfigSchema" @@ -100,7 +100,7 @@ class NodeAccountChangePasswordAction(NodeSessionAbstractAction, discriminator=" "node", config.node_name, "service", - "UserManager", + "user-manager", "change_password", config.username, config.current_password, diff --git a/src/primaite/game/agent/actions/software.py b/src/primaite/game/agent/actions/software.py index 49edd7c5..f170146b 100644 --- a/src/primaite/game/agent/actions/software.py +++ b/src/primaite/game/agent/actions/software.py @@ -22,7 +22,7 @@ __all__ = ( ) -class ConfigureRansomwareScriptAction(AbstractAction, discriminator="configure_ransomware_script"): +class ConfigureRansomwareScriptAction(AbstractAction, discriminator="configure-ransomware-script"): """Action which sets config parameters for a ransomware script on a node.""" config: "ConfigureRansomwareScriptAction.ConfigSchema" @@ -39,17 +39,17 @@ class ConfigureRansomwareScriptAction(AbstractAction, discriminator="configure_r def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: - return ["do_nothing"] + return ["do-nothing"] data = dict( server_ip_address=config.server_ip_address, server_password=config.server_password, payload=config.payload, ) - return ["network", "node", config.node_name, "application", "RansomwareScript", "configure", data] + return ["network", "node", config.node_name, "application", "ransomware-script", "configure", data] class RansomwareConfigureC2ServerAction( - ConfigureRansomwareScriptAction, discriminator="c2_server_ransomware_configure" + ConfigureRansomwareScriptAction, discriminator="c2-server-ransomware-configure" ): """Action which causes a C2 server to send a command to set options on a ransomware script remotely.""" @@ -58,10 +58,10 @@ class RansomwareConfigureC2ServerAction( data = dict( server_ip_address=config.server_ip_address, server_password=config.server_password, payload=config.payload ) - return ["network", "node", config.node_name, "application", "C2Server", "ransomware_configure", data] + return ["network", "node", config.node_name, "application", "c2-server", "ransomware_configure", data] -class ConfigureDoSBotAction(AbstractAction, discriminator="configure_dos_bot"): +class ConfigureDoSBotAction(AbstractAction, discriminator="configure-dos-bot"): """Action which sets config parameters for a DoS bot on a node.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -90,10 +90,10 @@ class ConfigureDoSBotAction(AbstractAction, discriminator="configure_dos_bot"): max_sessions=config.max_sessions, ) data = {k: v for k, v in data.items() if v is not None} - return ["network", "node", config.node_name, "application", "DoSBot", "configure", data] + return ["network", "node", config.node_name, "application", "dos-bot", "configure", data] -class ConfigureC2BeaconAction(AbstractAction, discriminator="configure_c2_beacon"): +class ConfigureC2BeaconAction(AbstractAction, discriminator="configure-c2-beacon"): """Action which configures a C2 Beacon based on the parameters given.""" class ConfigSchema(AbstractAction.ConfigSchema): @@ -114,10 +114,10 @@ class ConfigureC2BeaconAction(AbstractAction, discriminator="configure_c2_beacon masquerade_protocol=config.masquerade_protocol, masquerade_port=config.masquerade_port, ) - return ["network", "node", config.node_name, "application", "C2Beacon", "configure", data] + return ["network", "node", config.node_name, "application", "c2-beacon", "configure", data] -class NodeSendRemoteCommandAction(AbstractAction, discriminator="node_send_remote_command"): +class NodeSendRemoteCommandAction(AbstractAction, discriminator="node-send-remote-command"): """Action which sends a terminal command to a remote node via SSH.""" config: "NodeSendRemoteCommandAction.ConfigSchema" @@ -137,14 +137,14 @@ class NodeSendRemoteCommandAction(AbstractAction, discriminator="node_send_remot "node", config.node_name, "service", - "Terminal", + "terminal", "send_remote_command", config.remote_ip, {"command": config.command}, ] -class TerminalC2ServerAction(AbstractAction, discriminator="c2_server_terminal_command"): +class TerminalC2ServerAction(AbstractAction, discriminator="c2-server-terminal-command"): """Action which causes the C2 Server to send a command to the C2 Beacon to execute the terminal command passed.""" config: "TerminalC2ServerAction.ConfigSchema" @@ -162,7 +162,7 @@ class TerminalC2ServerAction(AbstractAction, discriminator="c2_server_terminal_c def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: - return ["do_nothing"] + return ["do-nothing"] command_model = { "commands": config.commands, @@ -170,10 +170,10 @@ class TerminalC2ServerAction(AbstractAction, discriminator="c2_server_terminal_c "username": config.username, "password": config.password, } - return ["network", "node", config.node_name, "application", "C2Server", "terminal_command", command_model] + return ["network", "node", config.node_name, "application", "c2-server", "terminal_command", command_model] -class RansomwareLaunchC2ServerAction(AbstractAction, discriminator="c2_server_ransomware_launch"): +class RansomwareLaunchC2ServerAction(AbstractAction, discriminator="c2-server-ransomware-launch"): """Action which causes the C2 Server to send a command to the C2 Beacon to launch the RansomwareScript.""" config: "RansomwareLaunchC2ServerAction.ConfigSchema" @@ -187,12 +187,12 @@ class RansomwareLaunchC2ServerAction(AbstractAction, discriminator="c2_server_ra def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: - return ["do_nothing"] + return ["do-nothing"] # This action currently doesn't require any further configuration options. - return ["network", "node", config.node_name, "application", "C2Server", "ransomware_launch"] + return ["network", "node", config.node_name, "application", "c2-server", "ransomware_launch"] -class ExfiltrationC2ServerAction(AbstractAction, discriminator="c2_server_data_exfiltrate"): +class ExfiltrationC2ServerAction(AbstractAction, discriminator="c2-server-data-exfiltrate"): """Action which exfiltrates a target file from a certain node onto the C2 beacon and then the C2 Server.""" config: "ExfiltrationC2ServerAction.ConfigSchema" @@ -212,7 +212,7 @@ class ExfiltrationC2ServerAction(AbstractAction, discriminator="c2_server_data_e def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: - return ["do_nothing"] + return ["do-nothing"] command_model = { "target_file_name": config.target_file_name, @@ -222,10 +222,10 @@ class ExfiltrationC2ServerAction(AbstractAction, discriminator="c2_server_data_e "username": config.username, "password": config.password, } - return ["network", "node", config.node_name, "application", "C2Server", "exfiltrate", command_model] + return ["network", "node", config.node_name, "application", "c2-server", "exfiltrate", command_model] -class ConfigureDatabaseClientAction(AbstractAction, discriminator="configure_database_client"): +class ConfigureDatabaseClientAction(AbstractAction, discriminator="configure-database-client"): """Action which sets config parameters for a database client on a node.""" config: "ConfigureDatabaseClientAction.ConfigSchema" @@ -241,6 +241,6 @@ class ConfigureDatabaseClientAction(AbstractAction, discriminator="configure_dat def form_request(cls, config: ConfigSchema) -> RequestFormat: """Return the action formatted as a request that can be ingested by the simulation.""" if config.node_name is None: - return ["do_nothing"] + return ["do-nothing"] data = {"server_ip_address": config.server_ip_address, "server_password": config.server_password} - return ["network", "node", config.node_name, "application", "DatabaseClient", "configure", data] + return ["network", "node", config.node_name, "application", "database-client", "configure", data] diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index cb1c15dd..6e6c1bec 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -130,7 +130,7 @@ class AbstractAgent(BaseModel, ABC): """ # in RL agent, this method will send CAOS observation to RL agent, then receive a int 0-39, # then use a bespoke conversion to take 1-40 int back into CAOS action - return ("do_nothing", {}) + return ("do-nothing", {}) def format_request(self, action: Tuple[str, Dict], options: Dict[str, int]) -> RequestFormat: # this will take something like APPLICATION.EXECUTE and add things like target_ip_address in simulator. @@ -161,7 +161,7 @@ class AbstractAgent(BaseModel, ABC): return agent_class(config=config) -class AbstractScriptedAgent(AbstractAgent, discriminator="AbstractScriptedAgent"): +class AbstractScriptedAgent(AbstractAgent, discriminator="abstract-scripted-agent"): """Base class for actors which generate their own behaviour.""" config: "AbstractScriptedAgent.ConfigSchema" = Field(default_factory=lambda: AbstractScriptedAgent.ConfigSchema()) @@ -177,7 +177,7 @@ class AbstractScriptedAgent(AbstractAgent, discriminator="AbstractScriptedAgent" return super().get_action(obs=obs, timestep=timestep) -class ProxyAgent(AbstractAgent, discriminator="ProxyAgent"): +class ProxyAgent(AbstractAgent, discriminator="proxy-agent"): """Agent that sends observations to an RL model and receives actions from that model.""" config: "ProxyAgent.ConfigSchema" = Field(default_factory=lambda: ProxyAgent.ConfigSchema()) diff --git a/src/primaite/game/agent/observations/acl_observation.py b/src/primaite/game/agent/observations/acl_observation.py index ef171431..b2f5e786 100644 --- a/src/primaite/game/agent/observations/acl_observation.py +++ b/src/primaite/game/agent/observations/acl_observation.py @@ -16,7 +16,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class ACLObservation(AbstractObservation, discriminator="ACL"): +class ACLObservation(AbstractObservation, discriminator="acl"): """ACL observation, provides information about access control lists within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index 82ae9acc..ed9dcd8f 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -13,7 +13,7 @@ from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_ST _LOGGER = getLogger(__name__) -class FileObservation(AbstractObservation, discriminator="FILE"): +class FileObservation(AbstractObservation, discriminator="file"): """File observation, provides status information about a file within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -125,7 +125,7 @@ class FileObservation(AbstractObservation, discriminator="FILE"): ) -class FolderObservation(AbstractObservation, discriminator="FOLDER"): +class FolderObservation(AbstractObservation, discriminator="folder"): """Folder observation, provides status information about a folder within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/firewall_observation.py b/src/primaite/game/agent/observations/firewall_observation.py index f0390697..ac3b30d8 100644 --- a/src/primaite/game/agent/observations/firewall_observation.py +++ b/src/primaite/game/agent/observations/firewall_observation.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class FirewallObservation(AbstractObservation, discriminator="FIREWALL"): +class FirewallObservation(AbstractObservation, discriminator="firewall"): """Firewall observation, provides status information about a firewall within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -181,7 +181,7 @@ class FirewallObservation(AbstractObservation, discriminator="FIREWALL"): }, } if self.include_users: - sess = firewall_state["services"]["UserSessionManager"] + sess = firewall_state["services"]["user-session-manager"] obs["users"] = { "local_login": 1 if sess["current_local_user"] else 0, "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index ed4dd21c..17bcb983 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class HostObservation(AbstractObservation, discriminator="HOST"): +class HostObservation(AbstractObservation, discriminator="host"): """Host observation, provides status information about a host within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -209,7 +209,7 @@ class HostObservation(AbstractObservation, discriminator="HOST"): obs["num_file_creations"] = node_state["file_system"]["num_file_creations"] obs["num_file_deletions"] = node_state["file_system"]["num_file_deletions"] if self.include_users: - sess = node_state["services"]["UserSessionManager"] + sess = node_state["services"]["user-session-manager"] obs["users"] = { "local_login": 1 if sess["current_local_user"] else 0, "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), diff --git a/src/primaite/game/agent/observations/link_observation.py b/src/primaite/game/agent/observations/link_observation.py index 303e421c..014a96c2 100644 --- a/src/primaite/game/agent/observations/link_observation.py +++ b/src/primaite/game/agent/observations/link_observation.py @@ -13,7 +13,7 @@ from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_ST _LOGGER = getLogger(__name__) -class LinkObservation(AbstractObservation, discriminator="LINK"): +class LinkObservation(AbstractObservation, discriminator="link"): """Link observation, providing information about a specific link within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -90,7 +90,7 @@ class LinkObservation(AbstractObservation, discriminator="LINK"): return cls(where=where) -class LinksObservation(AbstractObservation, discriminator="LINKS"): +class LinksObservation(AbstractObservation, discriminator="links"): """Collection of link observations representing multiple links within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 4c8fbaf5..1aa6470d 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -12,7 +12,7 @@ from primaite.utils.validation.ip_protocol import IPProtocol from primaite.utils.validation.port import Port -class NICObservation(AbstractObservation, discriminator="NETWORK_INTERFACE"): +class NICObservation(AbstractObservation, discriminator="network-interface"): """Status information about a network interface within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -227,7 +227,7 @@ class NICObservation(AbstractObservation, discriminator="NETWORK_INTERFACE"): ) -class PortObservation(AbstractObservation, discriminator="PORT"): +class PortObservation(AbstractObservation, discriminator="port"): """Port observation, provides status information about a network port within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 2937aa7c..3a3283a2 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class NodesObservation(AbstractObservation, discriminator="NODES"): +class NodesObservation(AbstractObservation, discriminator="nodes"): """Nodes observation, provides status information about nodes within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 0d28aa98..032435b8 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, computed_field, ConfigDict, Field, model_validat from primaite.game.agent.observations.observations import AbstractObservation, WhereType -class NestedObservation(AbstractObservation, discriminator="CUSTOM"): +class NestedObservation(AbstractObservation, discriminator="custom"): """Observation type that allows combining other observations into a gymnasium.spaces.Dict space.""" class NestedObservationItem(BaseModel): @@ -48,7 +48,7 @@ class NestedObservation(AbstractObservation, discriminator="CUSTOM"): def __init__(self, components: Dict[str, AbstractObservation]) -> None: """Initialise nested observation.""" self.components: Dict[str, AbstractObservation] = components - """Maps label: observation object""" + """Maps label observation object""" self.default_observation = {label: obs.default_observation for label, obs in self.components.items()} """Default observation is just the default observations of constituents.""" @@ -84,7 +84,7 @@ class NestedObservation(AbstractObservation, discriminator="CUSTOM"): ```yaml observation_space: - - type: CUSTOM + - type: custom options: components: @@ -119,7 +119,7 @@ class NestedObservation(AbstractObservation, discriminator="CUSTOM"): return cls(components=instances) -class NullObservation(AbstractObservation, discriminator="NONE"): +class NullObservation(AbstractObservation, discriminator="none"): """Empty observation that acts as a placeholder.""" def __init__(self) -> None: @@ -157,7 +157,7 @@ class ObservationManager(BaseModel): """Config Schema for Observation Manager.""" model_config = ConfigDict(extra="forbid") - type: str = "NONE" + type: str = "none" """discriminator name for the top-level observation.""" options: AbstractObservation.ConfigSchema = Field( default_factory=lambda: NullObservation.ConfigSchema(), validate_default=True @@ -187,7 +187,7 @@ class ObservationManager(BaseModel): return data # (TODO: duplicate default definition between here and the actual model) - obs_type = data["type"] if "type" in data else "NONE" + obs_type = data["type"] if "type" in data else "none" obs_class = AbstractObservation._registry[obs_type] # if no options are passed in, try to create a default schema. Only works if there are no mandatory fields diff --git a/src/primaite/game/agent/observations/router_observation.py b/src/primaite/game/agent/observations/router_observation.py index 8eaad1b1..9a7f51cd 100644 --- a/src/primaite/game/agent/observations/router_observation.py +++ b/src/primaite/game/agent/observations/router_observation.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port _LOGGER = getLogger(__name__) -class RouterObservation(AbstractObservation, discriminator="ROUTER"): +class RouterObservation(AbstractObservation, discriminator="router"): """Router observation, provides status information about a router within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -113,7 +113,7 @@ class RouterObservation(AbstractObservation, discriminator="ROUTER"): if self.ports: obs["PORTS"] = {i + 1: p.observe(state) for i, p in enumerate(self.ports)} if self.include_users: - sess = router_state["services"]["UserSessionManager"] + sess = router_state["services"]["user-session-manager"] obs["users"] = { "local_login": 1 if sess["current_local_user"] else 0, "remote_sessions": min(self.max_users, len(sess["active_remote_sessions"])), diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 6e2fbb73..07ec1abf 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -10,7 +10,7 @@ from primaite.game.agent.observations.observations import AbstractObservation, W from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -class ServiceObservation(AbstractObservation, discriminator="SERVICE"): +class ServiceObservation(AbstractObservation, discriminator="service"): """Service observation, shows status of a service in the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): @@ -73,7 +73,7 @@ class ServiceObservation(AbstractObservation, discriminator="SERVICE"): return cls(where=parent_where + ["services", config.service_name]) -class ApplicationObservation(AbstractObservation, discriminator="APPLICATION"): +class ApplicationObservation(AbstractObservation, discriminator="application"): """Application observation, shows the status of an application within the simulation environment.""" class ConfigSchema(AbstractObservation.ConfigSchema): diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 3e961bdf..59f7a133 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -12,7 +12,7 @@ the structure: ```yaml reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_name: database_server @@ -20,7 +20,7 @@ the structure: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_name: web_server @@ -92,7 +92,7 @@ class AbstractReward(BaseModel): return 0.0 -class DummyReward(AbstractReward, discriminator="DUMMY"): +class DummyReward(AbstractReward, discriminator="dummy"): """Dummy reward function component which always returns 0.0.""" def calculate(self, state: Dict, last_action_response: "AgentHistoryItem") -> float: @@ -108,7 +108,7 @@ class DummyReward(AbstractReward, discriminator="DUMMY"): return 0.0 -class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRITY"): +class DatabaseFileIntegrity(AbstractReward, discriminator="database-file-integrity"): """Reward function component which rewards the agent for maintaining the integrity of a database file.""" config: "DatabaseFileIntegrity.ConfigSchema" @@ -118,7 +118,7 @@ class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRI class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for DatabaseFileIntegrity.""" - type: str = "DATABASE_FILE_INTEGRITY" + type: str = "database-file-integrity" node_hostname: str folder_name: str file_name: str @@ -161,7 +161,7 @@ class DatabaseFileIntegrity(AbstractReward, discriminator="DATABASE_FILE_INTEGRI return 0 -class WebServer404Penalty(AbstractReward, discriminator="WEB_SERVER_404_PENALTY"): +class WebServer404Penalty(AbstractReward, discriminator="web-server-404-penalty"): """Reward function component which penalises the agent when the web server returns a 404 error.""" config: "WebServer404Penalty.ConfigSchema" @@ -171,7 +171,7 @@ class WebServer404Penalty(AbstractReward, discriminator="WEB_SERVER_404_PENALTY" class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebServer404Penalty.""" - type: str = "WEB_SERVER_404_PENALTY" + type: str = "web-server-404-penalty" node_hostname: str service_name: str sticky: bool = True @@ -215,7 +215,7 @@ class WebServer404Penalty(AbstractReward, discriminator="WEB_SERVER_404_PENALTY" return self.reward -class WebpageUnavailablePenalty(AbstractReward, discriminator="WEBPAGE_UNAVAILABLE_PENALTY"): +class WebpageUnavailablePenalty(AbstractReward, discriminator="webpage-unavailable-penalty"): """Penalises the agent when the web browser fails to fetch a webpage.""" config: "WebpageUnavailablePenalty.ConfigSchema" @@ -225,7 +225,7 @@ class WebpageUnavailablePenalty(AbstractReward, discriminator="WEBPAGE_UNAVAILAB class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for WebpageUnavailablePenalty.""" - type: str = "WEBPAGE_UNAVAILABLE_PENALTY" + type: str = "webpage-unavailable-penalty" node_hostname: str = "" sticky: bool = True @@ -248,7 +248,7 @@ class WebpageUnavailablePenalty(AbstractReward, discriminator="WEBPAGE_UNAVAILAB "nodes", self.config.node_hostname, "applications", - "WebBrowser", + "web-browser", ] web_browser_state = access_from_nested_dict(state, self.location_in_state) @@ -261,7 +261,7 @@ class WebpageUnavailablePenalty(AbstractReward, discriminator="WEBPAGE_UNAVAILAB "node", self.config.node_hostname, "application", - "WebBrowser", + "web-browser", "execute", ] @@ -289,7 +289,7 @@ class WebpageUnavailablePenalty(AbstractReward, discriminator="WEBPAGE_UNAVAILAB return self.reward -class GreenAdminDatabaseUnreachablePenalty(AbstractReward, discriminator="GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY"): +class GreenAdminDatabaseUnreachablePenalty(AbstractReward, discriminator="green-admin-database-unreachable-penalty"): """Penalises the agent when the green db clients fail to connect to the database.""" config: "GreenAdminDatabaseUnreachablePenalty.ConfigSchema" @@ -298,7 +298,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, discriminator="GREEN_ class ConfigSchema(AbstractReward.ConfigSchema): """ConfigSchema for GreenAdminDatabaseUnreachablePenalty.""" - type: str = "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY" + type: str = "green-admin-database-unreachable-penalty" node_hostname: str sticky: bool = True @@ -322,7 +322,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, discriminator="GREEN_ "node", self.config.node_hostname, "application", - "DatabaseClient", + "database-client", "execute", ] @@ -339,7 +339,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward, discriminator="GREEN_ return self.reward -class SharedReward(AbstractReward, discriminator="SHARED_REWARD"): +class SharedReward(AbstractReward, discriminator="shared-reward"): """Adds another agent's reward to the overall reward.""" config: "SharedReward.ConfigSchema" @@ -347,7 +347,7 @@ class SharedReward(AbstractReward, discriminator="SHARED_REWARD"): class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for SharedReward.""" - type: str = "SHARED_REWARD" + type: str = "shared-reward" agent_name: str def default_callback(agent_name: str) -> Never: @@ -376,17 +376,17 @@ class SharedReward(AbstractReward, discriminator="SHARED_REWARD"): return self.callback(self.config.agent_name) -class ActionPenalty(AbstractReward, discriminator="ACTION_PENALTY"): - """Apply a negative reward when taking any action except do_nothing.""" +class ActionPenalty(AbstractReward, discriminator="action-penalty"): + """Apply a negative reward when taking any action except do-nothing.""" config: "ActionPenalty.ConfigSchema" class ConfigSchema(AbstractReward.ConfigSchema): """Config schema for ActionPenalty. - :param action_penalty: Reward to give agents for taking any action except do_nothing + :param action_penalty: Reward to give agents for taking any action except do-nothing :type action_penalty: float - :param do_nothing_penalty: Reward to give agent for taking the do_nothing action + :param do_nothing_penalty: Reward to give agent for taking the do-nothing action :type do_nothing_penalty: float """ @@ -403,7 +403,7 @@ class ActionPenalty(AbstractReward, discriminator="ACTION_PENALTY"): :return: Reward value :rtype: float """ - if last_action_response.action == "do_nothing": + if last_action_response.action == "do-nothing": return self.config.do_nothing_penalty else: diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index f36c93de..b0a8ae29 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -13,7 +13,7 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "AbstractTAPAgent" -class AbstractTAPAgent(PeriodicAgent, discriminator="AbstractTAP"): +class AbstractTAPAgent(PeriodicAgent, discriminator="abstract-tap"): """Base class for TAP agents to inherit from.""" config: "AbstractTAPAgent.ConfigSchema" = Field(default_factory=lambda: AbstractTAPAgent.ConfigSchema()) @@ -27,7 +27,7 @@ class AbstractTAPAgent(PeriodicAgent, discriminator="AbstractTAP"): class ConfigSchema(PeriodicAgent.ConfigSchema): """Configuration schema for Abstract TAP agents.""" - type: str = "AbstractTAP" + type: str = "abstract-tap" agent_settings: AbstractTAPAgent.AgentSettingsSchema = Field( default_factory=lambda: AbstractTAPAgent.AgentSettingsSchema() ) diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index b32df428..fc0b1869 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -9,18 +9,18 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "DataManipulationAgent" -class DataManipulationAgent(PeriodicAgent, discriminator="RedDatabaseCorruptingAgent"): +class DataManipulationAgent(PeriodicAgent, discriminator="red-database-corrupting-agent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" class AgentSettingsSchema(PeriodicAgent.AgentSettingsSchema): """Schema for the `agent_settings` part of the agent config.""" - target_application: str = "DataManipulationBot" + target_application: str = "data-manipulation-bot" class ConfigSchema(PeriodicAgent.ConfigSchema): """Configuration Schema for DataManipulationAgent.""" - type: str = "RedDatabaseCorruptingAgent" + type: str = "red-database-corrupting-agent" agent_settings: "DataManipulationAgent.AgentSettingsSchema" = Field( default_factory=lambda: DataManipulationAgent.AgentSettingsSchema() ) @@ -43,13 +43,13 @@ class DataManipulationAgent(PeriodicAgent, discriminator="RedDatabaseCorruptingA """ if timestep < self.next_execution_timestep: self.logger.debug(msg="Performing do nothing action") - return "do_nothing", {} + return "do-nothing", {} self._set_next_execution_timestep( timestep=timestep + self.config.agent_settings.frequency, variance=self.config.agent_settings.variance ) self.logger.info(msg="Performing a data manipulation attack!") - return "node_application_execute", { + return "node-application-execute", { "node_name": self.start_node, "application_name": self.config.agent_settings.target_application, } diff --git a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 2ddc39b7..babf9179 100644 --- a/src/primaite/game/agent/scripted_agents/probabilistic_agent.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -13,7 +13,7 @@ from primaite.game.agent.interface import AbstractScriptedAgent __all__ = "ProbabilisticAgent" -class ProbabilisticAgent(AbstractScriptedAgent, discriminator="ProbabilisticAgent"): +class ProbabilisticAgent(AbstractScriptedAgent, discriminator="probabilistic-agent"): """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" rng: Generator = Field(default_factory=lambda: np.random.default_rng(np.random.randint(0, 65535))) @@ -46,7 +46,7 @@ class ProbabilisticAgent(AbstractScriptedAgent, discriminator="ProbabilisticAgen class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration schema for Probabilistic Agent.""" - type: str = "ProbabilisticAgent" + type: str = "probabilistic-agent" agent_settings: "ProbabilisticAgent.AgentSettingsSchema" = Field( default_factory=lambda: ProbabilisticAgent.AgentSettingsSchema() ) diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py index 3d652dfc..eebf2c93 100644 --- a/src/primaite/game/agent/scripted_agents/random_agent.py +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -11,7 +11,7 @@ from primaite.game.agent.interface import AbstractScriptedAgent __all__ = ("RandomAgent", "PeriodicAgent") -class RandomAgent(AbstractScriptedAgent, discriminator="RandomAgent"): +class RandomAgent(AbstractScriptedAgent, discriminator="random-agent"): """Agent that ignores its observation and acts completely at random.""" config: "RandomAgent.ConfigSchema" = Field(default_factory=lambda: RandomAgent.ConfigSchema()) @@ -19,7 +19,7 @@ class RandomAgent(AbstractScriptedAgent, discriminator="RandomAgent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Random Agents.""" - type: str = "RandomAgent" + type: str = "random-agent" def get_action(self) -> Tuple[str, Dict]: """Sample the action space randomly. @@ -34,7 +34,7 @@ class RandomAgent(AbstractScriptedAgent, discriminator="RandomAgent"): return self.action_manager.get_action(self.action_manager.space.sample()) -class PeriodicAgent(AbstractScriptedAgent, discriminator="PeriodicAgent"): +class PeriodicAgent(AbstractScriptedAgent, discriminator="periodic-agent"): """Agent that does nothing most of the time, but executes application at regular intervals (with variance).""" config: "PeriodicAgent.ConfigSchema" = Field(default_factory=lambda: PeriodicAgent.ConfigSchema()) @@ -72,7 +72,7 @@ class PeriodicAgent(AbstractScriptedAgent, discriminator="PeriodicAgent"): class ConfigSchema(AbstractScriptedAgent.ConfigSchema): """Configuration Schema for Periodic Agent.""" - type: str = "PeriodicAgent" + type: str = "periodic-agent" """Name of the agent.""" agent_settings: "PeriodicAgent.AgentSettingsSchema" = Field( default_factory=lambda: PeriodicAgent.AgentSettingsSchema() @@ -113,9 +113,9 @@ class PeriodicAgent(AbstractScriptedAgent, discriminator="PeriodicAgent"): self._set_next_execution_timestep( timestep + self.config.agent_settings.frequency, self.config.agent_settings.variance ) - return "node_application_execute", { + return "node-application-execute", { "node_name": self.start_node, "application_name": self.config.agent_settings.target_application, } - return "do_nothing", {} + return "do-nothing", {} diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 66a684de..ef4e75dd 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -109,7 +109,7 @@ " - install\n", " - RansomwareScript\n", " 5:\n", - " action: c2_server_ransomware_configure\n", + " action: c2-server-ransomware-configure\n", " options:\n", " node_id: 1\n", " config:\n", @@ -416,7 +416,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | c2_server_ransomware_configure\n", + "### **Command and Control** | C2 Server Actions | c2-server-ransomware-configure\n", "\n", "Another action the C2 Server grants is the ability for a Red Agent to configure the RansomwareScript via the C2 Server rather than the note directly.\n", "\n", @@ -435,7 +435,7 @@ " ...\n", " action_map:\n", " 5:\n", - " action: c2_server_ransomware_configure\n", + " action: c2-server-ransomware-configure\n", " options:\n", " node_id: 1\n", " config:\n", diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 7ccd202e..750372b3 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -244,7 +244,7 @@ class SimComponent(BaseModel): ..code::python - class WebBrowser(Application, discriminator="WebBrowser"): + class WebBrowser(Application, discriminator="web-browser"): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() # all requests generic to any Application get initialised rm.add_request(...) # initialise any requests specific to the web browser diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index bad26a0a..58607bf6 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -130,8 +130,8 @@ class File(FileSystemItemABC): Return False if corruption is detected, otherwise True """ - warnings.warn("node_file_checkhash is currently not implemented.") - self.sys_log.warning("node_file_checkhash is currently not implemented.") + warnings.warn("node-file-checkhash is currently not implemented.") + self.sys_log.warning("node-file-checkhash is currently not implemented.") return False if self.deleted: diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 3dd5d1ce..5b9a6931 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -396,8 +396,8 @@ class Folder(FileSystemItemABC): Return False if corruption is detected, otherwise True """ - warnings.warn("node_folder_checkhash is currently not implemented.") - self.sys_log.error("node_folder_checkhash is currently not implemented.") + warnings.warn("node-folder-checkhash is currently not implemented.") + self.sys_log.error("node-folder-checkhash is currently not implemented.") return False if self.deleted: diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index e16a7fcc..fdc5f2a1 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -22,7 +22,7 @@ class NetworkNodeAdder(BaseModel): Here is a template that users can use to define custom node adders: ``` - class YourNodeAdder(NetworkNodeAdder, discriminator="your_name"): + class YourNodeAdder(NetworkNodeAdder, discriminator="your-name"): class ConfigSchema(NetworkNodeAdder.ConfigSchema): property_1 : str property_2 : int @@ -99,13 +99,13 @@ class NetworkNodeAdder(BaseModel): adder_class.add_nodes_to_net(config=adder_class.ConfigSchema(**config), network=network) -class OfficeLANAdder(NetworkNodeAdder, discriminator="office_lan"): +class OfficeLANAdder(NetworkNodeAdder, discriminator="office-lan"): """Creates an office LAN.""" class ConfigSchema(NetworkNodeAdder.ConfigSchema): """Configuration schema for OfficeLANAdder.""" - type: Literal["office_lan"] = "office_lan" + type: Literal["office-lan"] = "office-lan" lan_name: str """Name of lan used for generating hostnames for new nodes.""" subnet_base: int diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index bacba15b..89254459 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -824,7 +824,7 @@ class User(SimComponent): return self.model_dump() -class UserManager(Service, discriminator="UserManager"): +class UserManager(Service, discriminator="user-manager"): """ Manages users within the PrimAITE system, handling creation, authentication, and administration. @@ -836,7 +836,7 @@ class UserManager(Service, discriminator="UserManager"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for UserManager.""" - type: str = "UserManager" + type: str = "user-manager" config: "UserManager.ConfigSchema" = Field(default_factory=lambda: UserManager.ConfigSchema()) @@ -849,7 +849,7 @@ class UserManager(Service, discriminator="UserManager"): :param username: The username for the default admin user :param password: The password for the default admin user """ - kwargs["name"] = "UserManager" + kwargs["name"] = "user-manager" kwargs["port"] = PORT_LOOKUP["NONE"] kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) @@ -1037,7 +1037,7 @@ class UserManager(Service, discriminator="UserManager"): @property def _user_session_manager(self) -> "UserSessionManager": - return self.software_manager.software["UserSessionManager"] # noqa + return self.software_manager.software["user-session-manager"] # noqa class UserSession(SimComponent): @@ -1137,7 +1137,7 @@ class RemoteUserSession(UserSession): return state -class UserSessionManager(Service, discriminator="UserSessionManager"): +class UserSessionManager(Service, discriminator="user-session-manager"): """ Manages user sessions on a Node, including local and remote sessions. @@ -1147,7 +1147,7 @@ class UserSessionManager(Service, discriminator="UserSessionManager"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for UserSessionManager.""" - type: str = "UserSessionManager" + type: str = "user-session-manager" config: "UserSessionManager.ConfigSchema" = Field(default_factory=lambda: UserSessionManager.ConfigSchema()) @@ -1179,7 +1179,7 @@ class UserSessionManager(Service, discriminator="UserSessionManager"): :param username: The username for the default admin user :param password: The password for the default admin user """ - kwargs["name"] = "UserSessionManager" + kwargs["name"] = "user-session-manager" kwargs["port"] = PORT_LOOKUP["NONE"] kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) @@ -1289,7 +1289,7 @@ class UserSessionManager(Service, discriminator="UserSessionManager"): :return: The UserManager instance. """ - return self.software_manager.software["UserManager"] # noqa + return self.software_manager.software["user-manager"] # noqa def pre_timestep(self, timestep: int) -> None: """Apply any pre-timestep logic that helps make sure we have the correct observations.""" @@ -1608,17 +1608,17 @@ class Node(SimComponent, ABC): @property def user_manager(self) -> Optional[UserManager]: """The Nodes User Manager.""" - return self.software_manager.software.get("UserManager") # noqa + return self.software_manager.software.get("user-manager") # noqa @property def user_session_manager(self) -> Optional[UserSessionManager]: """The Nodes User Session Manager.""" - return self.software_manager.software.get("UserSessionManager") # noqa + return self.software_manager.software.get("user-session-manager") # noqa @property def terminal(self) -> Optional[Terminal]: - """The Nodes Terminal.""" - return self.software_manager.software.get("Terminal") + """The Node's Terminal.""" + return self.software_manager.software.get("terminal") def local_login(self, username: str, password: str) -> Optional[str]: """ diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index a47af2ad..ed11b8e5 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -33,6 +33,6 @@ class Computer(HostNode, discriminator="computer"): * Web Browser """ - SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} + SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "ftp-client": FTPClient} pass diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index f8786a08..13796602 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -262,7 +262,7 @@ class NIC(IPWiredNetworkInterface): return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" -class HostNode(Node, discriminator="HostNode"): +class HostNode(Node, discriminator="host-node"): """ Represents a host node in the network. @@ -309,14 +309,14 @@ class HostNode(Node, discriminator="HostNode"): SYSTEM_SOFTWARE: ClassVar[Dict] = { "HostARP": HostARP, - "ICMP": ICMP, - "DNSClient": DNSClient, - "NTPClient": NTPClient, - "WebBrowser": WebBrowser, - "NMAP": NMAP, - "UserSessionManager": UserSessionManager, - "UserManager": UserManager, - "Terminal": Terminal, + "icmp": ICMP, + "dns-client": DNSClient, + "ntp-client": NTPClient, + "web-browser": WebBrowser, + "nmap": NMAP, + "user-session-manager": UserSessionManager, + "user-manager": UserManager, + "terminal": Terminal, } """List of system software that is automatically installed on nodes.""" @@ -337,7 +337,7 @@ class HostNode(Node, discriminator="HostNode"): :return: NMAP application installed on the Node. :rtype: Optional[NMAP] """ - return self.software_manager.software.get("NMAP") + return self.software_manager.software.get("nmap") @property def arp(self) -> Optional[ARP]: @@ -347,7 +347,7 @@ class HostNode(Node, discriminator="HostNode"): :return: ARP Cache for given HostNode :rtype: Optional[ARP] """ - return self.software_manager.software.get("ARP") + return self.software_manager.software.get("arp") def default_gateway_hello(self): """ @@ -379,8 +379,8 @@ class HostNode(Node, discriminator="HostNode"): dst_port = frame.udp.dst_port can_accept_nmap = False - if self.software_manager.software.get("NMAP"): - if self.software_manager.software["NMAP"].operating_state == ApplicationOperatingState.RUNNING: + if self.software_manager.software.get("nmap"): + if self.software_manager.software["nmap"].operating_state == ApplicationOperatingState.RUNNING: can_accept_nmap = True accept_nmap = can_accept_nmap and frame.payload.__class__.__name__ == "PortScanPayload" diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 185b6bae..388b57d7 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -7,7 +7,7 @@ from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.services.arp.arp import ARP -class NetworkNode(Node, discriminator="NetworkNode"): +class NetworkNode(Node, discriminator="network-node"): """ Represents an abstract base class for a network node that can receive and process network frames. @@ -40,4 +40,4 @@ class NetworkNode(Node, discriminator="NetworkNode"): :return: ARP Cache for given NetworkNode :rtype: Optional[ARP] """ - return self.software_manager.software.get("ARP") + return self.software_manager.software.get("arp") diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 87408670..ca962c24 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -91,7 +91,7 @@ class WirelessAccessPoint(IPWirelessNetworkInterface): ) -class WirelessRouter(Router, discriminator="wireless_router"): +class WirelessRouter(Router, discriminator="wireless-router"): """ A WirelessRouter class that extends the functionality of a standard Router to include wireless capabilities. diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index c840748e..97dde839 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -160,7 +160,7 @@ def arcd_uc2_network() -> Network: db_client_1: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") db_client_1.configure(server_ip_address=IPv4Address("192.168.1.14")) db_client_1.run() - web_browser_1 = client_1.software_manager.software.get("WebBrowser") + web_browser_1 = client_1.software_manager.software.get("web-browser") web_browser_1.target_url = "http://arcd.com/users/" client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") @@ -182,10 +182,10 @@ def arcd_uc2_network() -> Network: ) client_2.power_on() client_2.software_manager.install(DatabaseClient) - db_client_2 = client_2.software_manager.software.get("DatabaseClient") + db_client_2 = client_2.software_manager.software.get("database-client") db_client_2.configure(server_ip_address=IPv4Address("192.168.1.14")) db_client_2.run() - web_browser_2 = client_2.software_manager.software.get("WebBrowser") + web_browser_2 = client_2.software_manager.software.get("web-browser") web_browser_2.target_url = "http://arcd.com/users/" network.connect( endpoint_b=client_2.network_interface[1], @@ -218,7 +218,7 @@ def arcd_uc2_network() -> Network: network.connect(endpoint_b=database_server.network_interface[1], endpoint_a=switch_1.network_interface[3]) database_server.software_manager.install(DatabaseService) - database_service: DatabaseService = database_server.software_manager.software.get("DatabaseService") # noqa + database_service: DatabaseService = database_server.software_manager.software.get("database-service") # noqa database_service.start() database_service.configure_backup(backup_server=IPv4Address("192.168.1.16")) @@ -234,7 +234,7 @@ def arcd_uc2_network() -> Network: web_server.power_on() web_server.software_manager.install(DatabaseClient) - database_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = web_server.software_manager.software.get("database-client") database_client.configure(server_ip_address=IPv4Address("192.168.1.14")) network.connect(endpoint_b=web_server.network_interface[1], endpoint_a=switch_1.network_interface[2]) database_client.run() @@ -243,7 +243,7 @@ def arcd_uc2_network() -> Network: web_server.software_manager.install(WebServer) # register the web_server to a domain - dns_server_service: DNSServer = domain_controller.software_manager.software.get("DNSServer") # noqa + dns_server_service: DNSServer = domain_controller.software_manager.software.get("dns-server") # noqa dns_server_service.dns_register("arcd.com", web_server.network_interface[1].ip_address) # Backup Server diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 2a1deef4..abc83203 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -38,8 +38,8 @@ class Simulation(SimComponent): rm.add_request("network", RequestType(func=self.network._request_manager)) # pass through domain requests to the domain object rm.add_request("domain", RequestType(func=self.domain._request_manager)) - # if 'do_nothing' is requested, just return a success - rm.add_request("do_nothing", RequestType(func=lambda request, context: RequestResponse(status="success"))) + # if 'do-nothing' is requested, just return a success + rm.add_request("do-nothing", RequestType(func=lambda request, context: RequestResponse(status="success"))) return rm def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 67749e21..108687b9 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -61,7 +61,7 @@ class DatabaseClientConnection(BaseModel): return str(self) -class DatabaseClient(Application, discriminator="DatabaseClient"): +class DatabaseClient(Application, discriminator="database-client"): """ A DatabaseClient application. @@ -72,7 +72,7 @@ class DatabaseClient(Application, discriminator="DatabaseClient"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for DatabaseClient.""" - type: str = "DatabaseClient" + type: str = "database-client" db_server_ip: Optional[IPV4Address] = None server_password: Optional[str] = None @@ -97,7 +97,7 @@ class DatabaseClient(Application, discriminator="DatabaseClient"): """Native Client Connection for using the client directly (similar to psql in a terminal).""" def __init__(self, **kwargs): - kwargs["name"] = "DatabaseClient" + kwargs["name"] = "database-client" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 6a29aedf..5aada5fb 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -44,7 +44,7 @@ class PortScanPayload(SimComponent): return state -class NMAP(Application, discriminator="NMAP"): +class NMAP(Application, discriminator="nmap"): """ A class representing the NMAP application for network scanning. @@ -55,7 +55,7 @@ class NMAP(Application, discriminator="NMAP"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for NMAP.""" - type: str = "NMAP" + type: str = "nmap" config: "NMAP.ConfigSchema" = Field(default_factory=lambda: NMAP.ConfigSchema()) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 71a896bc..1c2c1179 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -162,11 +162,11 @@ class AbstractC2(Application): :return: An FTPClient object is successful, else None :rtype: union[FTPClient, None] """ - ftp_client: Union[FTPClient, None] = self.software_manager.software.get("FTPClient") + ftp_client: Union[FTPClient, None] = self.software_manager.software.get("ftp-client") if ftp_client is None: self.sys_log.warning(f"{self.__class__.__name__}: No FTPClient. Attempting to install.") self.software_manager.install(FTPClient) - ftp_client = self.software_manager.software.get("FTPClient") + ftp_client = self.software_manager.software.get("ftp-client") # Force start if the service is stopped. if ftp_client.operating_state == ServiceOperatingState.STOPPED: @@ -189,11 +189,11 @@ class AbstractC2(Application): :return: An FTPServer object is successful, else None :rtype: Optional[FTPServer] """ - ftp_server: Optional[FTPServer] = self.software_manager.software.get("FTPServer") + ftp_server: Optional[FTPServer] = self.software_manager.software.get("ftp-server") if ftp_server is None: self.sys_log.warning(f"{self.__class__.__name__}:No FTPServer installed. Attempting to install FTPServer.") self.software_manager.install(FTPServer) - ftp_server = self.software_manager.software.get("FTPServer") + ftp_server = self.software_manager.software.get("ftp-server") # Force start if the service is stopped. if ftp_server.operating_state == ServiceOperatingState.STOPPED: diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py index 14e446a4..486a0eaf 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_beacon.py @@ -17,7 +17,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import Port, PORT_LOOKUP -class C2Beacon(AbstractC2, discriminator="C2Beacon"): +class C2Beacon(AbstractC2, discriminator="c2-beacon"): """ C2 Beacon Application. @@ -39,7 +39,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): class ConfigSchema(AbstractC2.ConfigSchema): """ConfigSchema for C2Beacon.""" - type: str = "C2Beacon" + type: str = "c2-beacon" c2_server_ip_address: Optional[IPV4Address] = None keep_alive_frequency: int = 5 masquerade_protocol: IPProtocol = PROTOCOL_LOOKUP["TCP"] @@ -54,13 +54,13 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): "The currently in use terminal session." def __init__(self, **kwargs): - kwargs["name"] = "C2Beacon" + kwargs["name"] = "c2-beacon" super().__init__(**kwargs) @property def _host_terminal(self) -> Optional[Terminal]: - """Return the Terminal that is installed on the same machine as the C2 Beacon.""" - host_terminal: Terminal = self.software_manager.software.get("Terminal") + """Return the terminal that is installed on the same machine as the C2 Beacon.""" + host_terminal: Terminal = self.software_manager.software.get("terminal") if host_terminal is None: self.sys_log.warning(f"{self.__class__.__name__} cannot find a terminal on its host.") return host_terminal @@ -68,7 +68,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): @property def _host_ransomware_script(self) -> RansomwareScript: """Return the RansomwareScript that is installed on the same machine as the C2 Beacon.""" - ransomware_script: RansomwareScript = self.software_manager.software.get("RansomwareScript") + ransomware_script: RansomwareScript = self.software_manager.software.get("ransomware-script") if ransomware_script is None: self.sys_log.warning(f"{self.__class__.__name__} cannot find installed ransomware on its host.") return ransomware_script @@ -300,7 +300,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. - :return: Returns the Request Response returned by the Terminal execute method. + :return: Returns the Request Response returned by the terminal execute method. :rtype: Request Response """ command_opts = RansomwareOpts.model_validate(payload.payload) @@ -324,7 +324,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. - :return: Returns the Request Response returned by the Terminal execute method. + :return: Returns the Request Response returned by the terminal execute method. :rtype: Request Response """ if self._host_ransomware_script is None: @@ -351,7 +351,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. - :return: Returns a tuple containing Request Response returned by the Terminal execute method. + :return: Returns a tuple containing Request Response returned by the terminal execute method. :rtype: Request Response """ if self._host_ftp_server is None: @@ -372,7 +372,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): ) # Using the terminal to start the FTP Client on the remote machine. - self.terminal_session.execute(command=["service", "start", "FTPClient"]) + self.terminal_session.execute(command=["service", "start", "ftp-client"]) # Need to supply to the FTP Client the C2 Beacon's host IP. host_network_interfaces = self.software_manager.node.network_interfaces @@ -430,7 +430,7 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): # Using the terminal to send the target data back to the C2 Beacon. exfil_response: RequestResponse = RequestResponse.from_bool( - self.terminal_session.execute(command=["service", "FTPClient", "send", ftp_opts]) + self.terminal_session.execute(command=["service", "ftp-client", "send", ftp_opts]) ) # Validating that we successfully received the target data. @@ -472,14 +472,14 @@ class C2Beacon(AbstractC2, discriminator="C2Beacon"): def _command_terminal(self, payload: C2Packet) -> RequestResponse: """ - C2 Command: Terminal. + C2 Command: terminal. Creates a request that executes a terminal command. This request is then sent to the terminal service in order to be executed. :payload C2Packet: The incoming INPUT command. :type Masquerade Packet: C2Packet. - :return: Returns the Request Response returned by the Terminal execute method. + :return: Returns the Request Response returned by the terminal execute method. :rtype: Request Response """ command_opts = TerminalOpts.model_validate(payload.payload) diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index df4c34a8..987029e4 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -16,7 +16,7 @@ from primaite.simulator.system.applications.red_applications.c2 import ( from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import AbstractC2, C2Command, C2Payload -class C2Server(AbstractC2, discriminator="C2Server"): +class C2Server(AbstractC2, discriminator="c2-server"): """ C2 Server Application. @@ -37,7 +37,7 @@ class C2Server(AbstractC2, discriminator="C2Server"): class ConfigSchema(AbstractC2.ConfigSchema): """ConfigSchema for C2Server.""" - type: str = "C2Server" + type: str = "c2-server" config: ConfigSchema = Field(default_factory=lambda: C2Server.ConfigSchema()) @@ -125,7 +125,7 @@ class C2Server(AbstractC2, discriminator="C2Server"): return rm def __init__(self, **kwargs): - kwargs["name"] = "C2Server" + kwargs["name"] = "c2-server" super().__init__(**kwargs) self.run() diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 7ad31e3b..7bf7b203 100644 --- a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py @@ -40,13 +40,13 @@ class DataManipulationAttackStage(IntEnum): "Signifies that the attack has failed." -class DataManipulationBot(Application, discriminator="DataManipulationBot"): +class DataManipulationBot(Application, discriminator="data-manipulation-bot"): """A bot that simulates a script which performs a SQL injection attack.""" class ConfigSchema(Application.ConfigSchema): """Configuration schema for DataManipulationBot.""" - type: str = "DataManipulationBot" + type: str = "data-manipulation-bot" server_ip: Optional[IPV4Address] = None server_password: Optional[str] = None payload: str = "DELETE" @@ -64,7 +64,7 @@ class DataManipulationBot(Application, discriminator="DataManipulationBot"): "Whether to repeat attacking once finished." def __init__(self, **kwargs): - kwargs["name"] = "DataManipulationBot" + kwargs["name"] = "data-manipulation-bot" kwargs["port"] = PORT_LOOKUP["NONE"] kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] @@ -92,7 +92,7 @@ class DataManipulationBot(Application, discriminator="DataManipulationBot"): @property def _host_db_client(self) -> DatabaseClient: """Return the database client that is installed on the same machine as the DataManipulationBot.""" - db_client = self.software_manager.software.get("DatabaseClient") + db_client = self.software_manager.software.get("database-client") if db_client is None: self.sys_log.warning(f"{self.__class__.__name__} cannot find a database client on its host.") return db_client diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index 6153c2a5..1528de57 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -32,13 +32,13 @@ class DoSAttackStage(IntEnum): "Attack is completed." -class DoSBot(DatabaseClient, discriminator="DoSBot"): +class DoSBot(DatabaseClient, discriminator="dos-bot"): """A bot that simulates a Denial of Service attack.""" class ConfigSchema(DatabaseClient.ConfigSchema): """ConfigSchema for DoSBot.""" - type: str = "DoSBot" + type: str = "dos-bot" target_ip_address: Optional[IPV4Address] = None target_port: Port = PORT_LOOKUP["POSTGRES_SERVER"] payload: Optional[str] = None @@ -72,7 +72,7 @@ class DoSBot(DatabaseClient, discriminator="DoSBot"): def __init__(self, **kwargs): super().__init__(**kwargs) - self.name = "DoSBot" + self.name = "dos-bot" self.target_ip_address = self.config.target_ip_address self.target_port = self.config.target_port self.payload = self.config.payload diff --git a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py index 0a818a85..450311ba 100644 --- a/src/primaite/simulator/system/applications/red_applications/ransomware_script.py +++ b/src/primaite/simulator/system/applications/red_applications/ransomware_script.py @@ -14,7 +14,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP -class RansomwareScript(Application, discriminator="RansomwareScript"): +class RansomwareScript(Application, discriminator="ransomware-script"): """Ransomware Kill Chain - Designed to be used by the TAP001 Agent on the example layout Network. :ivar payload: The attack stage query payload. (Default ENCRYPT) @@ -23,7 +23,7 @@ class RansomwareScript(Application, discriminator="RansomwareScript"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for RansomwareScript.""" - type: str = "RansomwareScript" + type: str = "ransomware-script" server_ip: Optional[IPV4Address] = None server_password: Optional[str] = None payload: str = "ENCRYPT" @@ -38,7 +38,7 @@ class RansomwareScript(Application, discriminator="RansomwareScript"): "Payload String for the payload stage" def __init__(self, **kwargs): - kwargs["name"] = "RansomwareScript" + kwargs["name"] = "ransomware-script" kwargs["port"] = PORT_LOOKUP["NONE"] kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] @@ -63,7 +63,7 @@ class RansomwareScript(Application, discriminator="RansomwareScript"): @property def _host_db_client(self) -> DatabaseClient: """Return the database client that is installed on the same machine as the Ransomware Script.""" - db_client: DatabaseClient = self.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = self.software_manager.software.get("database-client") if db_client is None: self.sys_log.warning(f"{self.__class__.__name__} cannot find a database client on its host.") return db_client diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 3eb18f7f..f4944652 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -23,7 +23,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class WebBrowser(Application, discriminator="WebBrowser"): +class WebBrowser(Application, discriminator="web-browser"): """ Represents a web browser in the simulation environment. @@ -33,7 +33,7 @@ class WebBrowser(Application, discriminator="WebBrowser"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for WebBrowser.""" - type: str = "WebBrowser" + type: str = "web-browser" target_url: Optional[str] = None config: "WebBrowser.ConfigSchema" = Field(default_factory=lambda: WebBrowser.ConfigSchema()) @@ -48,7 +48,7 @@ class WebBrowser(Application, discriminator="WebBrowser"): """Keep a log of visited websites and information about the visit, such as response code.""" def __init__(self, **kwargs): - kwargs["name"] = "WebBrowser" + kwargs["name"] = "web-browser" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] # default for web is port 80 if kwargs.get("port") is None: @@ -108,7 +108,7 @@ class WebBrowser(Application, discriminator="WebBrowser"): return False # get the IP address of the domain name via DNS - dns_client: DNSClient = self.software_manager.software.get("DNSClient") + dns_client: DNSClient = self.software_manager.software.get("dns-client") domain_exists = dns_client.check_domain_exists(target_domain=parsed_url.hostname) # if domain does not exist, the request fails diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index ddb30a3b..adc8d565 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -60,12 +60,12 @@ class SoftwareManager: @property def arp(self) -> "ARP": """Provides access to the ARP service instance, if installed.""" - return self.software.get("ARP") # noqa + return self.software.get("arp") # noqa @property def icmp(self) -> "ICMP": """Provides access to the ICMP service instance, if installed.""" - return self.software.get("ICMP") # noqa + return self.software.get("icmp") # noqa def get_open_ports(self) -> List[Port]: """ @@ -243,7 +243,7 @@ class SoftwareManager: :param session: The transport session the payload originates from. """ if payload.__class__.__name__ == "PortScanPayload": - self.software.get("NMAP").receive(payload=payload, session_id=session_id) + self.software.get("nmap").receive(payload=payload, session_id=session_id) return main_receiver = self.port_protocol_mapping.get((port, protocol), None) if main_receiver: diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 311f7e25..0946f985 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -15,7 +15,7 @@ from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import PORT_LOOKUP -class ARP(Service, discriminator="ARP"): +class ARP(Service, discriminator="arp"): """ The ARP (Address Resolution Protocol) Service. @@ -26,7 +26,7 @@ class ARP(Service, discriminator="ARP"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for ARP.""" - type: str = "ARP" + type: str = "arp" config: "ARP.ConfigSchema" = Field(default_factory=lambda: ARP.ConfigSchema()) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 369905db..0969370a 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class DatabaseService(Service, discriminator="DatabaseService"): +class DatabaseService(Service, discriminator="database-service"): """ A class for simulating a generic SQL Server service. @@ -29,11 +29,11 @@ class DatabaseService(Service, discriminator="DatabaseService"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DatabaseService.""" - type: str = "DatabaseService" + type: str = "database-service" backup_server_ip: Optional[IPv4Address] = None db_password: Optional[str] = None - config: "DatabaseService.ConfigSchema" = Field(default_factory=lambda: DatabaseService.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: DatabaseService.ConfigSchema()) backup_server_ip: IPv4Address = None """IP address of the backup server.""" @@ -45,7 +45,7 @@ class DatabaseService(Service, discriminator="DatabaseService"): """File name of latest backup.""" def __init__(self, **kwargs): - kwargs["name"] = "DatabaseService" + kwargs["name"] = "database-service" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -69,7 +69,7 @@ class DatabaseService(Service, discriminator="DatabaseService"): """ super().install() - if not self.parent.software_manager.software.get("FTPClient"): + if not self.parent.software_manager.software.get("ftp-client"): self.parent.sys_log.info(f"{self.name}: Installing FTPClient to enable database backups") self.parent.software_manager.install(FTPClient) @@ -93,7 +93,7 @@ class DatabaseService(Service, discriminator="DatabaseService"): return False software_manager: SoftwareManager = self.software_manager - ftp_client_service: FTPClient = software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = software_manager.software.get("ftp-client") if not ftp_client_service: self.sys_log.error( @@ -127,7 +127,7 @@ class DatabaseService(Service, discriminator="DatabaseService"): return False software_manager: SoftwareManager = self.software_manager - ftp_client_service: FTPClient = software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = software_manager.software.get("ftp-client") if not ftp_client_service: self.sys_log.error( diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 83a14033..4f065a0b 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -18,21 +18,21 @@ if TYPE_CHECKING: _LOGGER = getLogger(__name__) -class DNSClient(Service, discriminator="DNSClient"): +class DNSClient(Service, discriminator="dns-client"): """Represents a DNS Client as a Service.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DNSClient.""" - type: str = "DNSClient" + type: str = "dns-client" dns_server: Optional[IPV4Address] = None - config: "DNSClient.ConfigSchema" = Field(default_factory=lambda: DNSClient.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: DNSClient.ConfigSchema()) dns_cache: Dict[str, IPv4Address] = {} "A dict of known mappings between domain/URLs names and IPv4 addresses." def __init__(self, **kwargs): - kwargs["name"] = "DNSClient" + kwargs["name"] = "dns-client" kwargs["port"] = PORT_LOOKUP["DNS"] # DNS uses UDP by default # it switches to TCP when the bytes exceed 512 (or 4096) bytes diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index ef19a13e..696af993 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -14,22 +14,22 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class DNSServer(Service, discriminator="DNSServer"): +class DNSServer(Service, discriminator="dns-server"): """Represents a DNS Server as a Service.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DNSServer.""" - type: str = "DNSServer" + type: str = "dns-server" domain_mapping: dict = {} - config: "DNSServer.ConfigSchema" = Field(default_factory=lambda: DNSServer.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: DNSServer.ConfigSchema()) dns_table: Dict[str, IPv4Address] = {} "A dict of mappings between domain names and IPv4 addresses." def __init__(self, **kwargs): - kwargs["name"] = "DNSServer" + kwargs["name"] = "dns-server" kwargs["port"] = PORT_LOOKUP["DNS"] # DNS uses UDP by default # it switches to TCP when the bytes exceed 512 (or 4096) bytes diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 23b55330..5e97243a 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -18,7 +18,7 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class FTPClient(FTPServiceABC, discriminator="FTPClient"): +class FTPClient(FTPServiceABC, discriminator="ftp-client"): """ A class for simulating an FTP client service. @@ -26,15 +26,15 @@ class FTPClient(FTPServiceABC, discriminator="FTPClient"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPClient.ConfigSchema" = Field(default_factory=lambda: FTPClient.ConfigSchema()) - class ConfigSchema(Service.ConfigSchema): """ConfigSchema for FTPClient.""" - type: str = "FTPClient" + type: str = "ftp-client" + + config: ConfigSchema = Field(default_factory=lambda: FTPClient.ConfigSchema()) def __init__(self, **kwargs): - kwargs["name"] = "FTPClient" + kwargs["name"] = "ftp-client" kwargs["port"] = PORT_LOOKUP["FTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 43184684..0c451d12 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -12,7 +12,7 @@ from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class FTPServer(FTPServiceABC, discriminator="FTPServer"): +class FTPServer(FTPServiceABC, discriminator="ftp-server"): """ A class for simulating an FTP server service. @@ -20,17 +20,18 @@ class FTPServer(FTPServiceABC, discriminator="FTPServer"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPServer.ConfigSchema" = Field(default_factory=lambda: FTPServer.ConfigSchema()) server_password: Optional[str] = None class ConfigSchema(FTPServiceABC.ConfigSchema): """ConfigSchema for FTPServer.""" - type: str = "FTPServer" + type: str = "ftp-server" server_password: Optional[str] = None + config: ConfigSchema = Field(default_factory=lambda: FTPServer.ConfigSchema()) + def __init__(self, **kwargs): - kwargs["name"] = "FTPServer" + kwargs["name"] = "ftp-server" kwargs["port"] = PORT_LOOKUP["FTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/icmp/icmp.py b/src/primaite/simulator/system/services/icmp/icmp.py index 77dbd5be..207940cf 100644 --- a/src/primaite/simulator/system/services/icmp/icmp.py +++ b/src/primaite/simulator/system/services/icmp/icmp.py @@ -16,7 +16,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ICMP(Service, discriminator="ICMP"): +class ICMP(Service, discriminator="icmp"): """ The Internet Control Message Protocol (ICMP) service. @@ -27,14 +27,14 @@ class ICMP(Service, discriminator="ICMP"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for ICMP.""" - type: str = "ICMP" + type: str = "icmp" config: "ICMP.ConfigSchema" = Field(default_factory=lambda: ICMP.ConfigSchema()) request_replies: Dict = {} def __init__(self, **kwargs): - kwargs["name"] = "ICMP" + kwargs["name"] = "icmp" kwargs["port"] = PORT_LOOKUP["NONE"] kwargs["protocol"] = PROTOCOL_LOOKUP["ICMP"] super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/icmp/router_icmp.py b/src/primaite/simulator/system/services/icmp/router_icmp.py index 63fbd4b2..4c69e381 100644 --- a/src/primaite/simulator/system/services/icmp/router_icmp.py +++ b/src/primaite/simulator/system/services/icmp/router_icmp.py @@ -1,13 +1,13 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -# class RouterICMP(ICMP): +# class RouterICMP(icmp): # """ -# A class to represent a router's Internet Control Message Protocol (ICMP) handler. +# A class to represent a router's Internet Control Message Protocol (icmp) handler. # # :param sys_log: System log for logging network events and errors. # :type sys_log: SysLog -# :param arp_cache: The ARP cache for resolving MAC addresses. +# :param arp_cache: The arp cache for resolving MAC addresses. # :type arp_cache: ARPCache -# :param router: The router to which this ICMP handler belongs. +# :param router: The router to which this icmp handler belongs. # :type router: Router # """ # @@ -19,7 +19,7 @@ # # def process_icmp(self, frame: Frame, from_network_interface: NIC, is_reattempt: bool = False): # """ -# Process incoming ICMP frames based on ICMP type. +# Process incoming icmp frames based on icmp type. # # :param frame: The incoming frame to process. # :param from_network_interface: The network interface where the frame is coming from. @@ -36,13 +36,13 @@ # self.sys_log.info(f"Received echo request from {frame.ip.src_ip_address}") # target_mac_address = self.arp.get_arp_cache_mac_address(frame.ip.src_ip_address) # src_nic = self.arp.get_arp_cache_network_interface(frame.ip.src_ip_address) -# tcp_header = TCPHeader(src_port=Port["ARP"], dst_port=Port["ARP"]) +# tcp_header = TCPHeader(src_port=Port["arp"], dst_port=Port["arp"]) # # # Network Layer # ip_packet = IPPacket( # src_ip_address=network_interface.ip_address, # dst_ip_address=frame.ip.src_ip_address, -# protocol=IPProtocol["ICMP"], +# protocol=IPProtocol["icmp"], # ) # # Data Link Layer # ethernet_header = EthernetHeader( @@ -54,7 +54,7 @@ # identifier=frame.icmp.identifier, # sequence=frame.icmp.sequence + 1, # ) -# payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard ICMP 32 bytes size +# payload = secrets.token_urlsafe(int(32 / 1.3)) # Standard icmp 32 bytes size # frame = Frame( # ethernet=ethernet_header, # ip=ip_packet, diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index e3af43f7..30d8c258 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -15,23 +15,23 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class NTPClient(Service, discriminator="NTPClient"): +class NTPClient(Service, discriminator="ntp-client"): """Represents a NTP client as a service.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for NTPClient.""" - type: str = "NTPClient" + type: str = "ntp-client" ntp_server_ip: Optional[IPV4Address] = None - config: "NTPClient.ConfigSchema" = Field(default_factory=lambda: NTPClient.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: NTPClient.ConfigSchema()) ntp_server: Optional[IPv4Address] = None "The NTP server the client sends requests to." time: Optional[datetime] = None def __init__(self, **kwargs): - kwargs["name"] = "NTPClient" + kwargs["name"] = "ntp-client" kwargs["port"] = PORT_LOOKUP["NTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index b2d8356c..05696d9f 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -13,18 +13,18 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class NTPServer(Service, discriminator="NTPServer"): +class NTPServer(Service, discriminator="ntp-server"): """Represents a NTP server as a service.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for NTPServer.""" - type: str = "NTPServer" + type: str = "ntp-server" - config: "NTPServer.ConfigSchema" = Field(default_factory=lambda: NTPServer.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: NTPServer.ConfigSchema()) def __init__(self, **kwargs): - kwargs["name"] = "NTPServer" + kwargs["name"] = "ntp-server" kwargs["port"] = PORT_LOOKUP["NTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 01d9095b..2ce7d176 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -129,13 +129,13 @@ class RemoteTerminalConnection(TerminalClientConnection): return self.parent_terminal.send(payload=payload, session_id=self.ssh_session_id) -class Terminal(Service, discriminator="Terminal"): +class Terminal(Service, discriminator="terminal"): """Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for Terminal.""" - type: str = "Terminal" + type: str = "terminal" config: "Terminal.ConfigSchema" = Field(default_factory=lambda: Terminal.ConfigSchema()) @@ -143,7 +143,7 @@ class Terminal(Service, discriminator="Terminal"): """Dictionary of connect requests made to remote nodes.""" def __init__(self, **kwargs): - kwargs["name"] = "Terminal" + kwargs["name"] = "terminal" kwargs["port"] = PORT_LOOKUP["SSH"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -186,7 +186,7 @@ class Terminal(Service, discriminator="Terminal"): return RequestResponse(status="failure", data={}) rm.add_request( - "node_session_remote_login", + "node-session-remote-login", request_type=RequestType(func=_remote_login), ) diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 40a713a5..2eddefc1 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -21,15 +21,15 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) -class WebServer(Service, discriminator="WebServer"): +class WebServer(Service, discriminator="web-server"): """Class used to represent a Web Server Service in simulation.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for WebServer.""" - type: str = "WebServer" + type: str = "web-server" - config: "WebServer.ConfigSchema" = Field(default_factory=lambda: WebServer.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: WebServer.ConfigSchema()) response_codes_this_timestep: List[HttpStatusCode] = [] @@ -57,7 +57,7 @@ class WebServer(Service, discriminator="WebServer"): return super().pre_timestep(timestep) def __init__(self, **kwargs): - kwargs["name"] = "WebServer" + kwargs["name"] = "web-server" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] # default for web is port 80 if kwargs.get("port") is None: @@ -146,7 +146,7 @@ class WebServer(Service, discriminator="WebServer"): def _establish_db_connection(self) -> None: """Establish a connection to db.""" - db_client = self.software_manager.software.get("DatabaseClient") + db_client = self.software_manager.software.get("database-client") self.db_connection: DatabaseClientConnection = db_client.get_new_connection() def send( diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 3e57f579..9c117dfe 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -24,20 +24,20 @@ agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -77,7 +77,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -91,222 +91,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -318,8 +318,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -332,7 +332,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -345,7 +345,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -358,7 +358,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -371,7 +371,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -384,139 +384,139 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: ACTION_PENALTY + - type: action-penalty weight: 1.0 options: action_penalty: -0.75 @@ -585,7 +585,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -597,9 +597,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -611,10 +611,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -623,7 +623,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -643,20 +643,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -665,20 +665,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 9cf95a64..18b37466 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -12,37 +12,37 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) action_probabilities: 0: 1.0 - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1,] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -82,7 +82,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -96,7 +96,7 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} @@ -104,118 +104,118 @@ agents: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_checkhash" + action: "node-file-checkhash" options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_checkhash" + action: "node-folder-checkhash" options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 19: # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 20: - action: "node_startup" + action: "node-startup" options: node_name: client_1 21: - action: "node_reset" + action: "node-reset" options: node_name: client_1 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "router_acl_add_rule" + 22: # "acl: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -227,8 +227,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "router_acl_add_rule" + 23: # "acl: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -241,7 +241,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 24: # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -254,7 +254,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 25: # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -267,7 +267,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 26: - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -280,7 +280,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 27: - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -293,139 +293,139 @@ agents: src_wildcard: NONE dst_wildcard: NONE 28: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 29: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 30: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 31: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 32: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 33: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 34: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 35: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 36: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 37: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 38: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 39: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 40: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 41: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 42: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 43: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 44: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 45: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 46: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 47: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 48: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 49: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 50: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 51: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 52: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 53: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_hostname: database_server @@ -433,7 +433,7 @@ agents: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_hostname: web_server @@ -492,7 +492,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -504,9 +504,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -518,7 +518,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service - type: server hostname: backup_server @@ -527,7 +527,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - type: server hostname: security_suite @@ -547,14 +547,14 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.1 data_manipulation_p_of_success: 0.1 payload: "DELETE" server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - type: computer hostname: client_2 @@ -563,9 +563,9 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: router_1 diff --git a/tests/assets/configs/basic_c2_setup.yaml b/tests/assets/configs/basic_c2_setup.yaml index 0cae2ba0..ac2b026e 100644 --- a/tests/assets/configs/basic_c2_setup.yaml +++ b/tests/assets/configs/basic_c2_setup.yaml @@ -40,7 +40,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 applications: - - type: C2Server + - type: c2-server options: listen_on_ports: - 80 @@ -52,7 +52,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 applications: - - type: C2Beacon + - type: c2-beacon options: c2_server_ip_address: 192.168.10.21 keep_alive_frequency: 5 diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 3a62c75c..8108ecbe 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -26,18 +26,18 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser agent_settings: action_probabilities: diff --git a/tests/assets/configs/basic_node_with_software_listening_ports.yaml b/tests/assets/configs/basic_node_with_software_listening_ports.yaml index 53eee87f..7b04196b 100644 --- a/tests/assets/configs/basic_node_with_software_listening_ports.yaml +++ b/tests/assets/configs/basic_node_with_software_listening_ports.yaml @@ -26,13 +26,13 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 10.10.1.12 listen_on_ports: - 631 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://sometech.ai listen_on_ports: diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index b0591da6..c9ac5f8d 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -29,22 +29,22 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser reward_function: reward_components: - - type: DUMMY + - type: dummy agent_settings: action_probabilities: @@ -53,13 +53,13 @@ agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: @@ -96,25 +96,25 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: - switch_1:eth-1<->client_1:eth-1 - switch_1:eth-2<->client_2:eth-1 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_hostname: database_server @@ -122,7 +122,7 @@ agents: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_hostname: web_server @@ -147,41 +147,41 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: RansomwareScript - - type: WebBrowser + - type: ransomware-script + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.10 server_password: arcd - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.21 server_password: arcd - - type: DoSBot + - type: dos-bot options: target_ip_address: 192.168.10.21 payload: SPOOF DATA port_scan_p_of_success: 0.8 services: - - type: DNSClient - - type: DNSServer + - type: dns-client + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.10 - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.10 - - type: WebServer - - type: FTPServer - - type: NTPClient + - type: web-server + - type: ftp-server + - type: ntp-client options: ntp_server_ip: 192.168.1.10 - - type: NTPServer + - type: ntp-server - hostname: client_2 type: computer ip_address: 192.168.10.22 diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index 726c9ab0..e9464f0f 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -24,7 +24,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -34,33 +34,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -70,26 +70,26 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 @@ -100,30 +100,30 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -168,7 +168,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -182,222 +182,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -409,8 +409,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -423,7 +423,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -436,7 +436,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -449,7 +449,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -462,7 +462,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -475,151 +475,151 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -689,7 +689,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -701,9 +701,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -715,10 +715,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -727,7 +727,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -747,20 +747,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -769,20 +769,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: router_1 diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index b4d018c8..2aa472d8 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -51,18 +51,18 @@ game: agents: - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser agent_settings: action_probabilities: @@ -223,7 +223,7 @@ simulation: start_up_duration: 0 shut_down_duration: 0 services: - - type: DNSServer + - type: dns-server links: - endpoint_a_hostname: client_1 endpoint_a_port: 1 diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 41b7fce9..3403a7ff 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -12,17 +12,17 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} reward_function: reward_components: - - type: DUMMY + - type: dummy agent_settings: # options specific to this particular agent type, basically args of __init__(self) action_probabilities: @@ -30,47 +30,47 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: DataManipulationBot + application_name: data-manipulation-bot reward_function: reward_components: - - type: DUMMY + - type: dummy agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1,] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -110,7 +110,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -124,7 +124,7 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} @@ -132,118 +132,118 @@ agents: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_checkhash" + action: "node-file-checkhash" options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_checkhash" + action: "node-folder-checkhash" options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 19: # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 20: - action: "node_startup" + action: "node-startup" options: node_name: client_1 21: - action: "node_reset" + action: "node-reset" options: node_name: client_1 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "router_acl_add_rule" + 22: # "acl: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -255,8 +255,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "router_acl_add_rule" + 23: # "acl: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -269,7 +269,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 24: # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -282,7 +282,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 25: # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -295,7 +295,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 26: - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -308,7 +308,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 27: - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -321,139 +321,139 @@ agents: src_wildcard: NONE dst_wildcard: NONE 28: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 29: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 30: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 31: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 32: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 33: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 34: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 35: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 36: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 37: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 38: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 39: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 40: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 41: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 42: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 43: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 44: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 45: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 46: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 47: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 48: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 49: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 50: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 51: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 52: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 53: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_hostname: database_server @@ -461,7 +461,7 @@ agents: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_hostname: web_server @@ -520,7 +520,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -532,9 +532,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -546,7 +546,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service - type: server hostname: backup_server @@ -555,7 +555,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - type: server hostname: security_suite @@ -575,14 +575,14 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.1 data_manipulation_p_of_success: 0.1 payload: "DELETE" server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - type: computer hostname: client_2 @@ -591,9 +591,9 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: router_1 diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index fcfc93ef..8bf7d506 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -24,7 +24,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -34,33 +34,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -70,26 +70,26 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 @@ -100,31 +100,31 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -169,7 +169,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -183,222 +183,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_checkhash" + action: "node-file-checkhash" options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_checkhash" + action: "node-folder-checkhash" options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -410,8 +410,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -424,7 +424,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -437,7 +437,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -450,7 +450,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -463,7 +463,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -476,132 +476,132 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 @@ -610,19 +610,19 @@ agents: reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -692,7 +692,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -704,9 +704,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -718,10 +718,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -730,7 +730,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -750,27 +750,27 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: ExtendedApplication + - type: extended-application options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient - - type: DatabaseService + - type: dns-client + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: ExtendedService + - type: extended-service - hostname: client_2 type: computer @@ -779,20 +779,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: router_1 diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index 4b11dbcc..eb39aa1a 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -51,13 +51,13 @@ game: agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: @@ -85,24 +85,24 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: - client_1:eth-1<->switch_1:eth-1 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: firewall_acl_add_rule + action: firewall-acl-add-rule options: - type: firewall_acl_add_rule + type: firewall-acl-add-rule target_firewall_nodename: firewall firewall_port_name: internal firewall_port_direction: inbound @@ -116,14 +116,14 @@ agents: src_wildcard: NONE dst_wildcard: NONE 2: - action: firewall_acl_remove_rule + action: firewall-acl-remove-rule options: target_firewall_nodename: firewall firewall_port_name: internal firewall_port_direction: inbound position: 1 3: - action: firewall_acl_add_rule + action: firewall-acl-add-rule options: target_firewall_nodename: firewall firewall_port_name: internal @@ -134,18 +134,18 @@ agents: dst_ip: ALL src_port: ARP dst_port: DNS - protocol_name: ICMP + protocol_name: icmp src_wildcard: NONE dst_wildcard: NONE 4: - action: firewall_acl_remove_rule + action: firewall-acl-remove-rule options: target_firewall_nodename: firewall firewall_port_name: internal firewall_port_direction: outbound position: 1 5: - action: firewall_acl_add_rule + action: firewall-acl-add-rule options: target_firewall_nodename: firewall firewall_port_name: dmz @@ -160,14 +160,14 @@ agents: src_wildcard: NONE dst_wildcard: NONE 6: - action: firewall_acl_remove_rule + action: firewall-acl-remove-rule options: target_firewall_nodename: firewall firewall_port_name: dmz firewall_port_direction: inbound position: 1 7: - action: firewall_acl_add_rule + action: firewall-acl-add-rule options: target_firewall_nodename: firewall firewall_port_name: dmz @@ -182,14 +182,14 @@ agents: src_wildcard: NONE dst_wildcard: NONE 8: - action: firewall_acl_remove_rule + action: firewall-acl-remove-rule options: target_firewall_nodename: firewall firewall_port_name: dmz firewall_port_direction: outbound position: 2 9: - action: firewall_acl_add_rule + action: firewall-acl-add-rule options: target_firewall_nodename: firewall firewall_port_name: external @@ -200,18 +200,18 @@ agents: dst_ip: 192.168.10.10 # dmz src_port: POSTGRES_SERVER dst_port: POSTGRES_SERVER - protocol_name: ICMP + protocol_name: icmp src_wildcard: NONE dst_wildcard: NONE 10: - action: firewall_acl_remove_rule + action: firewall-acl-remove-rule options: target_firewall_nodename: firewall firewall_port_name: external firewall_port_direction: inbound position: 10 11: - action: firewall_acl_add_rule + action: firewall-acl-add-rule options: target_firewall_nodename: firewall firewall_port_name: external @@ -220,28 +220,28 @@ agents: permission: DENY src_ip: 192.168.20.10 # external_computer dst_ip: 192.168.0.10 # client_1 - src_port: NONE - dst_port: NONE - protocol_name: none + src_port: ALL + dst_port: ALL + protocol_name: NONE src_wildcard: NONE dst_wildcard: NONE 12: - action: firewall_acl_remove_rule + action: firewall-acl-remove-rule options: target_firewall_nodename: firewall firewall_port_name: external firewall_port_direction: outbound position: 1 13: - action: network_port_disable + action: network-port-disable options: - type: network_port_disable + type: network-port-disable target_nodename: firewall port_num: 3 14: - action: network_port_enable + action: network-port-enable options: - type: network_port_enable + type: network-port-enable target_nodename: firewall port_num: 3 @@ -401,7 +401,7 @@ simulation: start_up_duration: 0 shut_down_duration: 0 services: - - type: DNSServer + - type: dns-server links: - endpoint_a_hostname: client_1 endpoint_a_port: 1 diff --git a/tests/assets/configs/fixing_duration_one_item.yaml b/tests/assets/configs/fixing_duration_one_item.yaml index da5a9993..ff3a6504 100644 --- a/tests/assets/configs/fixing_duration_one_item.yaml +++ b/tests/assets/configs/fixing_duration_one_item.yaml @@ -26,18 +26,18 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser agent_settings: action_probabilities: 0: 0.4 @@ -46,13 +46,13 @@ agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: @@ -89,25 +89,25 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: - switch_1:eth-1<->client_1:eth-1 - switch_1:eth-2<->client_2:eth-1 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_hostname: database_server @@ -115,7 +115,7 @@ agents: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_hostname: web_server @@ -140,46 +140,46 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: RansomwareScript - - type: WebBrowser + - type: ransomware-script + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.10 server_password: arcd fixing_duration: 1 - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.21 server_password: arcd - - type: DoSBot + - type: dos-bot options: target_ip_address: 192.168.10.21 payload: SPOOF DATA port_scan_p_of_success: 0.8 services: - - type: DNSClient - - type: DNSServer + - type: dns-client + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.10 - - type: DatabaseService + - type: database-service options: fixing_duration: 5 backup_server_ip: 192.168.1.10 - - type: WebServer - - type: FTPClient - - type: FTPServer + - type: web-server + - type: ftp-client + - type: ftp-server options: server_password: arcd - - type: NTPClient + - type: ntp-client options: ntp_server_ip: 192.168.1.10 - - type: NTPServer + - type: ntp-server - hostname: client_2 type: computer ip_address: 192.168.10.22 @@ -187,12 +187,12 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.10 server_password: arcd services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: switch_1 diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index 2baca409..6b80519c 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -16,48 +16,48 @@ game: agents: - ref: agent_1 team: BLUE - type: ProxyAgent + type: proxy-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_install + action: node-application-install options: node_name: client_1 - application_name: DatabaseClient + application_name: database-client 2: - action: node_application_install + action: node-application-install options: node_name: client_2 - application_name: RansomwareScript + application_name: ransomware-script 3: - action: node_application_install + action: node-application-install options: node_name: client_3 - application_name: DoSBot + application_name: dos-bot 4: - action: configure_database_client + action: configure-database-client options: node_name: client_1 server_ip_address: 10.0.0.5 5: - action: configure_database_client + action: configure-database-client options: node_name: client_1 server_password: correct_password 6: - action: configure_ransomware_script + action: configure-ransomware-script options: node_name: client_2 server_ip_address: 10.0.0.5 server_password: correct_password payload: ENCRYPT 7: - action: configure_dos_bot + action: configure-dos-bot options: node_name: client_3 target_ip_address: 10.0.0.5 @@ -68,10 +68,10 @@ agents: dos_intensity: 1.0 max_sessions: 1000 8: - action: node_application_install + action: node-application-install options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client agent_settings: flatten_obs: True action_masking: False @@ -103,7 +103,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 10.0.0.1 services: - - type: DatabaseService + - type: database-service options: db_password: correct_password links: diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 93baf4af..e99ea49b 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -20,7 +20,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -30,33 +30,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -66,26 +66,26 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 @@ -96,31 +96,31 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender_1 team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -160,7 +160,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -174,222 +174,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -401,8 +401,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -415,7 +415,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -428,7 +428,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -441,7 +441,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -454,7 +454,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -467,149 +467,149 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -621,20 +621,20 @@ agents: - ref: defender_2 team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -674,7 +674,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -688,222 +688,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -915,8 +915,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -929,7 +929,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -942,7 +942,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -955,7 +955,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -968,7 +968,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -981,132 +981,132 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 @@ -1114,17 +1114,17 @@ agents: reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -1194,7 +1194,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -1206,9 +1206,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -1220,10 +1220,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -1232,7 +1232,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -1252,20 +1252,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -1274,20 +1274,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client diff --git a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml index c2f79144..9b49d466 100644 --- a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml +++ b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml @@ -21,12 +21,12 @@ game: agents: - ref: client_1_red_nmap team: RED - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: node_network_service_recon + action: node-network-service-recon options: source_node: client_1 target_ip_address: 192.168.10.0/24 diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index f6d549e8..e3cb33e1 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -21,12 +21,12 @@ game: agents: - ref: client_1_red_nmap team: RED - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: node_nmap_ping_scan + action: node-nmap-ping-scan options: source_node: client_1 target_ip_address: 192.168.1.0/24 diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 873401b9..2692a3bc 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -21,12 +21,12 @@ game: agents: - ref: client_1_red_nmap team: RED - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: node_nmap_port_scan + action: node-nmap-port-scan options: source_node: client_1 target_ip_address: 192.168.10.0/24 diff --git a/tests/assets/configs/scenario_with_placeholders/greens_1.yaml b/tests/assets/configs/scenario_with_placeholders/greens_1.yaml index 677cd5a5..3f9b65f4 100644 --- a/tests/assets/configs/scenario_with_placeholders/greens_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/greens_1.yaml @@ -1,7 +1,7 @@ agents: &greens - ref: green_A team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.2 @@ -10,17 +10,17 @@ agents: &greens action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 1.0 options: node_hostname: client diff --git a/tests/assets/configs/scenario_with_placeholders/greens_2.yaml b/tests/assets/configs/scenario_with_placeholders/greens_2.yaml index eb7823f8..77a689e7 100644 --- a/tests/assets/configs/scenario_with_placeholders/greens_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/greens_2.yaml @@ -1,7 +1,7 @@ agents: &greens - ref: green_B team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.95 @@ -10,17 +10,17 @@ agents: &greens action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 1.0 options: node_hostname: client diff --git a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml index 0170143f..b95955b4 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_1.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_1.yaml @@ -1,11 +1,11 @@ reds: &reds - ref: red_A team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: possible_start_nodes: [client,] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 10 frequency: 10 variance: 0 diff --git a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml index e14eaa43..a4a7550a 100644 --- a/tests/assets/configs/scenario_with_placeholders/reds_2.yaml +++ b/tests/assets/configs/scenario_with_placeholders/reds_2.yaml @@ -1,10 +1,10 @@ reds: &reds - ref: red_B team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: possible_start_nodes: [client_1,] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 3 frequency: 2 variance: 1 diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index 7ea0145a..ab8c968c 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -26,12 +26,12 @@ agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: routers: [] @@ -46,7 +46,7 @@ agents: include_num_access: false include_nmne: false - - type: LINKS + - type: links label: LINKS options: link_references: @@ -56,48 +56,48 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_shutdown + action: node-shutdown options: node_name: client 2: - action: node_shutdown + action: node-shutdown options: node_name: server 3: - action: node_startup + action: node-startup options: node_name: client 4: - action: node_startup + action: node-startup options: node_name: server 5: - action: host_nic_disable + action: host-nic-disable options: node_name: client nic_num: 1 6: - action: host_nic_disable + action: host-nic-disable options: node_name: server nic_num: 1 7: - action: host_nic_enable + action: host-nic-enable options: node_name: client nic_num: 1 8: - action: host_nic_enable + action: host-nic-enable options: node_name: server nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server @@ -117,10 +117,10 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.3 - - type: DataManipulationBot + - type: data-manipulation-bot options: server_ip: 192.168.1.3 payload: "DELETE" @@ -135,7 +135,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DatabaseService + - type: database-service links: - endpoint_a_hostname: client diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index d5615a72..07256f01 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -23,7 +23,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -33,33 +33,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -69,57 +69,57 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -159,7 +159,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -173,222 +173,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_checkhash" + action: "node-file-checkhash" options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_checkhash" + action: "node-folder-checkhash" options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -400,8 +400,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -414,7 +414,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -427,7 +427,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -440,7 +440,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -453,7 +453,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -466,143 +466,143 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -673,7 +673,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -685,9 +685,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -699,10 +699,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -711,7 +711,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -731,20 +731,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -753,20 +753,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client diff --git a/tests/assets/configs/software_fixing_duration.yaml b/tests/assets/configs/software_fixing_duration.yaml index f685b420..63409f2b 100644 --- a/tests/assets/configs/software_fixing_duration.yaml +++ b/tests/assets/configs/software_fixing_duration.yaml @@ -26,18 +26,18 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser agent_settings: action_probabilities: 0: 0.4 @@ -46,13 +46,13 @@ agents: - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: @@ -89,25 +89,25 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: - switch_1:eth-1<->client_1:eth-1 - switch_1:eth-2<->client_2:eth-1 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_hostname: database_server @@ -115,7 +115,7 @@ agents: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_hostname: web_server @@ -140,22 +140,22 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: NMAP + - type: nmap options: fixing_duration: 1 - - type: RansomwareScript + - type: ransomware-script options: fixing_duration: 1 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ fixing_duration: 1 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.10 server_password: arcd fixing_duration: 1 - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 @@ -163,41 +163,41 @@ simulation: server_ip: 192.168.1.21 server_password: arcd fixing_duration: 1 - - type: DoSBot + - type: dos-bot options: target_ip_address: 192.168.10.21 payload: SPOOF DATA port_scan_p_of_success: 0.8 fixing_duration: 1 services: - - type: DNSClient + - type: dns-client options: dns_server: 192.168.1.10 fixing_duration: 3 - - type: DNSServer + - type: dns-server options: fixing_duration: 3 domain_mapping: arcd.com: 192.168.1.10 - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.10 fixing_duration: 3 - - type: WebServer + - type: web-server options: fixing_duration: 3 - - type: FTPClient + - type: ftp-client options: fixing_duration: 3 - - type: FTPServer + - type: ftp-server options: server_password: arcd fixing_duration: 3 - - type: NTPClient + - type: ntp-client options: ntp_server_ip: 192.168.1.10 fixing_duration: 3 - - type: NTPServer + - type: ntp-server options: fixing_duration: 3 - hostname: client_2 @@ -207,12 +207,12 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.10 server_password: arcd services: - - type: DNSClient + - type: dns-client links: - endpoint_a_hostname: switch_1 diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index 25bc38e6..0266dd00 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -23,7 +23,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -33,33 +33,33 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_2 - application_name: DatabaseClient + application_name: database-client reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_2 - ref: client_1_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent agent_settings: action_probabilities: 0: 0.3 @@ -69,26 +69,26 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser 2: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: WebBrowser + application_name: web-browser reward_function: reward_components: - - type: WEBPAGE_UNAVAILABLE_PENALTY + - type: webpage-unavailable-penalty weight: 0.25 options: node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + - type: green-admin-database-unreachable-penalty weight: 0.05 options: node_hostname: client_1 @@ -99,31 +99,31 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1, client_2] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -163,7 +163,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -177,222 +177,222 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-file-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. + action: "node-folder-scan" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context. options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: domain_controller 19: - action: "node_shutdown" + action: "node-shutdown" options: node_name: domain_controller 20: - action: node_startup + action: node-startup options: node_name: domain_controller 21: - action: node_reset + action: node-reset options: node_name: domain_controller 22: - action: "node_os_scan" + action: "node-os-scan" options: node_name: web_server 23: - action: "node_shutdown" + action: "node-shutdown" options: node_name: web_server 24: - action: node_startup + action: node-startup options: node_name: web_server 25: - action: node_reset + action: node-reset options: node_name: web_server 26: # old action num: 18 - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 27: - action: "node_shutdown" + action: "node-shutdown" options: node_name: database_server 28: - action: node_startup + action: node-startup options: node_name: database_server 29: - action: node_reset + action: node-reset options: node_name: database_server 30: - action: "node_os_scan" + action: "node-os-scan" options: node_name: backup_server 31: - action: "node_shutdown" + action: "node-shutdown" options: node_name: backup_server 32: - action: node_startup + action: node-startup options: node_name: backup_server 33: - action: node_reset + action: node-reset options: node_name: backup_server 34: - action: "node_os_scan" + action: "node-os-scan" options: node_name: security_suite 35: - action: "node_shutdown" + action: "node-shutdown" options: node_name: security_suite 36: - action: node_startup + action: node-startup options: node_name: security_suite 37: - action: node_reset + action: node-reset options: node_name: security_suite 38: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_1 39: # old action num: 19 # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 40: # old action num: 20 - action: node_startup + action: node-startup options: node_name: client_1 41: # old action num: 21 - action: node_reset + action: node-reset options: node_name: client_1 42: - action: "node_os_scan" + action: "node-os-scan" options: node_name: client_2 43: - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_2 44: - action: node_startup + action: node-startup options: node_name: client_2 45: - action: node_reset + action: node-reset options: node_name: client_2 - 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" - action: "router_acl_add_rule" + 46: # old action num: 22 # "acl: ADDRULE - Block outgoing traffic from client 1" + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -404,8 +404,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" - action: "router_acl_add_rule" + 47: # old action num: 23 # "acl: ADDRULE - Block outgoing traffic from client 2" + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -418,7 +418,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 48: # old action num: 24 # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -431,7 +431,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 49: # old action num: 25 # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -444,7 +444,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 50: # old action num: 26 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -457,7 +457,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 51: # old action num: 27 - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -470,157 +470,157 @@ agents: src_wildcard: NONE dst_wildcard: NONE 52: # old action num: 28 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 53: # old action num: 29 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 54: # old action num: 30 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 55: # old action num: 31 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 56: # old action num: 32 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 57: # old action num: 33 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 58: # old action num: 34 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 59: # old action num: 35 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 60: # old action num: 36 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 61: # old action num: 37 - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 62: # old action num: 38 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 63: # old action num: 39 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 64: # old action num: 40 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 65: # old action num: 41 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 66: # old action num: 42 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 67: # old action num: 43 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 68: # old action num: 44 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 69: # old action num: 45 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 70: # old action num: 46 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 71: # old action num: 47 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 72: # old action num: 48 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 73: # old action num: 49 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 74: # old action num: 50 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 75: # old action num: 51 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 76: # old action num: 52 - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 77: # old action num: 53 - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 78: - action: node_application_install + action: node-application-install options: node_name: domain_controller - application_name: DoSBot + application_name: dos-bot 79: - action: node_application_remove + action: node-application-remove options: node_name: domain_controller - application_name: DoSBot + application_name: dos-bot 80: - action: node_application_remove + action: node-application-remove options: node_name: domain_controller - application_name: WebBrowser + application_name: web-browser 81: - action: node_application_execute + action: node-application-execute options: node_name: domain_controller - application_name: DoSBot + application_name: dos-bot 82: - action: configure_dos_bot + action: configure-dos-bot options: node_name: domain_controller target_ip_address: 192.168.1.14 @@ -628,17 +628,17 @@ agents: reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_1_green_user - - type: SHARED_REWARD + - type: shared-reward weight: 1.0 options: agent_name: client_2_green_user @@ -709,7 +709,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -721,9 +721,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -735,10 +735,10 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 - - type: FTPClient + - type: ftp-client - hostname: backup_server type: server @@ -747,7 +747,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - hostname: security_suite type: server @@ -767,20 +767,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - hostname: client_2 type: computer @@ -789,20 +789,20 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser options: target_url: http://arcd.com/users/ - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.8 data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 2d124981..65d16a47 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -20,12 +20,12 @@ game: agents: - ref: client_2_green_user team: GREEN - type: ProbabilisticAgent + type: probabilistic-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} agent_settings: # options specific to this particular agent type, basically args of __init__(self) @@ -34,44 +34,44 @@ agents: - ref: data_manipulation_attacker team: RED - type: RedDatabaseCorruptingAgent + type: red-database-corrupting-agent action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_1 - application_name: DataManipulationBot + application_name: data-manipulation-bot agent_settings: # options specific to this particular agent type, basically args of __init__(self) possible_start_nodes: [client_1,] - target_application: DataManipulationBot + target_application: data-manipulation-bot start_step: 25 frequency: 20 variance: 5 - ref: defender team: BLUE - type: ProxyAgent + type: proxy-agent observation_space: - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -111,7 +111,7 @@ agents: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -125,125 +125,125 @@ agents: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {} action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} # scan webapp service 1: - action: node_service_scan + action: node-service-scan options: node_name: web_server - service_name: WebServer + service_name: web-server # stop webapp service 2: - action: node_service_stop + action: node-service-stop options: node_name: web_server - service_name: WebServer + service_name: web-server # start webapp service 3: - action: "node_service_start" + action: "node-service-start" options: node_name: web_server - service_name: WebServer + service_name: web-server 4: - action: "node_service_pause" + action: "node-service-pause" options: node_name: web_server - service_name: WebServer + service_name: web-server 5: - action: "node_service_resume" + action: "node-service-resume" options: node_name: web_server - service_name: WebServer + service_name: web-server 6: - action: "node_service_restart" + action: "node-service-restart" options: node_name: web_server - service_name: WebServer + service_name: web-server 7: - action: "node_service_disable" + action: "node-service-disable" options: node_name: web_server - service_name: WebServer + service_name: web-server 8: - action: "node_service_enable" + action: "node-service-enable" options: node_name: web_server - service_name: WebServer + service_name: web-server 9: # check database.db file - action: "node_file_scan" + action: "node-file-scan" options: node_name: database_server folder_name: database file_name: database.db 10: - action: "node_file_checkhash" + action: "node-file-checkhash" options: node_name: database_server folder_name: database file_name: database.db 11: - action: "node_file_delete" + action: "node-file-delete" options: node_name: database_server folder_name: database file_name: database.db 12: - action: "node_file_repair" + action: "node-file-repair" options: node_name: database_server folder_name: database file_name: database.db 13: - action: "node_service_fix" + action: "node-service-fix" options: node_name: database_server - service_name: DatabaseService + service_name: database-service 14: - action: "node_folder_scan" + action: "node-folder-scan" options: node_name: database_server folder_name: database 15: - action: "node_folder_checkhash" + action: "node-folder-checkhash" options: node_name: database_server folder_name: database 16: - action: "node_folder_repair" + action: "node-folder-repair" options: node_name: database_server folder_name: database 17: - action: "node_folder_restore" + action: "node-folder-restore" options: node_name: database_server folder_name: database 18: - action: "node_os_scan" + action: "node-os-scan" options: node_name: database_server 19: # shutdown client 1 - action: "node_shutdown" + action: "node-shutdown" options: node_name: client_1 20: - action: "node_startup" + action: "node-startup" options: node_name: client_1 21: - action: "node_reset" + action: "node-reset" options: node_name: client_1 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) - action: "router_acl_add_rule" + 22: # "acl: ADDRULE - Block outgoing traffic from client 1" (not supported in Primaite) + action: "router-acl-add-rule" options: target_router: router_1 position: 1 @@ -255,8 +255,8 @@ agents: protocol_name: ALL src_wildcard: NONE dst_wildcard: NONE - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) - action: "router_acl_add_rule" + 23: # "acl: ADDRULE - Block outgoing traffic from client 2" (not supported in Primaite) + action: "router-acl-add-rule" options: target_router: router_1 position: 2 @@ -269,7 +269,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 24: # block tcp traffic from client 1 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 3 @@ -282,7 +282,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 25: # block tcp traffic from client 2 to web app - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 4 @@ -295,7 +295,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 26: - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 5 @@ -308,7 +308,7 @@ agents: src_wildcard: NONE dst_wildcard: NONE 27: - action: "router_acl_add_rule" + action: "router-acl-add-rule" options: target_router: router_1 position: 6 @@ -321,139 +321,139 @@ agents: src_wildcard: NONE dst_wildcard: NONE 28: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 0 29: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 1 30: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 2 31: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 3 32: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 4 33: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 5 34: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 6 35: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 7 36: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 8 37: - action: "router_acl_remove_rule" + action: "router-acl-remove-rule" options: target_router: router_1 position: 9 38: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: domain_controller nic_num: 1 39: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: domain_controller nic_num: 1 40: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: web_server nic_num: 1 41: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: web_server nic_num: 1 42: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: database_server nic_num: 1 43: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: database_server nic_num: 1 44: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: backup_server nic_num: 1 45: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: backup_server nic_num: 1 46: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 1 47: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 1 48: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: security_suite nic_num: 2 49: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: security_suite nic_num: 2 50: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_1 nic_num: 1 51: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_1 nic_num: 1 52: - action: "host_nic_disable" + action: "host-nic-disable" options: node_name: client_2 nic_num: 1 53: - action: "host_nic_enable" + action: "host-nic-enable" options: node_name: client_2 nic_num: 1 reward_function: reward_components: - - type: DATABASE_FILE_INTEGRITY + - type: database-file-integrity weight: 0.5 options: node_hostname: database_server @@ -461,7 +461,7 @@ agents: file_name: database.db - - type: WEB_SERVER_404_PENALTY + - type: web-server-404-penalty weight: 0.5 options: node_hostname: web_server @@ -521,7 +521,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 services: - - type: DNSServer + - type: dns-server options: domain_mapping: arcd.com: 192.168.1.12 # web server @@ -533,9 +533,9 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: WebServer + - type: web-server applications: - - type: DatabaseClient + - type: database-client options: db_server_ip: 192.168.1.14 @@ -547,7 +547,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: DatabaseService + - type: database-service options: backup_server_ip: 192.168.1.16 @@ -558,7 +558,7 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: - - type: FTPServer + - type: ftp-server - type: server hostname: security_suite @@ -578,14 +578,14 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: DataManipulationBot + - type: data-manipulation-bot options: port_scan_p_of_success: 0.1 data_manipulation_p_of_success: 0.1 payload: "DELETE" server_ip: 192.168.1.14 services: - - type: DNSClient + - type: dns-client - type: computer hostname: client_2 @@ -594,16 +594,16 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 applications: - - type: WebBrowser + - type: web-browser services: - - type: DNSClient + - type: dns-client - type: printer hostname: HP_LaserJet_Pro_4102fdn_printer ip_address: 192.168.10.99 subnet_mask: 255.255.255.0 - - type: wireless_router + - type: wireless-router hostname: router_2 router_interface: ip_address: 192.169.1.1 diff --git a/tests/assets/configs/wireless_wan_network_config.yaml b/tests/assets/configs/wireless_wan_network_config.yaml index c8f61bad..40a9126c 100644 --- a/tests/assets/configs/wireless_wan_network_config.yaml +++ b/tests/assets/configs/wireless_wan_network_config.yaml @@ -24,7 +24,7 @@ simulation: default_gateway: 192.168.2.1 start_up_duration: 0 - - type: wireless_router + - type: wireless-router hostname: router_1 start_up_duration: 0 @@ -45,7 +45,7 @@ simulation: next_hop_ip_address: 192.168.1.2 metric: 0 - - type: wireless_router + - type: wireless-router hostname: router_2 start_up_duration: 0 diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml index a327b0f5..13ceee16 100644 --- a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml @@ -28,7 +28,7 @@ simulation: default_gateway: 192.168.2.1 start_up_duration: 0 - - type: wireless_router + - type: wireless-router hostname: router_1 start_up_duration: 0 @@ -49,7 +49,7 @@ simulation: next_hop_ip_address: 192.168.1.2 metric: 0 - - type: wireless_router + - type: wireless-router hostname: router_2 start_up_duration: 0 diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml index ff048c92..6c52fd95 100644 --- a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml @@ -28,7 +28,7 @@ simulation: default_gateway: 192.168.2.1 start_up_duration: 0 - - type: wireless_router + - type: wireless-router hostname: router_1 start_up_duration: 0 @@ -49,7 +49,7 @@ simulation: next_hop_ip_address: 192.168.1.2 metric: 0 - - type: wireless_router + - type: wireless-router hostname: router_2 start_up_duration: 0 diff --git a/tests/conftest.py b/tests/conftest.py index 70443042..230a763d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,21 +39,21 @@ ACTION_SPACE_NODE_ACTION_VALUES = 1 _LOGGER = getLogger(__name__) -class DummyService(Service, discriminator="DummyService"): +class DummyService(Service, discriminator="dummy-service"): """Test Service class""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for DummyService.""" - type: str = "DummyService" + type: str = "dummy-service" - config: "DummyService.ConfigSchema" = Field(default_factory=lambda: DummyService.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: DummyService.ConfigSchema()) def describe_state(self) -> Dict: return super().describe_state() def __init__(self, **kwargs): - kwargs["name"] = "DummyService" + kwargs["name"] = "dummy-service" kwargs["port"] = PORT_LOOKUP["HTTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -62,18 +62,18 @@ class DummyService(Service, discriminator="DummyService"): pass -class DummyApplication(Application, discriminator="DummyApplication"): +class DummyApplication(Application, discriminator="dummy-application"): """Test Application class""" class ConfigSchema(Application.ConfigSchema): """ConfigSchema for DummyApplication.""" - type: str = "DummyApplication" + type: str = "dummy-application" - config: "DummyApplication.ConfigSchema" = Field(default_factory=lambda: DummyApplication.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: DummyApplication.ConfigSchema()) def __init__(self, **kwargs): - kwargs["name"] = "DummyApplication" + kwargs["name"] = "dummy-application" kwargs["port"] = PORT_LOOKUP["HTTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -93,7 +93,7 @@ def uc2_network() -> Network: @pytest.fixture(scope="function") def service(file_system) -> DummyService: return DummyService( - name="DummyService", port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_service") + name="dummy-service", port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_service") ) @@ -105,7 +105,7 @@ def service_class(): @pytest.fixture(scope="function") def application(file_system) -> DummyApplication: return DummyApplication( - name="DummyApplication", + name="dummy-application", port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="dummy_application"), @@ -280,16 +280,17 @@ def example_network() -> Network: return network -class ControlledAgent(AbstractAgent, discriminator="ControlledAgent"): +class ControlledAgent(AbstractAgent, discriminator="controlled-agent"): """Agent that can be controlled by the tests.""" - config: "ControlledAgent.ConfigSchema" = Field(default_factory=lambda: ControlledAgent.ConfigSchema()) most_recent_action: Optional[Tuple[str, Dict]] = None class ConfigSchema(AbstractAgent.ConfigSchema): """Configuration Schema for Abstract Agent used in tests.""" - type: str = "ControlledAgent" + type: str = "controlled-agent" + + config: ConfigSchema = Field(default_factory=lambda: ControlledAgent.ConfigSchema()) def get_action(self, obs: None, timestep: int = 0) -> Tuple[str, Dict]: """Return the agent's most recent action, formatted in CAOS format.""" @@ -358,7 +359,7 @@ def install_stuff_to_sim(sim: Simulation): server_2.power_on() network.connect(endpoint_a=server_2.network_interface[1], endpoint_b=switch_2.network_interface[2]) - # 2: Configure base ACL + # 2: Configure base acl router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22) router.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["DNS"], dst_port=PORT_LOOKUP["DNS"], position=1) @@ -366,17 +367,17 @@ def install_stuff_to_sim(sim: Simulation): # 3: Install server software server_1.software_manager.install(DNSServer) - dns_service: DNSServer = server_1.software_manager.software.get("DNSServer") # noqa + dns_service: DNSServer = server_1.software_manager.software.get("dns-server") # noqa dns_service.dns_register("www.example.com", server_2.network_interface[1].ip_address) server_2.software_manager.install(WebServer) # 3.1: Ensure that the dns clients are configured correctly - client_1.software_manager.software.get("DNSClient").dns_server = server_1.network_interface[1].ip_address - server_2.software_manager.software.get("DNSClient").dns_server = server_1.network_interface[1].ip_address + client_1.software_manager.software.get("dns-client").dns_server = server_1.network_interface[1].ip_address + server_2.software_manager.software.get("dns-client").dns_server = server_1.network_interface[1].ip_address # 4: Check that client came pre-installed with web browser and dns client - assert isinstance(client_1.software_manager.software.get("WebBrowser"), WebBrowser) - assert isinstance(client_1.software_manager.software.get("DNSClient"), DNSClient) + assert isinstance(client_1.software_manager.software.get("web-browser"), WebBrowser) + assert isinstance(client_1.software_manager.software.get("dns-client"), DNSClient) # 4.1: Create a file on the computer client_1.file_system.create_file("cat.png", 300, folder_name="downloads") @@ -403,19 +404,19 @@ def install_stuff_to_sim(sim: Simulation): # 5.2: Assert the client is correctly configured c: Computer = [node for node in sim.network.nodes.values() if node.hostname == "client_1"][0] - assert c.software_manager.software.get("WebBrowser") is not None - assert c.software_manager.software.get("DNSClient") is not None + assert c.software_manager.software.get("web-browser") is not None + assert c.software_manager.software.get("dns-client") is not None assert str(c.network_interface[1].ip_address) == "10.0.1.2" # 5.3: Assert that server_1 is correctly configured s1: Server = [node for node in sim.network.nodes.values() if node.hostname == "server_1"][0] assert str(s1.network_interface[1].ip_address) == "10.0.2.2" - assert s1.software_manager.software.get("DNSServer") is not None + assert s1.software_manager.software.get("dns-server") is not None # 5.4: Assert that server_2 is correctly configured s2: Server = [node for node in sim.network.nodes.values() if node.hostname == "server_2"][0] assert str(s2.network_interface[1].ip_address) == "10.0.2.3" - assert s2.software_manager.software.get("WebServer") is not None + assert s2.software_manager.software.get("web-server") is not None # 6: Return the simulation return sim @@ -429,7 +430,7 @@ def game_and_agent(): install_stuff_to_sim(sim) config = { - "type": "ControlledAgent", + "type": "controlled-agent", "ref": "test_agent", "team": "BLUE", } diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 4ca97a0e..79d0db1b 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -14,13 +14,13 @@ from tests import TEST_ASSETS_ROOT def test_data_manipulation(uc2_network): """Tests the UC2 data manipulation scenario end-to-end. Is a work in progress.""" client_1: Computer = uc2_network.get_node_by_hostname("client_1") - db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") + db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("data-manipulation-bot") database_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = database_server.software_manager.software.get("DatabaseService") + db_service: DatabaseService = database_server.software_manager.software.get("database-service") web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = web_server.software_manager.software.get("database-client") db_connection: DatabaseClientConnection = db_client.get_new_connection() db_service.backup_database() @@ -61,7 +61,7 @@ def test_application_install_uninstall_on_uc2(): # Test we can Install the DoSBot app _, _, _, _, info = env.step(78) - assert "DoSBot" in domcon.software_manager.software + assert "dos-bot" in domcon.software_manager.software # installing takes 3 steps so let's wait for 3 steps env.step(0) @@ -75,13 +75,13 @@ def test_application_install_uninstall_on_uc2(): # Test we can Uninstall the DoSBot app _, _, _, _, info = env.step(79) - assert "DoSBot" not in domcon.software_manager.software + assert "dos-bot" not in domcon.software_manager.software # Test we cannot execute the DoSBot app as it was uninstalled _, _, _, _, info = env.step(81) assert info["agent_actions"]["defender"].response.status == "unreachable" # Test we can uninstall one of the default apps (WebBrowser) - assert "WebBrowser" in domcon.software_manager.software + assert "web-browser" in domcon.software_manager.software _, _, _, _, info = env.step(80) - assert "WebBrowser" not in domcon.software_manager.software + assert "web-browser" not in domcon.software_manager.software diff --git a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py index 2a3691ae..fb34f43a 100644 --- a/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py @@ -99,7 +99,7 @@ def test_web_browser_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - web_browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = client_1.software_manager.software.get("web-browser") assert web_browser.target_url == "http://arcd.com/users/" @@ -109,7 +109,7 @@ def test_database_client_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - database_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client_1.software_manager.software.get("database-client") assert database_client.server_ip_address == IPv4Address("192.168.1.10") assert database_client.server_password == "arcd" @@ -120,7 +120,7 @@ def test_data_manipulation_bot_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") + data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("data-manipulation-bot") assert data_manipulation_bot.server_ip_address == IPv4Address("192.168.1.21") assert data_manipulation_bot.payload == "DELETE" @@ -134,7 +134,7 @@ def test_dos_bot_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot") + dos_bot: DoSBot = client_1.software_manager.software.get("dos-bot") assert dos_bot.target_ip_address == IPv4Address("192.168.10.21") assert dos_bot.payload == "SPOOF DATA" @@ -149,7 +149,7 @@ def test_dns_client_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - dns_client: DNSClient = client_1.software_manager.software.get("DNSClient") + dns_client: DNSClient = client_1.software_manager.software.get("dns-client") assert dns_client.dns_server == IPv4Address("192.168.1.10") @@ -159,7 +159,7 @@ def test_dns_server_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - dns_server: DNSServer = client_1.software_manager.software.get("DNSServer") + dns_server: DNSServer = client_1.software_manager.software.get("dns-server") assert dns_server.dns_lookup("arcd.com") == IPv4Address("192.168.1.10") @@ -169,7 +169,7 @@ def test_database_service_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - database_service: DatabaseService = client_1.software_manager.software.get("DatabaseService") + database_service: DatabaseService = client_1.software_manager.software.get("database-service") assert database_service.backup_server_ip == IPv4Address("192.168.1.10") @@ -179,10 +179,10 @@ def test_web_server_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - web_server_service: WebServer = client_1.software_manager.software.get("WebServer") + web_server_service: WebServer = client_1.software_manager.software.get("web-server") # config should have also installed database client - web server service should be able to retrieve this - assert web_server_service.software_manager.software.get("DatabaseClient") is not None + assert web_server_service.software_manager.software.get("database-client") is not None def test_ftp_client_install(): @@ -190,7 +190,7 @@ def test_ftp_client_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - ftp_client_service: FTPClient = client_1.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = client_1.software_manager.software.get("ftp-client") assert ftp_client_service is not None @@ -199,7 +199,7 @@ def test_ftp_server_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - ftp_server_service: FTPServer = client_1.software_manager.software.get("FTPServer") + ftp_server_service: FTPServer = client_1.software_manager.software.get("ftp-server") assert ftp_server_service is not None @@ -208,7 +208,7 @@ def test_ntp_client_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - ntp_client_service: NTPClient = client_1.software_manager.software.get("NTPClient") + ntp_client_service: NTPClient = client_1.software_manager.software.get("ntp-client") assert ntp_client_service is not None assert ntp_client_service.ntp_server == IPv4Address("192.168.1.10") @@ -218,5 +218,5 @@ def test_ntp_server_install(): game = load_config(BASIC_CONFIG) client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") - ntp_server_service: NTPServer = client_1.software_manager.software.get("NTPServer") + ntp_server_service: NTPServer = client_1.software_manager.software.get("ntp-server") assert ntp_server_service is not None diff --git a/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py b/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py index 10896956..118b8c1f 100644 --- a/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py +++ b/tests/integration_tests/configuration_file_parsing/test_software_fixing_duration.py @@ -16,7 +16,7 @@ from tests import TEST_ASSETS_ROOT TEST_CONFIG = TEST_ASSETS_ROOT / "configs/software_fixing_duration.yaml" ONE_ITEM_CONFIG = TEST_ASSETS_ROOT / "configs/fixing_duration_one_item.yaml" -TestApplications = ["DummyApplication", "BroadcastTestClient"] +TestApplications = ["dummy-application", "broadcast-test-client"] def load_config(config_path: Union[str, Path]) -> PrimaiteGame: @@ -32,10 +32,10 @@ def test_default_fixing_duration(): game = load_config(TEST_CONFIG) client_2: Computer = game.simulation.network.get_node_by_hostname("client_2") - database_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client_2.software_manager.software.get("database-client") assert database_client.config.fixing_duration == 2 - dns_client: DNSClient = client_2.software_manager.software.get("DNSClient") + dns_client: DNSClient = client_2.software_manager.software.get("dns-client") assert dns_client.config.fixing_duration == 2 @@ -45,7 +45,15 @@ def test_fixing_duration_set_from_config(): client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") # in config - services take 3 timesteps to fix - for service in ["DNSClient", "DNSServer", "DatabaseService", "WebServer", "FTPClient", "FTPServer", "NTPServer"]: + for service in [ + "dns-client", + "dns-server", + "database-service", + "web-server", + "ftp-client", + "ftp-server", + "ntp-server", + ]: assert client_1.software_manager.software.get(service) is not None assert client_1.software_manager.software.get(service).config.fixing_duration == 3 @@ -53,7 +61,7 @@ def test_fixing_duration_set_from_config(): # remove test applications from list applications = set(Application._registry) - set(TestApplications) - for application in ["RansomwareScript", "WebBrowser", "DataManipulationBot", "DoSBot", "DatabaseClient"]: + for application in ["ransomware-script", "web-browser", "data-manipulation-bot", "dos-bot", "database-client"]: assert client_1.software_manager.software.get(application) is not None assert client_1.software_manager.software.get(application).config.fixing_duration == 1 @@ -64,18 +72,18 @@ def test_fixing_duration_for_one_item(): client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") # in config - services take 3 timesteps to fix - for service in ["DNSClient", "DNSServer", "WebServer", "FTPClient", "FTPServer", "NTPServer"]: + for service in ["dns-client", "dns-server", "web-server", "ftp-client", "ftp-server", "ntp-server"]: assert client_1.software_manager.software.get(service) is not None assert client_1.software_manager.software.get(service).config.fixing_duration == 2 # in config - applications take 1 timestep to fix # remove test applications from list - for applications in ["RansomwareScript", "WebBrowser", "DataManipulationBot", "DoSBot"]: + for applications in ["ransomware-script", "web-browser", "data-manipulation-bot", "dos-bot"]: assert client_1.software_manager.software.get(applications) is not None assert client_1.software_manager.software.get(applications).config.fixing_duration == 2 - database_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client_1.software_manager.software.get("database-client") assert database_client.config.fixing_duration == 1 - database_service: DatabaseService = client_1.software_manager.software.get("DatabaseService") + database_service: DatabaseService = client_1.software_manager.software.get("database-service") assert database_service.config.fixing_duration == 5 diff --git a/tests/integration_tests/extensions/applications/extended_application.py b/tests/integration_tests/extensions/applications/extended_application.py index fd6fea3f..5ea85c57 100644 --- a/tests/integration_tests/extensions/applications/extended_application.py +++ b/tests/integration_tests/extensions/applications/extended_application.py @@ -24,7 +24,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ExtendedApplication(Application, discriminator="ExtendedApplication"): +class ExtendedApplication(Application, discriminator="extended-application"): """ Clone of web browser that uses the extension framework instead of being part of PrimAITE directly. @@ -34,7 +34,7 @@ class ExtendedApplication(Application, discriminator="ExtendedApplication"): class ConfigSchema(Application.ConfigSchema): """ConfigSchema for ExtendedApplication.""" - type: str = "ExtendedApplication" + type: str = "extended-application" target_url: Optional[str] = None config: "ExtendedApplication.ConfigSchema" = Field(default_factory=lambda: ExtendedApplication.ConfigSchema()) @@ -51,7 +51,7 @@ class ExtendedApplication(Application, discriminator="ExtendedApplication"): """Keep a log of visited websites and information about the visit, such as response code.""" def __init__(self, **kwargs): - kwargs["name"] = "ExtendedApplication" + kwargs["name"] = "extended-application" kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] # default for web is port 80 if kwargs.get("port") is None: @@ -112,7 +112,7 @@ class ExtendedApplication(Application, discriminator="ExtendedApplication"): return False # get the IP address of the domain name via DNS - dns_client: DNSClient = self.software_manager.software.get("DNSClient") + dns_client: DNSClient = self.software_manager.software.get("dns-client") domain_exists = dns_client.check_domain_exists(target_domain=parsed_url.hostname) # if domain does not exist, the request fails diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index 99c5fdf5..cf3ead58 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -34,7 +34,7 @@ class SuperComputer(HostNode, discriminator="supercomputer"): * Web Browser """ - SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} + SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "ftp-client": FTPClient} def __init__(self, ip_address: IPV4Address, subnet_mask: IPV4Address, **kwargs): print("--- Extended Component: SuperComputer ---") diff --git a/tests/integration_tests/extensions/services/extended_service.py b/tests/integration_tests/extensions/services/extended_service.py index 79821b6c..fc8dc630 100644 --- a/tests/integration_tests/extensions/services/extended_service.py +++ b/tests/integration_tests/extensions/services/extended_service.py @@ -19,7 +19,7 @@ from primaite.utils.validation.port import PORT_LOOKUP _LOGGER = getLogger(__name__) -class ExtendedService(Service, discriminator="ExtendedService"): +class ExtendedService(Service, discriminator="extended-service"): """ A copy of DatabaseService that uses the extension framework instead of being part of PrimAITE. @@ -29,7 +29,7 @@ class ExtendedService(Service, discriminator="ExtendedService"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for ExtendedService.""" - type: str = "ExtendedService" + type: str = "extended-service" config: "ExtendedService.ConfigSchema" = Field(default_factory=lambda: ExtendedService.ConfigSchema()) @@ -46,7 +46,7 @@ class ExtendedService(Service, discriminator="ExtendedService"): """File name of latest backup.""" def __init__(self, **kwargs): - kwargs["name"] = "ExtendedService" + kwargs["name"] = "extended-service" kwargs["port"] = PORT_LOOKUP["POSTGRES_SERVER"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -65,7 +65,7 @@ class ExtendedService(Service, discriminator="ExtendedService"): """ super().install() - if not self.parent.software_manager.software.get("FTPClient"): + if not self.parent.software_manager.software.get("ftp-client"): self.parent.sys_log.info(f"{self.name}: Installing FTPClient to enable database backups") self.parent.software_manager.install(FTPClient) @@ -89,7 +89,7 @@ class ExtendedService(Service, discriminator="ExtendedService"): return False software_manager: SoftwareManager = self.software_manager - ftp_client_service: FTPClient = software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = software_manager.software.get("ftp-client") if not ftp_client_service: self.sys_log.error( @@ -124,7 +124,7 @@ class ExtendedService(Service, discriminator="ExtendedService"): return False software_manager: SoftwareManager = self.software_manager - ftp_client_service: FTPClient = software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = software_manager.software.get("ftp-client") if not ftp_client_service: self.sys_log.error( diff --git a/tests/integration_tests/extensions/test_extendable_config.py b/tests/integration_tests/extensions/test_extendable_config.py index 8e73f929..34d1e418 100644 --- a/tests/integration_tests/extensions/test_extendable_config.py +++ b/tests/integration_tests/extensions/test_extendable_config.py @@ -30,5 +30,5 @@ def test_extended_example_config(): extended_host = network.get_node_by_hostname("client_1") - assert "ExtendedApplication" in extended_host.software_manager.software - assert "ExtendedService" in extended_host.software_manager.software + assert "extended-application" in extended_host.software_manager.software + assert "extended-service" in extended_host.software_manager.software diff --git a/tests/integration_tests/game_layer/actions/test_application_request_permission.py b/tests/integration_tests/game_layer/actions/test_application_request_permission.py index c47b617b..f1fc4b34 100644 --- a/tests/integration_tests/game_layer/actions/test_application_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_application_request_permission.py @@ -28,27 +28,27 @@ def test_application_cannot_perform_actions_unless_running(game_and_agent_fixtur game, agent = game_and_agent_fixture client_1 = game.simulation.network.get_node_by_hostname("client_1") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.close() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_scan", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ("node-application-scan", {"node_name": "client_1", "application_name": "web-browser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_close", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ("node-application-close", {"node_name": "client_1", "application_name": "web-browser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_fix", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ("node-application-fix", {"node_name": "client_1", "application_name": "web-browser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED - action = ("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"}) + action = ("node-application-execute", {"node_name": "client_1", "application_name": "web-browser"}) agent.store_action(action) game.step() assert browser.operating_state == ApplicationOperatingState.CLOSED diff --git a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py index 59eb8a60..7cab59ed 100644 --- a/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py +++ b/tests/integration_tests/game_layer/actions/test_c2_suite_actions.py @@ -32,7 +32,7 @@ def game_and_agent_fixture(game_and_agent): c2_server_host = game.simulation.network.get_node_by_hostname("client_1") c2_server_host.software_manager.install(software_class=C2Server) - c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] + c2_server: C2Server = c2_server_host.software_manager.software["c2-server"] c2_server.run() return (game, agent) @@ -46,15 +46,15 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen server_1: Server = game.simulation.network.get_node_by_hostname("server_1") action = ( - "node_application_install", - {"node_name": "server_1", "application_name": "C2Beacon"}, + "node-application-install", + {"node_name": "server_1", "application_name": "c2-beacon"}, ) agent.store_action(action) game.step() assert agent.history[-1].response.status == "success" action = ( - "configure_c2_beacon", + "configure-c2-beacon", { "node_name": "server_1", "c2_server_ip_address": "10.0.1.2", @@ -68,15 +68,15 @@ def test_c2_beacon_default(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgen assert agent.history[-1].response.status == "success" action = ( - "node_application_execute", - {"node_name": "server_1", "application_name": "C2Beacon"}, + "node-application-execute", + {"node_name": "server_1", "application_name": "c2-beacon"}, ) agent.store_action(action) game.step() assert agent.history[-1].response.status == "success" # Asserting that we've confirmed our connection - c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon: C2Beacon = server_1.software_manager.software["c2-beacon"] assert c2_beacon.c2_connection_active == True @@ -91,9 +91,9 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA # Installing a database on Server_2 for the ransomware to attack server_2: Server = game.simulation.network.get_node_by_hostname("server_2") server_2.software_manager.install(DatabaseService) - server_2.software_manager.software["DatabaseService"].start() + server_2.software_manager.software["database-service"].start() # Configuring the C2 to connect to client 1 (C2 Server) - c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon: C2Beacon = server_1.software_manager.software["c2-beacon"] c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) c2_beacon.establish() assert c2_beacon.c2_connection_active == True @@ -101,15 +101,15 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA # C2 Action 1: Installing the RansomwareScript & Database client via Terminal action = ( - "c2_server_terminal_command", + "c2-server-terminal-command", { "node_name": "client_1", "ip_address": None, "username": "admin", "password": "admin", "commands": [ - ["software_manager", "application", "install", "RansomwareScript"], - ["software_manager", "application", "install", "DatabaseClient"], + ["software_manager", "application", "install", "ransomware-script"], + ["software_manager", "application", "install", "database-client"], ], }, ) @@ -118,7 +118,7 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA assert agent.history[-1].response.status == "success" action = ( - "c2_server_ransomware_configure", + "c2-server-ransomware-configure", { "node_name": "client_1", "server_ip_address": "10.0.2.3", @@ -131,14 +131,14 @@ def test_c2_server_ransomware(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA # Stepping a few timesteps to allow for the RansowmareScript to finish installing. - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() game.step() game.step() action = ( - "c2_server_ransomware_launch", + "c2-server-ransomware-launch", { "node_name": "client_1", }, @@ -162,10 +162,10 @@ def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, # Installing a database on Server_2 (creates a database.db file.) server_2: Server = game.simulation.network.get_node_by_hostname("server_2") server_2.software_manager.install(DatabaseService) - server_2.software_manager.software["DatabaseService"].start() + server_2.software_manager.software["database-service"].start() # Configuring the C2 to connect to client 1 (C2 Server) - c2_beacon: C2Beacon = server_1.software_manager.software["C2Beacon"] + c2_beacon: C2Beacon = server_1.software_manager.software["c2-beacon"] c2_beacon.configure(c2_server_ip_address=IPv4Address("10.0.1.2")) c2_beacon.establish() assert c2_beacon.c2_connection_active == True @@ -178,7 +178,7 @@ def test_c2_server_data_exfiltration(game_and_agent_fixture: Tuple[PrimaiteGame, # C2 Action: Data exfiltrate. action = ( - "c2_server_data_exfiltrate", + "c2-server-data-exfiltrate", { "node_name": "client_1", "target_file_name": "database.db", diff --git a/tests/integration_tests/game_layer/actions/test_configure_actions.py b/tests/integration_tests/game_layer/actions/test_configure_actions.py index 17559405..35d65a5a 100644 --- a/tests/integration_tests/game_layer/actions/test_configure_actions.py +++ b/tests/integration_tests/game_layer/actions/test_configure_actions.py @@ -31,10 +31,10 @@ class TestConfigureDatabaseAction: # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DatabaseClient) - db_client: DatabaseClient = client_1.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = client_1.software_manager.software["database-client"] action = ( - "configure_database_client", + "configure-database-client", { "node_name": "client_1", "server_ip_address": "192.168.1.99", @@ -54,10 +54,10 @@ class TestConfigureDatabaseAction: # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DatabaseClient) - db_client: DatabaseClient = client_1.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = client_1.software_manager.software["database-client"] action = ( - "configure_database_client", + "configure-database-client", { "node_name": "client_1", "server_ip_address": "192.168.1.99", @@ -76,11 +76,11 @@ class TestConfigureDatabaseAction: # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DatabaseClient) - db_client: DatabaseClient = client_1.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = client_1.software_manager.software["database-client"] old_ip = db_client.server_ip_address action = ( - "configure_database_client", + "configure-database-client", { "node_name": "client_1", "server_password": "admin123", @@ -115,14 +115,14 @@ class TestConfigureRansomwareScriptAction: # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(RansomwareScript) - ransomware_script: RansomwareScript = client_1.software_manager.software["RansomwareScript"] + ransomware_script: RansomwareScript = client_1.software_manager.software["ransomware-script"] old_ip = ransomware_script.server_ip_address old_pw = ransomware_script.server_password old_payload = ransomware_script.payload action = ( - "configure_ransomware_script", + "configure-ransomware-script", {"node_name": "client_1", **config}, ) agent.store_action(action) @@ -143,9 +143,9 @@ class TestConfigureRansomwareScriptAction: # make sure there is a database client on this node client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(RansomwareScript) - ransomware_script: RansomwareScript = client_1.software_manager.software["RansomwareScript"] + ransomware_script: RansomwareScript = client_1.software_manager.software["ransomware-script"] action = ( - "configure_ransomware_script", + "configure-ransomware-script", { "node_name": "client_1", "config": {"server_password": "admin123", "bad_option": 70}, @@ -163,10 +163,10 @@ class TestConfigureDoSBot: client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DoSBot) - dos_bot: DoSBot = client_1.software_manager.software["DoSBot"] + dos_bot: DoSBot = client_1.software_manager.software["dos-bot"] action = ( - "configure_dos_bot", + "configure-dos-bot", { "node_name": "client_1", "target_ip_address": "192.168.1.99", @@ -196,11 +196,11 @@ class TestConfigureYAML: # make sure there's no db client on the node yet client_1 = env.game.simulation.network.get_node_by_hostname("client_1") - assert client_1.software_manager.software.get("DatabaseClient") is None + assert client_1.software_manager.software.get("database-client") is None # take the install action, check that the db gets installed, step to get it to finish installing env.step(1) - db_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = client_1.software_manager.software.get("database-client") assert isinstance(db_client, DatabaseClient) assert db_client.operating_state == ApplicationOperatingState.INSTALLING env.step(0) @@ -224,11 +224,11 @@ class TestConfigureYAML: def test_c2_server_ransomware_configure(self): env = PrimaiteGymEnv(env_config=APP_CONFIG_YAML) client_2 = env.game.simulation.network.get_node_by_hostname("client_2") - assert client_2.software_manager.software.get("RansomwareScript") is None + assert client_2.software_manager.software.get("ransomware-script") is None # install ransomware script env.step(2) - ransom = client_2.software_manager.software.get("RansomwareScript") + ransom = client_2.software_manager.software.get("ransomware-script") assert isinstance(ransom, RansomwareScript) assert ransom.operating_state == ApplicationOperatingState.INSTALLING env.step(0) @@ -250,17 +250,17 @@ class TestConfigureYAML: assert ransom.attack() db_server = env.game.simulation.network.get_node_by_hostname("server_1") - db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") + db_service: DatabaseService = db_server.software_manager.software.get("database-service") assert db_service.db_file.health_status == FileSystemItemHealthStatus.CORRUPT def test_configure_dos_bot(self): env = PrimaiteGymEnv(env_config=APP_CONFIG_YAML) client_3 = env.game.simulation.network.get_node_by_hostname("client_3") - assert client_3.software_manager.software.get("DoSBot") is None + assert client_3.software_manager.software.get("dos-bot") is None # install DoSBot env.step(3) - bot = client_3.software_manager.software.get("DoSBot") + bot = client_3.software_manager.software.get("dos-bot") assert isinstance(bot, DoSBot) assert bot.operating_state == ApplicationOperatingState.INSTALLING env.step(0) diff --git a/tests/integration_tests/game_layer/actions/test_file_request_permission.py b/tests/integration_tests/game_layer/actions/test_file_request_permission.py index cab80434..905dbfc9 100644 --- a/tests/integration_tests/game_layer/actions/test_file_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_file_request_permission.py @@ -33,7 +33,7 @@ def test_create_file(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert client_1.file_system.get_file(folder_name=random_folder, file_name=random_file) is None action = ( - "node_file_create", + "node-file-create", {"node_name": "client_1", "folder_name": random_folder, "file_name": random_file}, ) agent.store_action(action) @@ -51,7 +51,7 @@ def test_file_delete_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert file.deleted is False action = ( - "node_file_delete", + "node-file-delete", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) @@ -72,7 +72,7 @@ def test_file_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent assert file.visible_health_status == FileSystemItemHealthStatus.NONE action = ( - "node_file_scan", + "node-file-scan", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) @@ -93,7 +93,7 @@ def test_file_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert file.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "node_file_repair", + "node-file-repair", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) @@ -113,7 +113,7 @@ def test_file_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAg assert file.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "node_file_restore", + "node-file-restore", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) @@ -132,7 +132,7 @@ def test_file_corrupt_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAg assert file.health_status == FileSystemItemHealthStatus.GOOD action = ( - "node_file_corrupt", + "node-file-corrupt", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, ) agent.store_action(action) @@ -150,7 +150,7 @@ def test_file_access_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert file.num_access == 0 action = ( - "node_file_access", + "node-file-access", {"node_name": "client_1", "folder_name": file.folder_name, "file_name": file.name}, ) agent.store_action(action) diff --git a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py index 207f7d48..1bd1add3 100644 --- a/tests/integration_tests/game_layer/actions/test_folder_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_folder_request_permission.py @@ -32,7 +32,7 @@ def test_create_folder(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert client_1.file_system.get_folder(folder_name=random_folder) is None action = ( - "node_folder_create", + "node-folder-create", { "node_name": "client_1", "folder_name": random_folder, @@ -60,7 +60,7 @@ def test_folder_scan_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAge assert folder.visible_health_status == FileSystemItemHealthStatus.NONE action = ( - "node_folder_scan", + "node-folder-scan", { "node_name": "client_1", # client_1, "folder_name": "downloads", # downloads @@ -87,7 +87,7 @@ def test_folder_repair_action(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyA assert folder.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "node_folder_repair", + "node-folder-repair", { "node_name": "client_1", # client_1, "folder_name": "downloads", # downloads @@ -111,7 +111,7 @@ def test_folder_restore_action(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert folder.health_status == FileSystemItemHealthStatus.CORRUPT action = ( - "node_folder_restore", + "node-folder-restore", { "node_name": "client_1", # client_1, "folder_name": "downloads", # downloads diff --git a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py index 11e39c7e..a68e4b23 100644 --- a/tests/integration_tests/game_layer/actions/test_nic_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_nic_request_permission.py @@ -29,7 +29,7 @@ def test_nic_cannot_be_turned_off_if_not_on(game_and_agent_fixture: Tuple[Primai assert nic.enabled is False action = ( - "host_nic_disable", + "host-nic-disable", { "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) @@ -50,7 +50,7 @@ def test_nic_cannot_be_turned_on_if_already_on(game_and_agent_fixture: Tuple[Pri assert nic.enabled action = ( - "host_nic_enable", + "host-nic-enable", { "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) @@ -71,7 +71,7 @@ def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[Pr assert nic.enabled action = ( - "host_nic_disable", + "host-nic-disable", { "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) @@ -83,7 +83,7 @@ def test_that_a_nic_can_be_enabled_and_disabled(game_and_agent_fixture: Tuple[Pr assert nic.enabled is False action = ( - "host_nic_enable", + "host-nic-enable", { "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) diff --git a/tests/integration_tests/game_layer/actions/test_node_request_permission.py b/tests/integration_tests/game_layer/actions/test_node_request_permission.py index 8a438673..58ea4c98 100644 --- a/tests/integration_tests/game_layer/actions/test_node_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_node_request_permission.py @@ -29,28 +29,28 @@ def test_node_startup_shutdown(game_and_agent_fixture: Tuple[PrimaiteGame, Proxy assert client_1.operating_state == NodeOperatingState.ON # turn it off - action = ("node_shutdown", {"node_name": "client_1"}) + action = ("node-shutdown", {"node_name": "client_1"}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.SHUTTING_DOWN for i in range(client_1.shut_down_duration + 1): - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.OFF # turn it on - action = ("node_startup", {"node_name": "client_1"}) + action = ("node-startup", {"node_name": "client_1"}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.BOOTING for i in range(client_1.start_up_duration + 1): - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() @@ -65,7 +65,7 @@ def test_node_cannot_be_started_up_if_node_is_already_on(game_and_agent_fixture: assert client_1.operating_state == NodeOperatingState.ON # turn it on - action = ("node_startup", {"node_name": "client_1"}) + action = ("node-startup", {"node_name": "client_1"}) agent.store_action(action) game.step() @@ -80,14 +80,14 @@ def test_node_cannot_be_shut_down_if_node_is_already_off(game_and_agent_fixture: client_1.power_off() for i in range(client_1.shut_down_duration + 1): - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() assert client_1.operating_state == NodeOperatingState.OFF # turn it ff - action = ("node_shutdown", {"node_name": "client_1"}) + action = ("node-shutdown", {"node_name": "client_1"}) agent.store_action(action) game.step() diff --git a/tests/integration_tests/game_layer/actions/test_service_request_permission.py b/tests/integration_tests/game_layer/actions/test_service_request_permission.py index 80e68131..9bf7a38c 100644 --- a/tests/integration_tests/game_layer/actions/test_service_request_permission.py +++ b/tests/integration_tests/game_layer/actions/test_service_request_permission.py @@ -26,12 +26,12 @@ def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): game, agent = game_and_agent_fixture server_1: Server = game.simulation.network.get_node_by_hostname("server_1") - dns_server = server_1.software_manager.software.get("DNSServer") + dns_server = server_1.software_manager.software.get("dns-server") dns_server.pause() assert dns_server.operating_state == ServiceOperatingState.PAUSED - action = ("node_service_start", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-start", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.PAUSED @@ -40,7 +40,7 @@ def test_service_start(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_start", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-start", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() @@ -52,9 +52,9 @@ def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]) game, agent = game_and_agent_fixture server_1: Server = game.simulation.network.get_node_by_hostname("server_1") - dns_server = server_1.software_manager.software.get("DNSServer") + dns_server = server_1.software_manager.software.get("dns-server") - action = ("node_service_resume", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-resume", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.RUNNING @@ -63,7 +63,7 @@ def test_service_resume(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]) assert dns_server.operating_state == ServiceOperatingState.PAUSED - action = ("node_service_resume", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-resume", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() @@ -75,32 +75,32 @@ def test_service_cannot_perform_actions_unless_running(game_and_agent_fixture: T game, agent = game_and_agent_fixture server_1: Server = game.simulation.network.get_node_by_hostname("server_1") - dns_server = server_1.software_manager.software.get("DNSServer") + dns_server = server_1.software_manager.software.get("dns-server") dns_server.stop() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-scan", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_pause", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-pause", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_resume", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-resume", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_restart", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-restart", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED - action = ("node_service_fix", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-fix", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert dns_server.operating_state == ServiceOperatingState.STOPPED diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index f15f7156..c39d8263 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -32,11 +32,11 @@ def test_remote_login(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): client_1 = game.simulation.network.get_node_by_hostname("client_1") # create a new user account on server_1 that will be logged into remotely - server_1_usm: UserManager = server_1.software_manager.software["UserManager"] + server_1_usm: UserManager = server_1.software_manager.software["user-manager"] server_1_usm.add_user("user123", "password", is_admin=True) action = ( - "node_session_remote_login", + "node-session-remote-login", { "node_name": "client_1", "username": "user123", @@ -64,11 +64,11 @@ def test_remote_login_wrong_password(game_and_agent_fixture: Tuple[PrimaiteGame, client_1 = game.simulation.network.get_node_by_hostname("client_1") # create a new user account on server_1 that will be logged into remotely - server_1_usm: UserManager = server_1.software_manager.software["UserManager"] + server_1_usm: UserManager = server_1.software_manager.software["user-manager"] server_1_usm.add_user("user123", "password", is_admin=True) action = ( - "node_session_remote_login", + "node-session-remote-login", { "node_name": "client_1", "username": "user123", @@ -96,11 +96,11 @@ def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame client_1 = game.simulation.network.get_node_by_hostname("client_1") # create a new user account on server_1 that will be logged into remotely - server_1_um: UserManager = server_1.software_manager.software["UserManager"] + server_1_um: UserManager = server_1.software_manager.software["user-manager"] server_1_um.add_user("user123", "password", is_admin=True) action = ( - "node_account_change_password", + "node-account-change-password", { "node_name": "server_1", # server_1 "username": "user123", @@ -122,12 +122,12 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam client_1 = game.simulation.network.get_node_by_hostname("client_1") # create a new user account on server_1 that will be logged into remotely - server_1_usm: UserManager = server_1.software_manager.software["UserManager"] + server_1_usm: UserManager = server_1.software_manager.software["user-manager"] server_1_usm.add_user("user123", "password", is_admin=True) # Log in remotely action = ( - "node_session_remote_login", + "node-session-remote-login", { "node_name": "client_1", "username": "user123", @@ -140,7 +140,7 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam # Change password action = ( - "node_account_change_password", + "node-account-change-password", { "node_name": "server_1", # server_1 "username": "user123", @@ -154,7 +154,7 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam # Assert that the user cannot execute an action action = ( - "node_send_remote_command", + "node-send-remote-command", { "node_name": "client_1", "remote_ip": str(server_1.network_interface[1].ip_address), diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py index 02cf005a..44ef4a70 100644 --- a/tests/integration_tests/game_layer/observations/test_acl_observations.py +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -28,7 +28,7 @@ def test_acl_observations(simulation): # quick set up of ntp client_1.software_manager.install(NTPClient) - ntp_client: NTPClient = client_1.software_manager.software.get("NTPClient") + ntp_client: NTPClient = client_1.software_manager.software.get("ntp-client") ntp_client.configure(server.network_interface.get(1).ip_address) server.software_manager.install(NTPServer) diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index bd9417ba..2a6ac44d 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -43,23 +43,23 @@ def simulation(example_network) -> Simulation: computer: Computer = example_network.get_node_by_hostname("client_1") server: Server = example_network.get_node_by_hostname("server_1") - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") web_browser.run() # Install DNS Client service on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software.get("DNSClient") + dns_client: DNSClient = computer.software_manager.software.get("dns-client") # set dns server dns_client.dns_server = server.network_interface[1].ip_address # Install Web Server service on server server.software_manager.install(WebServer) - web_server_service: WebServer = server.software_manager.software.get("WebServer") + web_server_service: WebServer = server.software_manager.software.get("web-server") web_server_service.start() # Install DNS Server service on server server.software_manager.install(DNSServer) - dns_server: DNSServer = server.software_manager.software.get("DNSServer") + dns_server: DNSServer = server.software_manager.software.get("dns-server") # register arcd.com to DNS dns_server.dns_register( domain_name="arcd.com", @@ -190,7 +190,7 @@ def test_nic_monitored_traffic(simulation): assert traffic_obs["tcp"][53]["outbound"] == 0 # send a database query - browser: WebBrowser = pc.software_manager.software.get("WebBrowser") + browser: WebBrowser = pc.software_manager.software.get("web-browser") browser.config.target_url = f"http://arcd.com/" browser.get_webpage() diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py index 291ee395..1d457b0f 100644 --- a/tests/integration_tests/game_layer/observations/test_software_observations.py +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -26,10 +26,10 @@ def test_service_observation(simulation): # install software on the computer pc.software_manager.install(NTPServer) - ntp_server = pc.software_manager.software.get("NTPServer") + ntp_server = pc.software_manager.software.get("ntp-server") assert ntp_server - service_obs = ServiceObservation(where=["network", "nodes", pc.hostname, "services", "NTPServer"]) + service_obs = ServiceObservation(where=["network", "nodes", pc.hostname, "services", "ntp-server"]) assert service_obs.space["operating_status"] == spaces.Discrete(7) assert service_obs.space["health_status"] == spaces.Discrete(5) @@ -51,10 +51,10 @@ def test_application_observation(simulation): # install software on the computer pc.software_manager.install(DatabaseClient) - web_browser: WebBrowser = pc.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = pc.software_manager.software.get("web-browser") assert web_browser - app_obs = ApplicationObservation(where=["network", "nodes", pc.hostname, "applications", "WebBrowser"]) + app_obs = ApplicationObservation(where=["network", "nodes", pc.hostname, "applications", "web-browser"]) web_browser.close() observation_state = app_obs.observe(simulation.describe_state()) diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index 464f95db..45fa445d 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -24,12 +24,12 @@ def test_rng_seed_set(create_env): env.reset(seed=3) for i in range(100): env.step(0) - a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] + a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do-nothing"] env.reset(seed=3) for i in range(100): env.step(0) - b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] + b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do-nothing"] assert a == b @@ -40,11 +40,11 @@ def test_rng_seed_unset(create_env): env.reset() for i in range(100): env.step(0) - a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] + a = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do-nothing"] env.reset() for i in range(100): env.step(0) - b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do_nothing"] + b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do-nothing"] assert a != b diff --git a/tests/integration_tests/game_layer/test_action_mask.py b/tests/integration_tests/game_layer/test_action_mask.py index 75965f16..9a489a50 100644 --- a/tests/integration_tests/game_layer/test_action_mask.py +++ b/tests/integration_tests/game_layer/test_action_mask.py @@ -44,13 +44,13 @@ def test_mask_contents_correct(): assert not mask[action_num] nic_obj.enable() - if act_type == "router_acl_add_rule": + if act_type == "router-acl-add-rule": assert mask[action_num] - if act_type == "router_acl_remove_rule": + if act_type == "router-acl-remove-rule": assert mask[action_num] - if act_type == "node_reset": + if act_type == "node-reset": node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON @@ -60,7 +60,7 @@ def test_mask_contents_correct(): assert not mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "node_shutdown": + if act_type == "node-shutdown": node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON @@ -70,7 +70,7 @@ def test_mask_contents_correct(): assert not mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "node_os_scan": + if act_type == "node-os-scan": node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON @@ -80,7 +80,7 @@ def test_mask_contents_correct(): assert not mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "node_startup": + if act_type == "node-startup": node_name = act_params["node_name"] node_obj = net.get_node_by_hostname(node_name) assert node_obj.operating_state is NodeOperatingState.ON @@ -90,13 +90,13 @@ def test_mask_contents_correct(): assert mask[action_num] node_obj.operating_state = NodeOperatingState.ON - if act_type == "do_nothing": + if act_type == "do-nothing": assert mask[action_num] - if act_type == "node_service_disable": + if act_type == "node-service-disable": assert mask[action_num] - if act_type in ["node_service_scan", "node_service_stop", "node_service_pause"]: + if act_type in ["node-service-scan", "node-service-stop", "node-service-pause"]: node_name = act_params["node_name"] service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) @@ -108,7 +108,7 @@ def test_mask_contents_correct(): assert not mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type == "node_service_resume": + if act_type == "node-service-resume": node_name = act_params["node_name"] service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) @@ -120,7 +120,7 @@ def test_mask_contents_correct(): assert mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type == "node_service_start": + if act_type == "node-service-start": node_name = act_params["node_name"] service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) @@ -132,7 +132,7 @@ def test_mask_contents_correct(): assert mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type == "node_service_enable": + if act_type == "node-service-enable": node_name = act_params["node_name"] service_name = act_params["service_name"] node_obj = net.get_node_by_hostname(node_name) @@ -144,7 +144,7 @@ def test_mask_contents_correct(): assert mask[action_num] service_obj.operating_state = ServiceOperatingState.RUNNING - if act_type in ["node_file_scan", "node_file_checkhash", "node_file_delete"]: + if act_type in ["node-file-scan", "node-file-checkhash", "node-file-delete"]: node_name = act_params["node_name"] folder_name = act_params["folder_name"] file_name = act_params["file_name"] diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 5a308cf8..60cbaa53 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -35,7 +35,7 @@ def test_do_nothing_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]) """Test that the do_nothingAction can form a request and that it is accepted by the simulation.""" game, agent = game_and_agent - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() @@ -51,12 +51,12 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy game, agent = game_and_agent # 1: Check that the service starts off in a good state, and that visible state is hidden until first scan - svc = game.simulation.network.get_node_by_hostname("server_1").software_manager.software.get("DNSServer") + svc = game.simulation.network.get_node_by_hostname("server_1").software_manager.software.get("dns-server") assert svc.health_state_actual == SoftwareHealthState.GOOD assert svc.health_state_visible == SoftwareHealthState.UNUSED # 2: Scan and check that the visible state is now correct - action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-scan", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -67,7 +67,7 @@ def test_node_service_scan_integration(game_and_agent: Tuple[PrimaiteGame, Proxy assert svc.health_state_visible == SoftwareHealthState.GOOD # 4: Scan and check that the visible state is now correct - action = ("node_service_scan", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-scan", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.COMPROMISED @@ -84,11 +84,11 @@ def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA game, agent = game_and_agent # 1: Corrupt the service - svc = game.simulation.network.get_node_by_hostname("server_1").software_manager.software.get("DNSServer") + svc = game.simulation.network.get_node_by_hostname("server_1").software_manager.software.get("dns-server") svc.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a patch action - action = ("node_service_fix", {"node_name": "server_1", "service_name": "DNSServer"}) + action = ("node-service-fix", {"node_name": "server_1", "service_name": "dns-server"}) agent.store_action(action) game.step() @@ -96,7 +96,7 @@ def test_node_service_fix_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA assert svc.health_state_actual == SoftwareHealthState.FIXING # 4: perform a few do-nothing steps and check that the service is now in the good state - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() assert svc.health_state_actual == SoftwareHealthState.GOOD @@ -106,7 +106,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox """ Test that the RouterACLAddRuleAction can form a request and that it is accepted by the simulation. - The ACL starts off with 4 rules, and we add a rule, and check that the ACL now has 5 rules. + The acl starts off with 4 rules, and we add a rule, and check that the acl now has 5 rules. """ game, agent = game_and_agent @@ -121,7 +121,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox # 2: Add a rule to block client 1 from reaching server 2 on router action = ( - "router_acl_add_rule", + "router-acl-add-rule", { "target_router": "router", "position": 4, @@ -138,7 +138,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox agent.store_action(action) game.step() - # 3: Check that the ACL now has 5 rules, and that client 1 cannot ping server 2 + # 3: Check that the acl now has 5 rules, and that client 1 cannot ping server 2 assert router.acl.num_rules == 5 assert not client_1.ping("10.0.2.3") # Cannot ping server_2 assert client_1.ping("10.0.2.2") # Can ping server_1 @@ -148,7 +148,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox # 4: Add a rule to block server_1 from reaching server_2 on router (this should not affect comms as they are on same subnet) action = ( - "router_acl_add_rule", + "router-acl-add-rule", { "target_router": "router", "position": 5, # 5th rule @@ -181,14 +181,14 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P server_1 = game.simulation.network.get_node_by_hostname("server_1") router = game.simulation.network.get_node_by_hostname("router") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com before we block it # 2: Remove rule that allows HTTP traffic across the network action = ( - "router_acl_remove_rule", + "router-acl-remove-rule", { "target_router": "router", "position": 3, # 4th rule @@ -200,7 +200,7 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P # 3: Check that the ACL now has 3 rules, and that client 1 cannot access example.com assert router.acl.num_rules == 3 assert not browser.get_webpage() - client_1.software_manager.software.get("DNSClient").dns_cache.clear() + client_1.software_manager.software.get("dns-client").dns_cache.clear() assert client_1.ping("10.0.2.2") # pinging still works because ICMP is allowed assert client_1.ping("10.0.2.3") @@ -214,14 +214,14 @@ def test_host_nic_disable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA server_1 = game.simulation.network.get_node_by_hostname("server_1") server_2 = game.simulation.network.get_node_by_hostname("server_2") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com before we block it # 2: Disable the NIC on client_1 action = ( - "host_nic_disable", + "host-nic-disable", { "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) @@ -252,7 +252,7 @@ def test_host_nic_enable_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAg # 2: Use action to enable nic action = ( - "host_nic_enable", + "host-nic-enable", { "node_name": "client_1", # client_1 "nic_num": 1, # the only nic (eth-1) @@ -279,7 +279,7 @@ def test_node_file_scan_integration(game_and_agent: Tuple[PrimaiteGame, ProxyAge # 2: perform a scan and make sure nothing has changed action = ( - "node_file_scan", + "node-file-scan", { "node_name": "client_1", # client_1, "folder_name": "downloads", # downloads, @@ -316,7 +316,7 @@ def test_node_file_delete_integration(game_and_agent: Tuple[PrimaiteGame, ProxyA # 2: delete the file action = ( - "node_file_delete", + "node-file-delete", { "node_name": "client_1", # client_1 "folder_name": "downloads", # downloads @@ -339,7 +339,7 @@ def test_node_file_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): client_1 = game.simulation.network.get_node_by_hostname("client_1") action = ( - "node_file_create", + "node-file-create", { "node_name": "client_1", "folder_name": "test", @@ -360,7 +360,7 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): client_1 = game.simulation.network.get_node_by_hostname("client_1") # action = ( - "node_file_create", + "node-file-create", { "node_name": "client_1", "folder_name": "test", @@ -373,7 +373,7 @@ def test_node_file_access(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): assert client_1.file_system.get_file(folder_name="test", file_name="file.txt").num_access == 0 action = ( - "node_file_access", + "node-file-access", { "node_name": "client_1", "folder_name": "test", @@ -393,7 +393,7 @@ def test_node_folder_create(game_and_agent: Tuple[PrimaiteGame, ProxyAgent]): client_1 = game.simulation.network.get_node_by_hostname("client_1") # action = ( - "node_folder_create", + "node-folder-create", { "node_name": "client_1", "folder_name": "test", @@ -414,14 +414,14 @@ def test_network_router_port_disable_integration(game_and_agent: Tuple[PrimaiteG server_1 = game.simulation.network.get_node_by_hostname("server_1") router = game.simulation.network.get_node_by_hostname("router") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com before we block it # 2: Disable the NIC on client_1 action = ( - "network_port_disable", + "network-port-disable", { "target_nodename": "router", # router "port_num": 1, # port 1 @@ -453,7 +453,7 @@ def test_network_router_port_enable_integration(game_and_agent: Tuple[PrimaiteGa # 2: Use action to enable port action = ( - "network_port_enable", + "network-port-enable", { "target_nodename": "router", # router "port_num": 1, # port 1 @@ -474,7 +474,7 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P # 1: Check that http traffic is going across the network nicely. client_1 = game.simulation.network.get_node_by_hostname("client_1") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() browser.config.target_url = "http://www.example.com" assert browser.get_webpage() # check that the browser can access example.com @@ -484,8 +484,8 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P # 2: Scan and check that the visible state is now correct action = ( - "node_application_scan", - {"node_name": "client_1", "application_name": "WebBrowser"}, + "node-application-scan", + {"node_name": "client_1", "application_name": "web-browser"}, ) agent.store_action(action) game.step() @@ -498,8 +498,8 @@ def test_node_application_scan_integration(game_and_agent: Tuple[PrimaiteGame, P # 4: Scan and check that the visible state is now correct action = ( - "node_application_scan", - {"node_name": "client_1", "application_name": "WebBrowser"}, + "node-application-scan", + {"node_name": "client_1", "application_name": "web-browser"}, ) agent.store_action(action) game.step() @@ -517,13 +517,13 @@ def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, Pr # 1: Check that http traffic is going across the network nicely. client_1 = game.simulation.network.get_node_by_hostname("client_1") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.health_state_actual = SoftwareHealthState.COMPROMISED # 2: Apply a fix action action = ( - "node_application_fix", - {"node_name": "client_1", "application_name": "WebBrowser"}, + "node-application-fix", + {"node_name": "client_1", "application_name": "web-browser"}, ) agent.store_action(action) game.step() @@ -532,7 +532,7 @@ def test_node_application_fix_integration(game_and_agent: Tuple[PrimaiteGame, Pr assert browser.health_state_actual == SoftwareHealthState.FIXING # 4: perform a few do-nothing steps and check that the application is now in the good state - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() assert browser.health_state_actual == SoftwareHealthState.GOOD @@ -545,14 +545,14 @@ def test_node_application_close_integration(game_and_agent: Tuple[PrimaiteGame, game, agent = game_and_agent client_1 = game.simulation.network.get_node_by_hostname("client_1") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() assert browser.operating_state == ApplicationOperatingState.RUNNING # 2: Apply a close action action = ( - "node_application_close", - {"node_name": "client_1", "application_name": "WebBrowser"}, + "node-application-close", + {"node_name": "client_1", "application_name": "web-browser"}, ) agent.store_action(action) game.step() @@ -570,25 +570,25 @@ def test_node_application_install_and_uninstall_integration(game_and_agent: Tupl client_1 = game.simulation.network.get_node_by_hostname("client_1") - assert client_1.software_manager.software.get("DoSBot") is None + assert client_1.software_manager.software.get("dos-bot") is None action = ( - "node_application_install", - {"node_name": "client_1", "application_name": "DoSBot"}, + "node-application-install", + {"node_name": "client_1", "application_name": "dos-bot"}, ) agent.store_action(action) game.step() - assert client_1.software_manager.software.get("DoSBot") is not None + assert client_1.software_manager.software.get("dos-bot") is not None action = ( - "node_application_remove", - {"node_name": "client_1", "application_name": "DoSBot"}, + "node-application-remove", + {"node_name": "client_1", "application_name": "dos-bot"}, ) agent.store_action(action) game.step() - assert client_1.software_manager.software.get("DoSBot") is None + assert client_1.software_manager.software.get("dos-bot") is None def test_firewall_acl_add_remove_rule_integration(): diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index de61b215..8aae311c 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -27,18 +27,18 @@ def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, Controlle comp = WebpageUnavailablePenalty(config=schema) client_1 = game.simulation.network.get_node_by_hostname("client_1") - browser: WebBrowser = client_1.software_manager.software.get("WebBrowser") + browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() browser.config.target_url = "http://www.example.com" agent.reward_function.register_component(comp, 0.7) # Check that before trying to fetch the webpage, the reward is 0.0 - agent.store_action(("do_nothing", {})) + agent.store_action(("do-nothing", {})) game.step() assert agent.reward_function.current_reward == 0.0 # Check that successfully fetching the webpage yields a reward of 0.7 - agent.store_action(("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"})) + agent.store_action(("node-application-execute", {"node_name": "client_1", "application_name": "web-browser"})) game.step() assert agent.reward_function.current_reward == 0.7 @@ -50,7 +50,7 @@ def test_WebpageUnavailablePenalty(game_and_agent: tuple[PrimaiteGame, Controlle src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], ) - agent.store_action(("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"})) + agent.store_action(("node-application-execute", {"node_name": "client_1", "application_name": "web-browser"})) game.step() assert agent.reward_function.current_reward == -0.7 @@ -62,12 +62,12 @@ def test_uc2_rewards(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): server_1: Server = game.simulation.network.get_node_by_hostname("server_1") server_1.software_manager.install(DatabaseService) - db_service = server_1.software_manager.software.get("DatabaseService") + db_service = server_1.software_manager.software.get("database-service") db_service.start() client_1 = game.simulation.network.get_node_by_hostname("client_1") client_1.software_manager.install(DatabaseClient) - db_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = client_1.software_manager.software.get("database-client") db_client.configure(server_ip_address=server_1.network_interface[1].ip_address) db_client.run() @@ -79,11 +79,11 @@ def test_uc2_rewards(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): schema = GreenAdminDatabaseUnreachablePenalty.ConfigSchema(node_hostname="client_1", sticky=True) comp = GreenAdminDatabaseUnreachablePenalty(config=schema) - request = ["network", "node", "client_1", "application", "DatabaseClient", "execute"] + request = ["network", "node", "client_1", "application", "database-client", "execute"] response = game.simulation.apply_request(request) state = game.get_sim_state() ahi = AgentHistoryItem( - timestep=0, action="node_application_execute", parameters={}, request=request, response=response + timestep=0, action="node-application-execute", parameters={}, request=request, response=response ) reward_value = comp.calculate(state, last_action_response=ahi) assert reward_value == 1.0 @@ -94,7 +94,7 @@ def test_uc2_rewards(game_and_agent: tuple[PrimaiteGame, ControlledAgent]): response = game.simulation.apply_request(request) state = game.get_sim_state() ahi = AgentHistoryItem( - timestep=0, action="node_application_execute", parameters={}, request=request, response=response + timestep=0, action="node-application-execute", parameters={}, request=request, response=response ) reward_value = comp.calculate( state, @@ -154,13 +154,13 @@ def test_action_penalty(): # Penalty = ActionPenalty(action_penalty=-0.75, do_nothing_penalty=0.125) Penalty = ActionPenalty(config=schema) - # Assert that penalty is applied if action isn't do_nothing + # Assert that penalty is applied if action isn't do-nothing reward_value = Penalty.calculate( state={}, last_action_response=AgentHistoryItem( timestep=0, - action="node_application_execute", - parameters={"node_name": "client", "application_name": "WebBrowser"}, + action="node-application-execute", + parameters={"node_name": "client", "application_name": "web-browser"}, request=["execute"], response=RequestResponse.from_bool(True), ), @@ -168,14 +168,14 @@ def test_action_penalty(): assert reward_value == -0.75 - # Assert that no penalty applied for a do_nothing action + # Assert that no penalty applied for a do-nothing action reward_value = Penalty.calculate( state={}, last_action_response=AgentHistoryItem( timestep=0, - action="do_nothing", + action="do-nothing", parameters={}, - request=["do_nothing"], + request=["do-nothing"], response=RequestResponse.from_bool(True), ), ) @@ -192,12 +192,12 @@ def test_action_penalty_e2e(game_and_agent: tuple[PrimaiteGame, ControlledAgent] agent.reward_function.register_component(comp, 1.0) - action = ("do_nothing", {}) + action = ("do-nothing", {}) agent.store_action(action) game.step() assert agent.reward_function.current_reward == 0.125 - action = ("node_file_scan", {"node_name": "client", "folder_name": "downloads", "file_name": "document.pdf"}) + action = ("node-file-scan", {"node_name": "client", "folder_name": "downloads", "file_name": "document.pdf"}) agent.store_action(action) game.step() assert agent.reward_function.current_reward == -0.75 diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py index 36c77fe1..479473d1 100644 --- a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -19,11 +19,11 @@ def test_wireless_link_loading(wireless_wan_network): airspace = router_1.airspace client.software_manager.install(FTPClient) - ftp_client: FTPClient = client.software_manager.software.get("FTPClient") + ftp_client: FTPClient = client.software_manager.software.get("ftp-client") ftp_client.start() server.software_manager.install(FTPServer) - ftp_server: FTPServer = server.software_manager.software.get("FTPServer") + ftp_server: FTPServer = server.software_manager.software.get("ftp-server") ftp_server.start() client.file_system.create_file(file_name="mixtape", size=10 * 10**6, file_type=FileType.MP3, folder_name="music") diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index d2ec06ae..ab944564 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -15,13 +15,13 @@ from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP from primaite.utils.validation.port import PORT_LOOKUP -class BroadcastTestService(Service, discriminator="BroadcastTestService"): +class BroadcastTestService(Service, discriminator="broadcast-test-service"): """A service for sending broadcast and unicast messages over a network.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for BroadcastTestService.""" - type: str = "BroadcastTestService" + type: str = "broadcast-test-service" config: "BroadcastTestService.ConfigSchema" = Field(default_factory=lambda: BroadcastTestService.ConfigSchema()) @@ -51,13 +51,13 @@ class BroadcastTestService(Service, discriminator="BroadcastTestService"): ) -class BroadcastTestClient(Application, discriminator="BroadcastTestClient"): +class BroadcastTestClient(Application, discriminator="broadcast-test-client"): """A client application to receive broadcast and unicast messages.""" class ConfigSchema(Service.ConfigSchema): """ConfigSchema for BroadcastTestClient.""" - type: str = "BroadcastTestClient" + type: str = "broadcast-test-client" config: ConfigSchema = Field(default_factory=lambda: BroadcastTestClient.ConfigSchema()) @@ -65,7 +65,7 @@ class BroadcastTestClient(Application, discriminator="BroadcastTestClient"): def __init__(self, **kwargs): # Set default client properties - kwargs["name"] = "BroadcastTestClient" + kwargs["name"] = "broadcast-test-client" kwargs["port"] = PORT_LOOKUP["HTTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -93,7 +93,7 @@ def broadcast_network() -> Network: ) client_1.power_on() client_1.software_manager.install(BroadcastTestClient) - application_1 = client_1.software_manager.software["BroadcastTestClient"] + application_1 = client_1.software_manager.software["broadcast-test-client"] application_1.run() client_2 = Computer( @@ -105,7 +105,7 @@ def broadcast_network() -> Network: ) client_2.power_on() client_2.software_manager.install(BroadcastTestClient) - application_2 = client_2.software_manager.software["BroadcastTestClient"] + application_2 = client_2.software_manager.software["broadcast-test-client"] application_2.run() server_1 = Server( @@ -136,13 +136,13 @@ def broadcast_service_and_clients( broadcast_network, ) -> Tuple[BroadcastTestService, BroadcastTestClient, BroadcastTestClient]: client_1: BroadcastTestClient = broadcast_network.get_node_by_hostname("client_1").software_manager.software[ - "BroadcastTestClient" + "broadcast-test-client" ] client_2: BroadcastTestClient = broadcast_network.get_node_by_hostname("client_2").software_manager.software[ - "BroadcastTestClient" + "broadcast-test-client" ] service: BroadcastTestService = broadcast_network.get_node_by_hostname("server_1").software_manager.software[ - "BroadcastService" + "broadcast-service" ] return service, client_1, client_2 diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index b32d9657..ea7fbc99 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -17,7 +17,7 @@ def test_capture_nmne(uc2_network: Network): of the "DELETE" SQL command as a malicious network event. """ web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa + db_client: DatabaseClient = web_server.software_manager.software["database-client"] # noqa db_client_connection: DatabaseClientConnection = db_client.get_new_connection() db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa @@ -94,7 +94,7 @@ def test_describe_state_nmne(uc2_network: Network): only shows MNEs since the last time describe_state was called. """ web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa + db_client: DatabaseClient = web_server.software_manager.software["database-client"] # noqa db_client_connection: DatabaseClientConnection = db_client.get_new_connection() db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa @@ -208,7 +208,7 @@ def test_capture_nmne_observations(uc2_network: Network): sim.network = uc2_network web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software["database-client"] db_client_connection: DatabaseClientConnection = db_client.get_new_connection() # Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs diff --git a/tests/integration_tests/network/test_firewall.py b/tests/integration_tests/network/test_firewall.py index 24fbfd05..ec0200a2 100644 --- a/tests/integration_tests/network/test_firewall.py +++ b/tests/integration_tests/network/test_firewall.py @@ -90,7 +90,7 @@ def dmz_external_internal_network() -> Network: ) external_node.power_on() external_node.software_manager.install(NTPServer) - ntp_service: NTPServer = external_node.software_manager.software["NTPServer"] + ntp_service: NTPServer = external_node.software_manager.software["ntp-server"] ntp_service.start() # connect external node to firewall node network.connect(endpoint_b=external_node.network_interface[1], endpoint_a=firewall_node.external_port) @@ -105,7 +105,7 @@ def dmz_external_internal_network() -> Network: ) internal_node.power_on() internal_node.software_manager.install(NTPClient) - internal_ntp_client: NTPClient = internal_node.software_manager.software["NTPClient"] + internal_ntp_client: NTPClient = internal_node.software_manager.software["ntp-client"] internal_ntp_client.configure(external_node.network_interface[1].ip_address) internal_ntp_client.start() # connect external node to firewall node @@ -120,7 +120,7 @@ def dmz_external_internal_network() -> Network: start_up_duration=0, ) dmz_node.power_on() - dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"] + dmz_ntp_client: NTPClient = dmz_node.software_manager.software["ntp-client"] dmz_ntp_client.configure(external_node.network_interface[1].ip_address) dmz_ntp_client.start() # connect external node to firewall node @@ -214,8 +214,8 @@ def test_service_blocked(dmz_external_internal_network): firewall = dmz_external_internal_network.get_node_by_hostname("firewall_1") internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node") dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node") - internal_ntp_client: NTPClient = internal_node.software_manager.software["NTPClient"] - dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"] + internal_ntp_client: NTPClient = internal_node.software_manager.software["ntp-client"] + dmz_ntp_client: NTPClient = dmz_node.software_manager.software["ntp-client"] assert not internal_ntp_client.time @@ -261,8 +261,8 @@ def test_service_allowed_with_rule(dmz_external_internal_network): firewall = dmz_external_internal_network.get_node_by_hostname("firewall_1") internal_node = dmz_external_internal_network.get_node_by_hostname("internal_node") dmz_node = dmz_external_internal_network.get_node_by_hostname("dmz_node") - internal_ntp_client: NTPClient = internal_node.software_manager.software["NTPClient"] - dmz_ntp_client: NTPClient = dmz_node.software_manager.software["NTPClient"] + internal_ntp_client: NTPClient = internal_node.software_manager.software["ntp-client"] + dmz_ntp_client: NTPClient = dmz_node.software_manager.software["ntp-client"] assert not internal_ntp_client.time diff --git a/tests/integration_tests/network/test_multi_lan_internet_example_network.py b/tests/integration_tests/network/test_multi_lan_internet_example_network.py index ea7e1c45..c58d79a4 100644 --- a/tests/integration_tests/network/test_multi_lan_internet_example_network.py +++ b/tests/integration_tests/network/test_multi_lan_internet_example_network.py @@ -12,7 +12,7 @@ def test_all_with_configured_dns_server_ip_can_resolve_url(): network = multi_lan_internet_network_example() for node in network.nodes.values(): - dns_client: DNSClient = node.software_manager.software.get("DNSClient") + dns_client: DNSClient = node.software_manager.software.get("dns-client") if not dns_client: continue @@ -24,8 +24,8 @@ def test_all_with_configured_dns_server_ip_can_resolve_url(): def test_external_pcs_can_access_sometech_website(): network = multi_lan_internet_network_example() - pc_1_browser: WebBrowser = network.get_node_by_hostname("pc_1").software_manager.software["WebBrowser"] - pc_2_browser: WebBrowser = network.get_node_by_hostname("pc_2").software_manager.software["WebBrowser"] + pc_1_browser: WebBrowser = network.get_node_by_hostname("pc_1").software_manager.software["web-browser"] + pc_2_browser: WebBrowser = network.get_node_by_hostname("pc_2").software_manager.software["web-browser"] assert pc_1_browser.get_webpage() assert pc_2_browser.get_webpage() @@ -34,8 +34,8 @@ def test_external_pcs_can_access_sometech_website(): def test_external_pcs_cannot_access_sometech_db(): network = multi_lan_internet_network_example() - pc_1_db_client: DatabaseClient = network.get_node_by_hostname("pc_1").software_manager.software["DatabaseClient"] - pc_2_db_client: DatabaseClient = network.get_node_by_hostname("pc_2").software_manager.software["DatabaseClient"] + pc_1_db_client: DatabaseClient = network.get_node_by_hostname("pc_1").software_manager.software["database-client"] + pc_2_db_client: DatabaseClient = network.get_node_by_hostname("pc_2").software_manager.software["database-client"] assert not pc_1_db_client.get_new_connection() assert not pc_2_db_client.get_new_connection() @@ -47,8 +47,8 @@ def test_external_pcs_cannot_access_ftp_on_sometech_storage_server(): some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv") some_tech_storage_srv.file_system.create_file(file_name="test.png") - pc_1_ftp_client: FTPClient = network.get_node_by_hostname("pc_1").software_manager.software["FTPClient"] - pc_2_ftp_client: FTPClient = network.get_node_by_hostname("pc_2").software_manager.software["FTPClient"] + pc_1_ftp_client: FTPClient = network.get_node_by_hostname("pc_1").software_manager.software["ftp-client"] + pc_2_ftp_client: FTPClient = network.get_node_by_hostname("pc_2").software_manager.software["ftp-client"] assert not pc_1_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -71,7 +71,7 @@ def test_sometech_webserver_can_access_sometech_db_server(): network = multi_lan_internet_network_example() web_db_client: DatabaseClient = network.get_node_by_hostname("some_tech_web_srv").software_manager.software[ - "DatabaseClient" + "database-client" ] assert web_db_client.get_new_connection() @@ -85,7 +85,7 @@ def test_sometech_webserver_cannot_access_ftp_on_sometech_storage_server(): web_server: Server = network.get_node_by_hostname("some_tech_web_srv") web_server.software_manager.install(FTPClient) - web_ftp_client: FTPClient = web_server.software_manager.software["FTPClient"] + web_ftp_client: FTPClient = web_server.software_manager.software["ftp-client"] assert not web_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -101,13 +101,13 @@ def test_sometech_dev_pcs_can_access_sometech_website(): some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc") - snr_dev_browser: WebBrowser = some_tech_snr_dev_pc.software_manager.software["WebBrowser"] + snr_dev_browser: WebBrowser = some_tech_snr_dev_pc.software_manager.software["web-browser"] assert snr_dev_browser.get_webpage() some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc") - jnr_dev_browser: WebBrowser = some_tech_jnr_dev_pc.software_manager.software["WebBrowser"] + jnr_dev_browser: WebBrowser = some_tech_jnr_dev_pc.software_manager.software["web-browser"] assert jnr_dev_browser.get_webpage() @@ -116,12 +116,12 @@ def test_sometech_dev_pcs_can_connect_to_sometech_db_server(): network = multi_lan_internet_network_example() some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc") - snr_dev_db_client: DatabaseClient = some_tech_snr_dev_pc.software_manager.software["DatabaseClient"] + snr_dev_db_client: DatabaseClient = some_tech_snr_dev_pc.software_manager.software["database-client"] assert snr_dev_db_client.get_new_connection() some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc") - jnr_dev_db_client: DatabaseClient = some_tech_jnr_dev_pc.software_manager.software["DatabaseClient"] + jnr_dev_db_client: DatabaseClient = some_tech_jnr_dev_pc.software_manager.software["database-client"] assert jnr_dev_db_client.get_new_connection() @@ -133,7 +133,7 @@ def test_sometech_snr_dev_can_access_ftp_on_sometech_storage_server(): some_tech_storage_srv.file_system.create_file(file_name="test.png") some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc") - snr_dev_ftp_client: FTPClient = some_tech_snr_dev_pc.software_manager.software["FTPClient"] + snr_dev_ftp_client: FTPClient = some_tech_snr_dev_pc.software_manager.software["ftp-client"] assert snr_dev_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -151,7 +151,7 @@ def test_sometech_jnr_dev_cannot_access_ftp_on_sometech_storage_server(): some_tech_storage_srv.file_system.create_file(file_name="test.png") some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc") - jnr_dev_ftp_client: FTPClient = some_tech_jnr_dev_pc.software_manager.software["FTPClient"] + jnr_dev_ftp_client: FTPClient = some_tech_jnr_dev_pc.software_manager.software["ftp-client"] assert not jnr_dev_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -167,7 +167,7 @@ def test_sometech_hr_pc_can_access_sometech_website(): some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1") - hr_browser: WebBrowser = some_tech_hr_pc.software_manager.software["WebBrowser"] + hr_browser: WebBrowser = some_tech_hr_pc.software_manager.software["web-browser"] assert hr_browser.get_webpage() @@ -177,7 +177,7 @@ def test_sometech_hr_pc_cannot_access_sometech_db(): some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1") - hr_db_client: DatabaseClient = some_tech_hr_pc.software_manager.software["DatabaseClient"] + hr_db_client: DatabaseClient = some_tech_hr_pc.software_manager.software["database-client"] assert not hr_db_client.get_new_connection() @@ -189,7 +189,7 @@ def test_sometech_hr_pc_cannot_access_ftp_on_sometech_storage_server(): some_tech_storage_srv.file_system.create_file(file_name="test.png") some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1") - hr_ftp_client: FTPClient = some_tech_hr_pc.software_manager.software["FTPClient"] + hr_ftp_client: FTPClient = some_tech_hr_pc.software_manager.software["ftp-client"] assert not hr_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, diff --git a/tests/integration_tests/network/test_routing.py b/tests/integration_tests/network/test_routing.py index 948b409f..7d23a2a6 100644 --- a/tests/integration_tests/network/test_routing.py +++ b/tests/integration_tests/network/test_routing.py @@ -188,11 +188,11 @@ def test_routing_services(multi_hop_network): pc_b = multi_hop_network.get_node_by_hostname("pc_b") pc_a.software_manager.install(NTPClient) - ntp_client = pc_a.software_manager.software["NTPClient"] + ntp_client = pc_a.software_manager.software["ntp-client"] ntp_client.start() pc_b.software_manager.install(NTPServer) - pc_b.software_manager.software["NTPServer"].start() + pc_b.software_manager.software["ntp-server"].start() ntp_client.configure(ntp_server_ip_address=pc_b.network_interface[1].ip_address) diff --git a/tests/integration_tests/network/test_users_creation_from_config.py b/tests/integration_tests/network/test_users_creation_from_config.py index 1963b1dd..5340c369 100644 --- a/tests/integration_tests/network/test_users_creation_from_config.py +++ b/tests/integration_tests/network/test_users_creation_from_config.py @@ -15,7 +15,7 @@ def test_users_from_config(): client_1 = network.get_node_by_hostname("client_1") - user_manager: UserManager = client_1.software_manager.software["UserManager"] + user_manager: UserManager = client_1.software_manager.software["user-manager"] assert len(user_manager.users) == 3 diff --git a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py index faf0466f..5475edd1 100644 --- a/tests/integration_tests/system/red_applications/test_c2_suite_integration.py +++ b/tests/integration_tests/system/red_applications/test_c2_suite_integration.py @@ -99,15 +99,15 @@ def basic_network() -> Network: def setup_c2(given_network: Network): """Installs the C2 Beacon & Server, configures and then returns.""" computer_a: Computer = given_network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + c2_server: C2Server = computer_a.software_manager.software.get("c2-server") computer_a.software_manager.install(DatabaseService) - computer_a.software_manager.software["DatabaseService"].start() + computer_a.software_manager.software["database-service"].start() computer_b: Computer = given_network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("c2-beacon") computer_b.software_manager.install(DatabaseClient) - computer_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.2")) - computer_b.software_manager.software["DatabaseClient"].run() + computer_b.software_manager.software["database-client"].configure(server_ip_address=IPv4Address("192.168.0.2")) + computer_b.software_manager.software["database-client"].run() c2_beacon.configure(c2_server_ip_address="192.168.0.2", keep_alive_frequency=2) c2_server.run() @@ -177,9 +177,9 @@ def test_c2_suite_configure_request(basic_network): "masquerade_port": 80, } - network.apply_request(["node", "node_b", "application", "C2Beacon", "configure", c2_beacon_config]) + network.apply_request(["node", "node_b", "application", "c2-beacon", "configure", c2_beacon_config]) network.apply_timestep(0) - network.apply_request(["node", "node_b", "application", "C2Beacon", "execute"]) + network.apply_request(["node", "node_b", "application", "c2-beacon", "execute"]) assert c2_beacon.c2_connection_active is True assert c2_server.c2_connection_active is True @@ -195,13 +195,13 @@ def test_c2_suite_ransomware_commands(basic_network): # Testing Via Requests: computer_b.software_manager.install(software_class=RansomwareScript) ransomware_config = {"server_ip_address": "192.168.0.2"} - network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_configure", ransomware_config]) + network.apply_request(["node", "node_a", "application", "c2-server", "ransomware_configure", ransomware_config]) - ransomware_script: RansomwareScript = computer_b.software_manager.software["RansomwareScript"] + ransomware_script: RansomwareScript = computer_b.software_manager.software["ransomware-script"] assert ransomware_script.server_ip_address == "192.168.0.2" - network.apply_request(["node", "node_a", "application", "C2Server", "ransomware_launch"]) + network.apply_request(["node", "node_a", "application", "c2-server", "ransomware_launch"]) database_file = computer_a.software_manager.file_system.get_file("database", "database.db") @@ -491,10 +491,10 @@ def test_c2_suite_yaml(): yaml_network = game.simulation.network computer_a: Computer = yaml_network.get_node_by_hostname("node_a") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + c2_server: C2Server = computer_a.software_manager.software.get("c2-server") computer_b: Computer = yaml_network.get_node_by_hostname("node_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("c2-beacon") c2_beacon.configure( c2_server_ip_address=c2_beacon.config.c2_server_ip_address, keep_alive_frequency=c2_beacon.config.keep_alive_frequency, diff --git a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py index 3ef6469e..1a85b20d 100644 --- a/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_data_manipulation_bot_and_server.py @@ -27,20 +27,20 @@ def data_manipulation_bot_and_db_server(client_server) -> Tuple[DataManipulation # install db client on computer computer.software_manager.install(DatabaseClient) - db_client: DatabaseClient = computer.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = computer.software_manager.software.get("database-client") db_client.run() # Install DoSBot on computer computer.software_manager.install(DataManipulationBot) - data_manipulation_bot: DataManipulationBot = computer.software_manager.software.get("DataManipulationBot") + data_manipulation_bot: DataManipulationBot = computer.software_manager.software.get("data-manipulation-bot") data_manipulation_bot.configure( server_ip_address=IPv4Address(server.network_interface[1].ip_address), payload="DELETE" ) # Install DB Server service on server server.software_manager.install(DatabaseService) - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") db_server_service.start() return data_manipulation_bot, computer, db_server_service, server @@ -64,26 +64,26 @@ def data_manipulation_db_server_green_client(example_network) -> Network: # install db client on client 1 client_1.software_manager.install(DatabaseClient) - db_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = client_1.software_manager.software.get("database-client") db_client.run() # install Data Manipulation bot on client 1 client_1.software_manager.install(DataManipulationBot) - data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") + data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("data-manipulation-bot") data_manipulation_bot.configure( server_ip_address=IPv4Address(server.network_interface[1].ip_address), payload="DELETE" ) # install db server service on server server.software_manager.install(DatabaseService) - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") db_server_service.start() # Install DB client (green) on client 2 client_2.software_manager.install(DatabaseClient) - database_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client_2.software_manager.software.get("database-client") database_client.configure(server_ip_address=IPv4Address(server.network_interface[1].ip_address)) database_client.run() @@ -137,13 +137,13 @@ def test_data_manipulation_disrupts_green_agent_connection(data_manipulation_db_ network: Network = data_manipulation_db_server_green_client client_1: Computer = network.get_node_by_hostname("client_1") - data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") + data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("data-manipulation-bot") client_2: Computer = network.get_node_by_hostname("client_2") - green_db_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + green_db_client: DatabaseClient = client_2.software_manager.software.get("database-client") server: Server = network.get_node_by_hostname("server_1") - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") green_db_connection: DatabaseClientConnection = green_db_client.get_new_connection() diff --git a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py index cb0195f0..47ddb504 100644 --- a/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py +++ b/tests/integration_tests/system/red_applications/test_dos_bot_and_server.py @@ -23,7 +23,7 @@ def dos_bot_and_db_server(client_server) -> Tuple[DoSBot, Computer, DatabaseServ # Install DoSBot on computer computer.software_manager.install(DoSBot) - dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") + dos_bot: DoSBot = computer.software_manager.software.get("dos-bot") dos_bot.configure( target_ip_address=IPv4Address(server.network_interface[1].ip_address), target_port=PORT_LOOKUP["POSTGRES_SERVER"], @@ -31,7 +31,7 @@ def dos_bot_and_db_server(client_server) -> Tuple[DoSBot, Computer, DatabaseServ # Install DB Server service on server server.software_manager.install(DatabaseService) - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") db_server_service.start() return dos_bot, computer, db_server_service, server @@ -56,7 +56,7 @@ def dos_bot_db_server_green_client(example_network) -> Network: # install DoS bot on client 1 client_1.software_manager.install(DoSBot) - dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot") + dos_bot: DoSBot = client_1.software_manager.software.get("dos-bot") dos_bot.configure( target_ip_address=IPv4Address(server.network_interface[1].ip_address), target_port=PORT_LOOKUP["POSTGRES_SERVER"], @@ -64,13 +64,13 @@ def dos_bot_db_server_green_client(example_network) -> Network: # install db server service on server server.software_manager.install(DatabaseService) - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") db_server_service.start() # Install DB client (green) on client 2 client_2.software_manager.install(DatabaseClient) - database_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client_2.software_manager.software.get("database-client") database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) database_client.run() @@ -159,13 +159,13 @@ def test_dos_blocks_green_agent_connection(dos_bot_db_server_green_client): network: Network = dos_bot_db_server_green_client client_1: Computer = network.get_node_by_hostname("client_1") - dos_bot: DoSBot = client_1.software_manager.software.get("DoSBot") + dos_bot: DoSBot = client_1.software_manager.software.get("dos-bot") client_2: Computer = network.get_node_by_hostname("client_2") - green_db_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + green_db_client: DatabaseClient = client_2.software_manager.software.get("database-client") server: Server = network.get_node_by_hostname("server_1") - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") assert db_server_service.health_state_actual is SoftwareHealthState.GOOD diff --git a/tests/integration_tests/system/red_applications/test_ransomware_script.py b/tests/integration_tests/system/red_applications/test_ransomware_script.py index 14b83e6a..a62b2fb6 100644 --- a/tests/integration_tests/system/red_applications/test_ransomware_script.py +++ b/tests/integration_tests/system/red_applications/test_ransomware_script.py @@ -22,20 +22,20 @@ def ransomware_script_and_db_server(client_server) -> Tuple[RansomwareScript, Co # install db client on computer computer.software_manager.install(DatabaseClient) - db_client: DatabaseClient = computer.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = computer.software_manager.software.get("database-client") db_client.run() # Install DoSBot on computer computer.software_manager.install(RansomwareScript) - ransomware_script_application: RansomwareScript = computer.software_manager.software.get("RansomwareScript") + ransomware_script_application: RansomwareScript = computer.software_manager.software.get("ransomware-script") ransomware_script_application.configure( server_ip_address=IPv4Address(server.network_interface[1].ip_address), payload="ENCRYPT" ) # Install DB Server service on server server.software_manager.install(DatabaseService) - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") db_server_service.start() return ransomware_script_application, computer, db_server_service, server @@ -59,26 +59,26 @@ def ransomware_script_db_server_green_client(example_network) -> Network: # install db client on client 1 client_1.software_manager.install(DatabaseClient) - db_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = client_1.software_manager.software.get("database-client") db_client.run() # install Ransomware Script bot on client 1 client_1.software_manager.install(RansomwareScript) - ransomware_script_application: RansomwareScript = client_1.software_manager.software.get("RansomwareScript") + ransomware_script_application: RansomwareScript = client_1.software_manager.software.get("ransomware-script") ransomware_script_application.configure( server_ip_address=IPv4Address(server.network_interface[1].ip_address), payload="ENCRYPT" ) # install db server service on server server.software_manager.install(DatabaseService) - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") db_server_service.start() # Install DB client (green) on client 2 client_2.software_manager.install(DatabaseClient) - database_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client_2.software_manager.software.get("database-client") database_client.configure(server_ip_address=IPv4Address(server.network_interface[1].ip_address)) database_client.run() @@ -110,15 +110,15 @@ def test_ransomware_disrupts_green_agent_connection(ransomware_script_db_server_ network: Network = ransomware_script_db_server_green_client client_1: Computer = network.get_node_by_hostname("client_1") - ransomware_script_application: RansomwareScript = client_1.software_manager.software.get("RansomwareScript") + ransomware_script_application: RansomwareScript = client_1.software_manager.software.get("ransomware-script") client_2: Computer = network.get_node_by_hostname("client_2") - green_db_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") + green_db_client: DatabaseClient = client_2.software_manager.software.get("database-client") green_db_client.connect() green_db_client_connection: DatabaseClientConnection = green_db_client.get_new_connection() server: Server = network.get_node_by_hostname("server_1") - db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_server_service: DatabaseService = server.software_manager.software.get("database-service") assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD assert green_db_client.query("SELECT") is True diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index fc7aa69c..04fbb298 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -21,7 +21,7 @@ def populated_node(application_class) -> Tuple[Application, Computer]: computer.power_on() computer.software_manager.install(application_class) - app = computer.software_manager.software.get("DummyApplication") + app = computer.software_manager.software.get("dummy-application") app.run() return app, computer @@ -39,7 +39,7 @@ def test_application_on_offline_node(application_class): ) computer.software_manager.install(application_class) - app: Application = computer.software_manager.software.get("DummyApplication") + app: Application = computer.software_manager.software.get("dummy-application") computer.power_off() diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index bb25f8c8..7c3ded2b 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -31,11 +31,11 @@ def peer_to_peer() -> Tuple[Computer, Computer]: assert node_a.ping("192.168.0.11") node_a.software_manager.install(DatabaseClient) - node_a.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11")) - node_a.software_manager.software["DatabaseClient"].run() + node_a.software_manager.software["database-client"].configure(server_ip_address=IPv4Address("192.168.0.11")) + node_a.software_manager.software["database-client"].run() node_b.software_manager.install(DatabaseService) - database_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa + database_service: DatabaseService = node_b.software_manager.software["database-service"] # noqa database_service.start() return node_a, node_b @@ -44,7 +44,7 @@ def peer_to_peer() -> Tuple[Computer, Computer]: def peer_to_peer_secure_db(peer_to_peer) -> Tuple[Computer, Computer]: node_a, node_b = peer_to_peer - database_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa + database_service: DatabaseService = node_b.software_manager.software["database-service"] # noqa database_service.stop() database_service.password = "12345" database_service.start() @@ -54,9 +54,9 @@ def peer_to_peer_secure_db(peer_to_peer) -> Tuple[Computer, Computer]: def test_database_client_server_connection(peer_to_peer): node_a, node_b = peer_to_peer - db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = node_a.software_manager.software["database-client"] - db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] + db_service: DatabaseService = node_b.software_manager.software["database-service"] db_client.connect() @@ -71,9 +71,9 @@ def test_database_client_server_connection(peer_to_peer): def test_database_client_server_correct_password(peer_to_peer_secure_db): node_a, node_b = peer_to_peer_secure_db - db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = node_a.software_manager.software["database-client"] - db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] + db_service: DatabaseService = node_b.software_manager.software["database-service"] db_client.configure(server_ip_address=IPv4Address("192.168.0.11"), server_password="12345") db_client.connect() @@ -84,9 +84,9 @@ def test_database_client_server_correct_password(peer_to_peer_secure_db): def test_database_client_server_incorrect_password(peer_to_peer_secure_db): node_a, node_b = peer_to_peer_secure_db - db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = node_a.software_manager.software["database-client"] - db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] + db_service: DatabaseService = node_b.software_manager.software["database-service"] # should fail db_client.connect() @@ -102,7 +102,7 @@ def test_database_client_server_incorrect_password(peer_to_peer_secure_db): def test_database_client_native_connection_query(uc2_network): """Tests DB query across the network returns HTTP status 200 and date.""" web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software["database-client"] db_client.connect() assert db_client.query(sql="SELECT") assert db_client.query(sql="INSERT") @@ -111,7 +111,7 @@ def test_database_client_native_connection_query(uc2_network): def test_database_client_connection_query(uc2_network): """Tests DB query across the network returns HTTP status 200 and date.""" web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software["database-client"] db_connection: DatabaseClientConnection = db_client.get_new_connection() @@ -122,13 +122,13 @@ def test_database_client_connection_query(uc2_network): def test_create_database_backup(uc2_network): """Run the backup_database method and check if the FTP server has the relevant file.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] # back up should be created assert db_service.backup_database() is True backup_server: Server = uc2_network.get_node_by_hostname("backup_server") - ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_server: FTPServer = backup_server.software_manager.software["ftp-server"] # backup file should exist in the backup server assert ftp_server.file_system.get_file(folder_name=db_service.uuid, file_name="database.db") is not None @@ -137,7 +137,7 @@ def test_create_database_backup(uc2_network): def test_restore_backup(uc2_network): """Run the restore_backup method and check if the backup is properly restored.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] # create a back up assert db_service.backup_database() is True @@ -156,7 +156,7 @@ def test_restore_backup(uc2_network): def test_restore_backup_without_updating_scan(uc2_network): """Same test as restore backup but the file is previously seen as corrupted.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] # create a back up assert db_service.backup_database() is True @@ -184,7 +184,7 @@ def test_restore_backup_without_updating_scan(uc2_network): def test_restore_backup_after_deleting_file_without_updating_scan(uc2_network): """Same test as restore backup but the file is previously seen as corrupted.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] assert db_service.backup_database() is True @@ -217,7 +217,7 @@ def test_restore_backup_after_deleting_file_without_updating_scan(uc2_network): def test_database_service_fix(uc2_network): """Test that the software fix applies to database service.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] assert db_service.backup_database() is True @@ -242,10 +242,10 @@ def test_database_service_fix(uc2_network): def test_database_cannot_be_queried_while_fixing(uc2_network): """Tests that the database service cannot be queried if the service is being fixed.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software["database-client"] db_connection: DatabaseClientConnection = db_client.get_new_connection() @@ -279,10 +279,10 @@ def test_database_cannot_be_queried_while_fixing(uc2_network): def test_database_can_create_connection_while_fixing(uc2_network): """Tests that connections cannot be created while the database is being fixed.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software["database-service"] client_2: Server = uc2_network.get_node_by_hostname("client_2") - db_client: DatabaseClient = client_2.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = client_2.software_manager.software["database-client"] db_connection: DatabaseClientConnection = db_client.get_new_connection() @@ -321,13 +321,13 @@ def test_database_can_create_connection_while_fixing(uc2_network): def test_database_client_cannot_query_offline_database_server(uc2_network): """Tests DB query across the network returns HTTP status 404 when db server is offline.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") + db_service: DatabaseService = db_server.software_manager.software.get("database-service") assert db_server.operating_state is NodeOperatingState.ON assert db_service.operating_state is ServiceOperatingState.RUNNING web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = web_server.software_manager.software.get("database-client") db_client.connect() assert len(db_client.client_connections) @@ -351,8 +351,8 @@ def test_database_client_cannot_query_offline_database_server(uc2_network): def test_database_client_uninstall_terminates_connections(peer_to_peer): node_a, node_b = peer_to_peer - db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"] - db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa + db_client: DatabaseClient = node_a.software_manager.software["database-client"] + db_service: DatabaseService = node_b.software_manager.software["database-service"] # noqa db_connection: DatabaseClientConnection = db_client.get_new_connection() @@ -366,7 +366,7 @@ def test_database_client_uninstall_terminates_connections(peer_to_peer): assert db_connection.query("SELECT") # Perform the DatabaseClient uninstall - node_a.software_manager.uninstall("DatabaseClient") + node_a.software_manager.uninstall("database-client") # Check that all connection counters are updated accordingly and client connection can no longer query the database assert len(db_service.connections) == 0 @@ -381,8 +381,8 @@ def test_database_client_uninstall_terminates_connections(peer_to_peer): def test_database_service_can_terminate_connection(peer_to_peer): node_a, node_b = peer_to_peer - db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"] - db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa + db_client: DatabaseClient = node_a.software_manager.software["database-client"] + db_service: DatabaseService = node_b.software_manager.software["database-service"] # noqa db_connection: DatabaseClientConnection = db_client.get_new_connection() @@ -418,7 +418,7 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti db_server.power_on() db_server.software_manager.install(DatabaseService) - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] # noqa + db_service: DatabaseService = db_server.software_manager.software["database-service"] # noqa db_service.start() client_a = Computer( @@ -427,8 +427,8 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti client_a.power_on() client_a.software_manager.install(DatabaseClient) - client_a.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11")) - client_a.software_manager.software["DatabaseClient"].run() + client_a.software_manager.software["database-client"].configure(server_ip_address=IPv4Address("192.168.0.11")) + client_a.software_manager.software["database-client"].run() client_b = Computer( hostname="client_b", ip_address="192.168.0.13", subnet_mask="255.255.255.0", start_up_duration=0 @@ -436,8 +436,8 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti client_b.power_on() client_b.software_manager.install(DatabaseClient) - client_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11")) - client_b.software_manager.software["DatabaseClient"].run() + client_b.software_manager.software["database-client"].configure(server_ip_address=IPv4Address("192.168.0.11")) + client_b.software_manager.software["database-client"].run() switch = Switch(hostname="switch", start_up_duration=0, num_ports=3) switch.power_on() @@ -446,13 +446,13 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti network.connect(endpoint_a=switch.network_interface[2], endpoint_b=client_a.network_interface[1]) network.connect(endpoint_a=switch.network_interface[3], endpoint_b=client_b.network_interface[1]) - db_client_a: DatabaseClient = client_a.software_manager.software["DatabaseClient"] # noqa + db_client_a: DatabaseClient = client_a.software_manager.software["database-client"] # noqa db_connection_a = db_client_a.get_new_connection() assert db_connection_a.query("SELECT") assert len(db_service.connections) == 1 - db_client_b: DatabaseClient = client_b.software_manager.software["DatabaseClient"] # noqa + db_client_b: DatabaseClient = client_b.software_manager.software["database-client"] # noqa db_connection_b = db_client_b.get_new_connection() assert db_connection_b.query("SELECT") @@ -467,4 +467,4 @@ def test_client_connection_terminate_does_not_terminate_another_clients_connecti def test_database_server_install_ftp_client(): server = Server(hostname="db_server", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) server.software_manager.install(DatabaseService) - assert server.software_manager.software.get("FTPClient") + assert server.software_manager.software.get("ftp-client") diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 38caf1a2..863c8c12 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -18,14 +18,14 @@ def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSSe # Install DNS Client on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software.get("DNSClient") + dns_client: DNSClient = computer.software_manager.software.get("dns-client") dns_client.start() # set server as DNS Server dns_client.dns_server = IPv4Address(server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address) # Install DNS Server on server server.software_manager.install(DNSServer) - dns_server: DNSServer = server.software_manager.software.get("DNSServer") + dns_server: DNSServer = server.software_manager.software.get("dns-server") dns_server.start() # register arcd.com as a domain dns_server.dns_register( diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index fa4df0a9..97b9049c 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -17,12 +17,12 @@ def ftp_client_and_ftp_server(client_server) -> Tuple[FTPClient, Computer, FTPSe # Install FTP Client service on computer computer.software_manager.install(FTPClient) - ftp_client: FTPClient = computer.software_manager.software.get("FTPClient") + ftp_client: FTPClient = computer.software_manager.software.get("ftp-client") ftp_client.start() # Install FTP Server service on server server.software_manager.install(FTPServer) - ftp_server: FTPServer = server.software_manager.software.get("FTPServer") + ftp_server: FTPServer = server.software_manager.software.get("ftp-server") ftp_server.start() return ftp_client, computer, ftp_server, server diff --git a/tests/integration_tests/system/test_nmap.py b/tests/integration_tests/system/test_nmap.py index d1925a94..e5f08a94 100644 --- a/tests/integration_tests/system/test_nmap.py +++ b/tests/integration_tests/system/test_nmap.py @@ -15,7 +15,7 @@ def test_ping_scan_all_on(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa expected_result = [IPv4Address("192.168.1.10"), IPv4Address("192.168.1.14")] actual_result = client_1_nmap.ping_scan(target_ip_address=["192.168.1.10", "192.168.1.14"]) @@ -27,7 +27,7 @@ def test_ping_scan_all_on_full_network(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa expected_result = [IPv4Address("192.168.1.1"), IPv4Address("192.168.1.10"), IPv4Address("192.168.1.14")] actual_result = client_1_nmap.ping_scan(target_ip_address=IPv4Network("192.168.1.0/24")) @@ -39,7 +39,7 @@ def test_ping_scan_some_on(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa network.get_node_by_hostname("server_2").power_off() @@ -53,7 +53,7 @@ def test_ping_scan_all_off(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa network.get_node_by_hostname("server_1").power_off() network.get_node_by_hostname("server_2").power_off() @@ -68,7 +68,7 @@ def test_port_scan_one_node_one_port(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa client_2 = network.get_node_by_hostname("client_2") @@ -99,7 +99,7 @@ def test_port_scan_full_subnet_all_ports_and_protocols(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa actual_result = client_1_nmap.port_scan( target_ip_address=IPv4Network("192.168.10.0/24"), @@ -127,7 +127,7 @@ def test_network_service_recon_all_ports_and_protocols(example_network): network = example_network client_1 = network.get_node_by_hostname("client_1") - client_1_nmap: NMAP = client_1.software_manager.software["NMAP"] # noqa + client_1_nmap: NMAP = client_1.software_manager.software["nmap"] # noqa actual_result = client_1_nmap.network_service_recon( target_ip_address=IPv4Network("192.168.10.0/24"), diff --git a/tests/integration_tests/system/test_ntp_client_server.py b/tests/integration_tests/system/test_ntp_client_server.py index 42340eb3..6c700fb8 100644 --- a/tests/integration_tests/system/test_ntp_client_server.py +++ b/tests/integration_tests/system/test_ntp_client_server.py @@ -29,12 +29,12 @@ def create_ntp_network(client_server) -> Tuple[NTPClient, Computer, NTPServer, S server.power_on() server.software_manager.install(NTPServer) - ntp_server: NTPServer = server.software_manager.software.get("NTPServer") + ntp_server: NTPServer = server.software_manager.software.get("ntp-server") ntp_server.start() client.power_on() client.software_manager.install(NTPClient) - ntp_client: NTPClient = client.software_manager.software.get("NTPClient") + ntp_client: NTPClient = client.software_manager.software.get("ntp-client") ntp_client.start() return ntp_client, client, ntp_server, server @@ -43,8 +43,8 @@ def create_ntp_network(client_server) -> Tuple[NTPClient, Computer, NTPServer, S def test_ntp_client_server(create_ntp_network): ntp_client, client, ntp_server, server = create_ntp_network - ntp_server: NTPServer = server.software_manager.software["NTPServer"] - ntp_client: NTPClient = client.software_manager.software["NTPClient"] + ntp_server: NTPServer = server.software_manager.software["ntp-server"] + ntp_client: NTPClient = client.software_manager.software["ntp-client"] assert ntp_server.operating_state == ServiceOperatingState.RUNNING assert ntp_client.operating_state == ServiceOperatingState.RUNNING @@ -64,8 +64,8 @@ def test_ntp_client_server(create_ntp_network): def test_ntp_server_failure(create_ntp_network): ntp_client, client, ntp_server, server = create_ntp_network - ntp_server: NTPServer = server.software_manager.software["NTPServer"] - ntp_client: NTPClient = client.software_manager.software["NTPClient"] + ntp_server: NTPServer = server.software_manager.software["ntp-server"] + ntp_client: NTPClient = client.software_manager.software["ntp-client"] assert ntp_client.operating_state == ServiceOperatingState.RUNNING assert ntp_client.operating_state == ServiceOperatingState.RUNNING 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 db5381d0..4bf5c7ba 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -14,11 +14,11 @@ from primaite.utils.validation.port import PORT_LOOKUP from tests import TEST_ASSETS_ROOT -class _DatabaseListener(Service, discriminator="_DatabaseListener"): +class _DatabaseListener(Service, discriminator="database-listener"): class ConfigSchema(Service.ConfigSchema): """ConfigSchema for _DatabaseListener.""" - type: str = "_DatabaseListener" + type: str = "database-listener" listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} config: "_DatabaseListener.ConfigSchema" = Field(default_factory=lambda: _DatabaseListener.ConfigSchema()) @@ -41,7 +41,7 @@ def test_http_listener(client_server): computer, server = client_server server.software_manager.install(DatabaseService) - server_db = server.software_manager.software["DatabaseService"] + server_db = server.software_manager.software["database-service"] server_db.start() server.software_manager.install(_DatabaseListener) @@ -49,7 +49,7 @@ def test_http_listener(client_server): server_db_listener.start() computer.software_manager.install(DatabaseClient) - computer_db_client: DatabaseClient = computer.software_manager.software["DatabaseClient"] + computer_db_client: DatabaseClient = computer.software_manager.software["database-client"] computer_db_client.run() computer_db_client.server_ip_address = server.network_interface[1].ip_address @@ -86,6 +86,6 @@ def test_set_listen_on_ports_from_config(): assert PORT_LOOKUP["SMB"] in client.software_manager.get_open_ports() assert PORT_LOOKUP["IPP"] in client.software_manager.get_open_ports() - web_browser = client.software_manager.software["WebBrowser"] + web_browser = client.software_manager.software["web-browser"] assert not web_browser.listen_on_ports.difference({PORT_LOOKUP["SMB"], PORT_LOOKUP["IPP"]}) diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 4e73a050..560b7770 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -23,7 +23,7 @@ def populated_node( server.power_on() server.software_manager.install(service_class) - service = server.software_manager.software.get("DummyService") + service = server.software_manager.software.get("dummy-service") service.start() return server, service @@ -42,7 +42,7 @@ def test_service_on_offline_node(service_class): computer.power_on() computer.software_manager.install(service_class) - service: Service = computer.software_manager.software.get("DummyService") + service: Service = computer.software_manager.software.get("dummy-service") computer.power_off() diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index 8aea34c1..5c79f755 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -20,23 +20,23 @@ def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebS # Install Web Browser on computer computer.software_manager.install(WebBrowser) - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") web_browser.run() # Install DNS Client service on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software.get("DNSClient") + dns_client: DNSClient = computer.software_manager.software.get("dns-client") # set dns server dns_client.dns_server = server.network_interfaces[next(iter(server.network_interfaces))].ip_address # Install Web Server service on server server.software_manager.install(WebServer) - web_server_service: WebServer = server.software_manager.software.get("WebServer") + web_server_service: WebServer = server.software_manager.software.get("web-server") web_server_service.start() # Install DNS Server service on server server.software_manager.install(DNSServer) - dns_server: DNSServer = server.software_manager.software.get("DNSServer") + dns_server: DNSServer = server.software_manager.software.get("dns-server") # register arcd.com to DNS dns_server.dns_register( domain_name="arcd.com", diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 41f1a837..e8045ed9 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -68,29 +68,29 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer, # Install DatabaseService on db server db_server.software_manager.install(DatabaseService) - db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") + db_service: DatabaseService = db_server.software_manager.software.get("database-service") db_service.start() # Install Web Browser on computer computer.software_manager.install(WebBrowser) - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") web_browser.config.target_url = "http://arcd.com/users/" web_browser.run() # Install DNS Client service on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software.get("DNSClient") + dns_client: DNSClient = computer.software_manager.software.get("dns-client") # set dns server dns_client.dns_server = web_server.network_interfaces[next(iter(web_server.network_interfaces))].ip_address # Install Web Server service on web server web_server.software_manager.install(WebServer) - web_server_service: WebServer = web_server.software_manager.software.get("WebServer") + web_server_service: WebServer = web_server.software_manager.software.get("web-server") web_server_service.start() # Install DNS Server service on web server web_server.software_manager.install(DNSServer) - dns_server: DNSServer = web_server.software_manager.software.get("DNSServer") + dns_server: DNSServer = web_server.software_manager.software.get("dns-server") # register arcd.com to DNS dns_server.dns_register( domain_name="arcd.com", @@ -99,7 +99,7 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer, # Install DatabaseClient service on web server web_server.software_manager.install(DatabaseClient) - db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") + db_client: DatabaseClient = web_server.software_manager.software.get("database-client") db_client.server_ip_address = IPv4Address(db_server_nic.ip_address) # set IP address of Database Server db_client.run() assert dns_client.check_domain_exists("arcd.com") @@ -111,7 +111,7 @@ def web_client_web_server_database(example_network) -> Tuple[Network, Computer, def test_web_client_requests_users(web_client_web_server_database): _, computer, _, _ = web_client_web_server_database - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") assert web_browser.get_webpage() @@ -121,8 +121,8 @@ def test_database_fix_disrupts_web_client(uc2_network): computer: Computer = uc2_network.get_node_by_hostname("client_1") db_server: Server = uc2_network.get_node_by_hostname("database_server") - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") - database_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") + database_service: DatabaseService = db_server.software_manager.software.get("database-service") # fix the database service database_service.fix() @@ -143,7 +143,7 @@ class TestWebBrowserHistory: def test_populating_history(self, web_client_web_server_database): network, computer, _, _ = web_client_web_server_database - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") assert web_browser.history == [] web_browser.get_webpage() assert len(web_browser.history) == 1 @@ -165,11 +165,11 @@ class TestWebBrowserHistory: def test_history_in_state(self, web_client_web_server_database): network, computer, _, _ = web_client_web_server_database - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") state = computer.describe_state() - assert "history" in state["applications"]["WebBrowser"] - assert len(state["applications"]["WebBrowser"]["history"]) == 0 + assert "history" in state["applications"]["web-browser"] + assert len(state["applications"]["web-browser"]["history"]) == 0 web_browser.get_webpage() router = network.get_node_by_hostname("router_1") @@ -179,5 +179,5 @@ class TestWebBrowserHistory: web_browser.get_webpage() state = computer.describe_state() - assert state["applications"]["WebBrowser"]["history"][0]["outcome"] == 200 - assert state["applications"]["WebBrowser"]["history"][1]["outcome"] == 404 + assert state["applications"]["web-browser"]["history"][0]["outcome"] == 200 + assert state["applications"]["web-browser"]["history"][1]["outcome"] == 404 diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index 21152199..87ad3aaf 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -48,13 +48,13 @@ def test_successful_application_requests(example_network): client_1 = net.get_node_by_hostname("client_1") client_1.software_manager.install(DummyApplication) - client_1.software_manager.software.get("DummyApplication").run() + client_1.software_manager.software.get("dummy-application").run() - resp_1 = net.apply_request(["node", "client_1", "application", "DummyApplication", "scan"]) + resp_1 = net.apply_request(["node", "client_1", "application", "dummy-application", "scan"]) assert resp_1 == RequestResponse(status="success", data={}) - resp_2 = net.apply_request(["node", "client_1", "application", "DummyApplication", "fix"]) + resp_2 = net.apply_request(["node", "client_1", "application", "dummy-application", "fix"]) assert resp_2 == RequestResponse(status="success", data={}) - resp_3 = net.apply_request(["node", "client_1", "application", "DummyApplication", "compromise"]) + resp_3 = net.apply_request(["node", "client_1", "application", "dummy-application", "compromise"]) assert resp_3 == RequestResponse(status="success", data={}) @@ -77,7 +77,7 @@ def test_successful_service_requests(example_network): "scan", "fix", ]: - resp_1 = net.apply_request(["node", "server_1", "service", "DummyService", verb]) + resp_1 = net.apply_request(["node", "server_1", "service", "dummy-service", verb]) assert resp_1 == RequestResponse(status="success", data={}) server_1.apply_timestep(timestep=1) server_1.apply_timestep(timestep=1) @@ -93,7 +93,7 @@ def test_non_existent_requests(example_network): net = example_network resp_1 = net.apply_request(["fake"]) assert resp_1.status == "unreachable" - resp_2 = net.apply_request(["network", "node", "client_39", "application", "WebBrowser", "execute"]) + resp_2 = net.apply_request(["network", "node", "client_39", "application", "web-browser", "execute"]) assert resp_2.status == "unreachable" @@ -102,8 +102,8 @@ def test_non_existent_requests(example_network): [ ["node", "client_1", "file_system", "folder", "root", "scan"], ["node", "client_1", "os", "scan"], - ["node", "client_1", "service", "DNSClient", "stop"], - ["node", "client_1", "application", "WebBrowser", "scan"], + ["node", "client_1", "service", "dns-client", "stop"], + ["node", "client_1", "application", "web-browser", "scan"], ["node", "client_1", "network_interface", 1, "disable"], ], ) @@ -128,10 +128,14 @@ class TestDataManipulationGreenRequests: """Test that green requests succeed when the node is on and fail if the node is off.""" net: Network = uc2_network - client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) - client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) - client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) - client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) + client_1_browser_execute = net.apply_request(["node", "client_1", "application", "web-browser", "execute"]) + client_1_db_client_execute = net.apply_request( + ["node", "client_1", "application", "database-client", "execute"] + ) + client_2_browser_execute = net.apply_request(["node", "client_2", "application", "web-browser", "execute"]) + client_2_db_client_execute = net.apply_request( + ["node", "client_2", "application", "database-client", "execute"] + ) assert client_1_browser_execute.status == "success" assert client_1_db_client_execute.status == "success" assert client_2_browser_execute.status == "success" @@ -145,13 +149,13 @@ class TestDataManipulationGreenRequests: client_2.shut_down_duration = 0 client_2.power_off() - client_1_browser_execute_off = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) + client_1_browser_execute_off = net.apply_request(["node", "client_1", "application", "web-browser", "execute"]) client_1_db_client_execute_off = net.apply_request( - ["node", "client_1", "application", "DatabaseClient", "execute"] + ["node", "client_1", "application", "database-client", "execute"] ) - client_2_browser_execute_off = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + client_2_browser_execute_off = net.apply_request(["node", "client_2", "application", "web-browser", "execute"]) client_2_db_client_execute_off = net.apply_request( - ["node", "client_2", "application", "DatabaseClient", "execute"] + ["node", "client_2", "application", "database-client", "execute"] ) assert client_1_browser_execute_off.status == "failure" assert client_1_db_client_execute_off.status == "failure" @@ -166,26 +170,34 @@ class TestDataManipulationGreenRequests: client_1: HostNode = net.get_node_by_hostname("client_1") client_2: HostNode = net.get_node_by_hostname("client_2") - client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) - client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + client_1_browser_execute = net.apply_request(["node", "client_1", "application", "web-browser", "execute"]) + client_2_browser_execute = net.apply_request(["node", "client_2", "application", "web-browser", "execute"]) assert client_1_browser_execute.status == "success" assert client_2_browser_execute.status == "success" router.acl.add_rule(ACLAction.DENY, src_port=PORT_LOOKUP["HTTP"], dst_port=PORT_LOOKUP["HTTP"], position=3) - client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) - client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + client_1_browser_execute = net.apply_request(["node", "client_1", "application", "web-browser", "execute"]) + client_2_browser_execute = net.apply_request(["node", "client_2", "application", "web-browser", "execute"]) assert client_1_browser_execute.status == "failure" assert client_2_browser_execute.status == "failure" - client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) - client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) + client_1_db_client_execute = net.apply_request( + ["node", "client_1", "application", "database-client", "execute"] + ) + client_2_db_client_execute = net.apply_request( + ["node", "client_2", "application", "database-client", "execute"] + ) assert client_1_db_client_execute.status == "success" assert client_2_db_client_execute.status == "success" router.acl.add_rule( ACLAction.DENY, src_port=PORT_LOOKUP["POSTGRES_SERVER"], dst_port=PORT_LOOKUP["POSTGRES_SERVER"] ) - client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) - client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) + client_1_db_client_execute = net.apply_request( + ["node", "client_1", "application", "database-client", "execute"] + ) + client_2_db_client_execute = net.apply_request( + ["node", "client_2", "application", "database-client", "execute"] + ) assert client_1_db_client_execute.status == "failure" assert client_2_db_client_execute.status == "failure" diff --git a/tests/unit_tests/_primaite/_game/_agent/test_actions.py b/tests/unit_tests/_primaite/_game/_agent/test_actions.py index 5750befd..cef24bb1 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_actions.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_actions.py @@ -21,7 +21,7 @@ from primaite.game.agent.actions.service import ( def test_do_nothing_action_form_request(): """Test that the do_nothingAction can form a request and that it is correct.""" request = DoNothingAction.form_request(DoNothingAction.ConfigSchema()) - assert request == ["do_nothing"] + assert request == ["do-nothing"] @pytest.mark.parametrize( diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_agent.py index 7956a44f..a2693591 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent.py @@ -13,15 +13,15 @@ def test_creating_empty_agent(): def test_creating_agent_from_dict(): action_config = { "action_map": { - 0: {"action": "do_nothing", "options": {}}, + 0: {"action": "do-nothing", "options": {}}, 1: { - "action": "node_application_execute", + "action": "node-application-execute", "options": {"node_name": "client", "application_name": "database"}, }, } } observation_config = { - "type": "FILE", + "type": "file", "options": { "file_name": "dog.pdf", "include_num_access": False, @@ -31,7 +31,7 @@ def test_creating_agent_from_dict(): reward_config = { "reward_components": [ { - "type": "DATABASE_FILE_INTEGRITY", + "type": "database-file-integrity", "weight": 0.3, "options": {"node_hostname": "server", "folder_name": "database", "file_name": "database.db"}, } diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index 5156a29f..5d5921a9 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -21,17 +21,17 @@ class TestFileSystemRequiresScan: def test_obs_config(self, yaml_option_string, expected_val): """Check that the default behaviour is to set FileSystemRequiresScan to True.""" obs_cfg_yaml = f""" - type: CUSTOM + type: custom options: components: - - type: NODES + - type: nodes label: NODES options: hosts: - hostname: domain_controller - hostname: web_server services: - - service_name: WebServer + - service_name: web-server - hostname: database_server folders: - folder_name: database @@ -77,7 +77,7 @@ class TestFileSystemRequiresScan: - UDP num_rules: 10 - - type: LINKS + - type: links label: LINKS options: link_references: @@ -91,7 +91,7 @@ class TestFileSystemRequiresScan: - switch_2:eth-1<->client_1:eth-1 - switch_2:eth-2<->client_2:eth-1 - switch_2:eth-7<->security_suite:eth-2 - - type: "NONE" + - type: "none" label: ICS options: {{}} diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py index f55033fd..305375f9 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -28,13 +28,13 @@ def test_probabilistic_agent(): action_space_cfg = { "action_map": { - 0: {"action": "do_nothing", "options": {}}, + 0: {"action": "do-nothing", "options": {}}, 1: { - "action": "node_application_execute", - "options": {"node_name": "client_1", "application_name": "WebBrowser"}, + "action": "node-application-execute", + "options": {"node_name": "client_1", "application_name": "web-browser"}, }, 2: { - "action": "node_file_delete", + "action": "node-file-delete", "options": {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}, }, }, @@ -44,8 +44,8 @@ def test_probabilistic_agent(): game.options = PrimaiteGameOptions(ports=[], protocols=[]) pa_config = { - "type": "ProbabilisticAgent", - "ref": "ProbabilisticAgent", + "type": "probabilistic-agent", + "ref": "probabilistic-agent", "team": "BLUE", "action_space": action_space_cfg, "agent_settings": { @@ -60,11 +60,11 @@ def test_probabilistic_agent(): node_file_delete_count = 0 for _ in range(N_TRIALS): a = pa.get_action(0) - if a == ("do_nothing", {}): + if a == ("do-nothing", {}): do_nothing_count += 1 - elif a == ("node_application_execute", {"node_name": "client_1", "application_name": "WebBrowser"}): + elif a == ("node-application-execute", {"node_name": "client_1", "application_name": "web-browser"}): node_application_execute_count += 1 - elif a == ("node_file_delete", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}): + elif a == ("node-file-delete", {"node_name": "client_1", "folder_name": "downloads", "file_name": "cat.png"}): node_file_delete_count += 1 else: raise AssertionError("Probabilistic agent produced an unexpected action.") diff --git a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py index 5af71319..935349d0 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_sticky_rewards.py @@ -81,22 +81,22 @@ class TestWebpageUnavailabilitySticky: reward = WebpageUnavailablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) browser_history = [] - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "WebBrowser"} - request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "web-browser"} + request = ["network", "node", "computer", "application", "web-browser", "execute"] response = RequestResponse(status="success", data={}) browser_history.append({"outcome": 200}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -104,34 +104,34 @@ class TestWebpageUnavailabilitySticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) browser_history = [] - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 0.0 # agent fails to fetch, get a -1.0 reward - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "WebBrowser"} - request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "web-browser"} + request = ["network", "node", "computer", "application", "web-browser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "WebBrowser"} - request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "web-browser"} + request = ["network", "node", "computer", "application", "web-browser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -142,22 +142,22 @@ class TestWebpageUnavailabilitySticky: reward = WebpageUnavailablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) browser_history = [] - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "WebBrowser"} - request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "web-browser"} + request = ["network", "node", "computer", "application", "web-browser", "execute"] response = RequestResponse(status="success", data={}) browser_history.append({"outcome": 200}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -165,33 +165,33 @@ class TestWebpageUnavailabilitySticky: # THE IMPORTANT BIT # agent did nothing, because reward is sticky, it stays at 1.0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 1.0 # agent fails to fetch, get a -1.0 reward - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "WebBrowser"} - request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "web-browser"} + request = ["network", "node", "computer", "application", "web-browser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "WebBrowser"} - request = ["network", "node", "computer", "application", "WebBrowser", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "web-browser"} + request = ["network", "node", "computer", "application", "web-browser", "execute"] response = RequestResponse(status="failure", data={}) browser_history.append({"outcome": 404}) - state = {"network": {"nodes": {"computer": {"applications": {"WebBrowser": {"history": browser_history}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"web-browser": {"history": browser_history}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -207,20 +207,20 @@ class TestGreenAdminDatabaseUnreachableSticky: reward = GreenAdminDatabaseUnreachablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "DatabaseClient"} - request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "database-client"} + request = ["network", "node", "computer", "application", "database-client", "execute"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -228,31 +228,31 @@ class TestGreenAdminDatabaseUnreachableSticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 0.0 # agent fails to fetch, get a -1.0 reward - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "DatabaseClient"} - request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "database-client"} + request = ["network", "node", "computer", "application", "database-client", "execute"] response = RequestResponse(status="failure", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "DatabaseClient"} - request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "database-client"} + request = ["network", "node", "computer", "application", "database-client", "execute"] response = RequestResponse(status="failure", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -266,20 +266,20 @@ class TestGreenAdminDatabaseUnreachableSticky: reward = GreenAdminDatabaseUnreachablePenalty(config=schema) # no response codes yet, reward is 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 0 # agent did a successful fetch - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "DatabaseClient"} - request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "database-client"} + request = ["network", "node", "computer", "application", "database-client", "execute"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) @@ -287,31 +287,31 @@ class TestGreenAdminDatabaseUnreachableSticky: # THE IMPORTANT BIT # agent did nothing, because reward is not sticky, it goes back to 0 - action, params, request = "do_nothing", {}, ["do_nothing"] + action, params, request = "do-nothing", {}, ["do-nothing"] response = RequestResponse(status="success", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == 1.0 # agent fails to fetch, get a -1.0 reward - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "DatabaseClient"} - request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "database-client"} + request = ["network", "node", "computer", "application", "database-client", "execute"] response = RequestResponse(status="failure", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) assert reward.calculate(state, last_action_response) == -1.0 # agent fails again to fetch, get a -1.0 reward again - action = "node_application_execute" - params = {"node_name": "computer", "application_name": "DatabaseClient"} - request = ["network", "node", "computer", "application", "DatabaseClient", "execute"] + action = "node-application-execute" + params = {"node_name": "computer", "application_name": "database-client"} + request = ["network", "node", "computer", "application", "database-client", "execute"] response = RequestResponse(status="failure", data={}) - state = {"network": {"nodes": {"computer": {"applications": {"DatabaseClient": {}}}}}} + state = {"network": {"nodes": {"computer": {"applications": {"database-client": {}}}}}} last_action_response = AgentHistoryItem( timestep=0, action=action, parameters=params, request=request, response=response ) diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py index 9691080d..e5e79e5f 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file.py @@ -46,7 +46,7 @@ def test_file_reveal_to_red_scan(file_system): assert file.revealed_to_red is True -@pytest.mark.skip(reason="node_file_checkhash not implemented") +@pytest.mark.skip(reason="node-file-checkhash not implemented") def test_simulated_file_check_hash(file_system): file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder") diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py index 59f3f000..13e3cbe2 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_file_actions.py @@ -32,7 +32,7 @@ def test_file_scan_request(populated_file_system): assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT -@pytest.mark.skip(reason="node_file_checkhash not implemented") +@pytest.mark.skip(reason="node-file-checkhash not implemented") def test_file_checkhash_request(populated_file_system): """Test that an agent can request a file hash check.""" fs, folder, file = populated_file_system diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py index b5d9b269..fd581ea6 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder.py @@ -120,7 +120,7 @@ def test_folder_corrupt_repair(file_system): assert file.health_status == FileSystemItemHealthStatus.GOOD -@pytest.mark.skip(reason="node_file_checkhash not implemented") +@pytest.mark.skip(reason="node-file-checkhash not implemented") def test_simulated_folder_check_hash(file_system): folder: Folder = file_system.create_folder(folder_name="test_folder") file_system.create_file(file_name="test_file.txt", folder_name="test_folder") diff --git a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py index 72857638..1eba3e55 100644 --- a/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_file_system/test_folder_actions.py @@ -51,7 +51,7 @@ def test_folder_scan_request(populated_file_system): assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT -@pytest.mark.skip(reason="node_folder_checkhash not implemented") +@pytest.mark.skip(reason="node-folder-checkhash not implemented") def test_folder_checkhash_request(populated_file_system): """Test that an agent can request a folder hash check.""" fs, folder, file = populated_file_system diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 672a4b5f..7026c116 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -57,13 +57,13 @@ def test_node_os_scan(node): # add services to node node.software_manager.install(DummyService) - service = node.software_manager.software.get("DummyService") + service = node.software_manager.software.get("dummy-service") service.set_health_state(SoftwareHealthState.COMPROMISED) assert service.health_state_visible == SoftwareHealthState.UNUSED # add application to node node.software_manager.install(DummyApplication) - application = node.software_manager.software.get("DummyApplication") + application = node.software_manager.software.get("dummy-application") application.set_health_state(SoftwareHealthState.COMPROMISED) assert application.health_state_visible == SoftwareHealthState.UNUSED @@ -103,12 +103,12 @@ def test_node_red_scan(node): # add services to node node.software_manager.install(DummyService) - service = node.software_manager.software.get("DummyService") + service = node.software_manager.software.get("dummy-service") assert service.revealed_to_red is False # add application to node node.software_manager.install(DummyApplication) - application = node.software_manager.software.get("DummyApplication") + application = node.software_manager.software.get("dummy-application") application.set_health_state(SoftwareHealthState.COMPROMISED) assert application.revealed_to_red is False diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py index 9885df67..43ffe366 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_creation.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_creation.py @@ -56,7 +56,7 @@ def test_office_lan_from_config(kwargs): net = Network() config = dict( - type="office_lan", + type="office-lan", lan_name=kwargs["lan_name"], subnet_base=kwargs["subnet_base"], pcs_ip_block_start=kwargs["pcs_ip_block_start"], diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py index 17f8445a..dfbd26cb 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_c2_suite.py @@ -44,8 +44,8 @@ def setup_c2(given_network: Network): computer_a: Computer = network.get_node_by_hostname("computer_a") computer_b: Computer = network.get_node_by_hostname("computer_b") - c2_beacon: C2Beacon = computer_b.software_manager.software.get("C2Beacon") - c2_server: C2Server = computer_a.software_manager.software.get("C2Server") + c2_beacon: C2Beacon = computer_b.software_manager.software.get("c2-beacon") + c2_server: C2Server = computer_a.software_manager.software.get("c2-server") c2_beacon.configure(c2_server_ip_address="192.168.0.1", keep_alive_frequency=2) c2_server.run() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py index 6e9ee224..35703e14 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_data_manipulation_bot.py @@ -20,13 +20,13 @@ def dm_client() -> Node: @pytest.fixture def dm_bot(dm_client) -> DataManipulationBot: - return dm_client.software_manager.software.get("DataManipulationBot") + return dm_client.software_manager.software.get("data-manipulation-bot") def test_create_dm_bot(dm_client): - data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software.get("DataManipulationBot") + data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software.get("data-manipulation-bot") - assert data_manipulation_bot.name == "DataManipulationBot" + assert data_manipulation_bot.name == "data-manipulation-bot" assert data_manipulation_bot.port == PORT_LOOKUP["NONE"] assert data_manipulation_bot.protocol == PROTOCOL_LOOKUP["NONE"] assert data_manipulation_bot.payload == "DELETE" @@ -75,8 +75,8 @@ def test_dm_bot_perform_data_manipulation_success(dm_bot): def test_dm_bot_fails_without_db_client(dm_client): - dm_client.software_manager.uninstall("DatabaseClient") - dm_bot = dm_client.software_manager.software.get("DataManipulationBot") + dm_client.software_manager.uninstall("database-client") + dm_bot = dm_client.software_manager.software.get("data-manipulation-bot") assert dm_bot._host_db_client is None dm_bot.attack_stage = DataManipulationAttackStage.PORT_SCAN dm_bot._perform_data_manipulation(p_of_success=1.0) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py index 9d8b7809..eab283b8 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/_red_applications/test_dos_bot.py @@ -18,7 +18,7 @@ def dos_bot() -> DoSBot: computer.power_on() computer.software_manager.install(DoSBot) - dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") + dos_bot: DoSBot = computer.software_manager.software.get("dos-bot") dos_bot.configure(target_ip_address=IPv4Address("192.168.0.1")) return dos_bot diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py index 9e448b87..2a5d34ab 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_application_registry.py @@ -5,18 +5,18 @@ from primaite.simulator.system.applications.application import Application def test_adding_to_app_registry(): - class temp_application(Application, discriminator="temp_app"): + class temp_application(Application, discriminator="temp-app"): pass - assert Application._registry["temp_app"] is temp_application + assert Application._registry["temp-app"] is temp_application with pytest.raises(ValueError): - class another_application(Application, discriminator="temp_app"): + class another_application(Application, discriminator="temp-app"): pass # This is kinda evil... # Because pytest doesn't reimport classes from modules, registering this temporary test application will change the # state of the Application registry for all subsequently run tests. So, we have to delete and unregister the class. del temp_application - Application._registry.pop("temp_app") + Application._registry.pop("temp-app") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py index 5917fde7..58215671 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -20,7 +20,7 @@ def database_client_on_computer() -> Tuple[DatabaseClient, Computer]: db_server = Server(hostname="db_server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", start_up_duration=0) db_server.power_on() db_server.software_manager.install(DatabaseService) - db_server.software_manager.software["DatabaseService"].start() + db_server.software_manager.software["database-service"].start() db_client = Computer( hostname="db_client", ip_address="192.168.0.2", subnet_mask="255.255.255.0", start_up_duration=0 @@ -28,7 +28,7 @@ def database_client_on_computer() -> Tuple[DatabaseClient, Computer]: db_client.power_on() db_client.software_manager.install(DatabaseClient) - database_client: DatabaseClient = db_client.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = db_client.software_manager.software.get("database-client") database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) database_client.run() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index f78b3261..c5f4e74c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -21,7 +21,7 @@ def web_browser() -> WebBrowser: ) computer.power_on() # Web Browser should be pre-installed in computer - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") web_browser.run() assert web_browser.operating_state is ApplicationOperatingState.RUNNING return web_browser @@ -37,8 +37,8 @@ def test_create_web_client(): ) computer.power_on() # Web Browser should be pre-installed in computer - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") - assert web_browser.name is "WebBrowser" + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") + assert web_browser.name is "web-browser" assert web_browser.port is PORT_LOOKUP["HTTP"] assert web_browser.protocol is PROTOCOL_LOOKUP["TCP"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index ef165c8f..2d0fdb8d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -11,7 +11,7 @@ def database_server() -> Node: node = Computer(hostname="db_node", ip_address="192.168.1.2", subnet_mask="255.255.255.0", start_up_duration=0) node.power_on() node.software_manager.install(DatabaseService) - node.software_manager.software.get("DatabaseService").start() + node.software_manager.software.get("database-service").start() return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 1bc5b353..f4dfe20e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -26,14 +26,14 @@ def dns_client() -> Computer: def test_create_dns_client(dns_client): assert dns_client is not None - dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") - assert dns_client_service.name is "DNSClient" + dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") + assert dns_client_service.name is "dns-client" assert dns_client_service.port is PORT_LOOKUP["DNS"] assert dns_client_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_dns_client_add_domain_to_cache_when_not_running(dns_client): - dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") + dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") assert dns_client.operating_state is NodeOperatingState.OFF assert dns_client_service.operating_state is ServiceOperatingState.STOPPED @@ -46,7 +46,7 @@ def test_dns_client_add_domain_to_cache_when_not_running(dns_client): def test_dns_client_check_domain_exists_when_not_running(dns_client): dns_client.operating_state = NodeOperatingState.ON - dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") + dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") dns_client_service.start() assert dns_client.operating_state is NodeOperatingState.ON @@ -73,7 +73,7 @@ def test_dns_client_check_domain_exists_when_not_running(dns_client): def test_dns_client_check_domain_in_cache(dns_client): """Test to make sure that the check_domain_in_cache returns the correct values.""" dns_client.operating_state = NodeOperatingState.ON - dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") + dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") dns_client_service.start() # add a domain to the dns client cache @@ -85,7 +85,7 @@ def test_dns_client_check_domain_in_cache(dns_client): def test_dns_client_receive(dns_client): """Test to make sure the DNS Client knows how to deal with request responses.""" - dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") + dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") dns_client_service.receive( payload=DNSPacket( @@ -99,6 +99,6 @@ def test_dns_client_receive(dns_client): def test_dns_client_receive_non_dns_payload(dns_client): - dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") + dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") assert dns_client_service.receive(payload=None) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 3bc2b1a4..65f472d9 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -30,15 +30,15 @@ def dns_server() -> Node: def test_create_dns_server(dns_server): assert dns_server is not None - dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") - assert dns_server_service.name is "DNSServer" + dns_server_service: DNSServer = dns_server.software_manager.software.get("dns-server") + assert dns_server_service.name is "dns-server" assert dns_server_service.port is PORT_LOOKUP["DNS"] assert dns_server_service.protocol is PROTOCOL_LOOKUP["TCP"] def test_dns_server_domain_name_registration(dns_server): """Test to check if the domain name registration works.""" - dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") + dns_server_service: DNSServer = dns_server.software_manager.software.get("dns-server") # register the web server in the domain controller dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) @@ -50,7 +50,7 @@ def test_dns_server_domain_name_registration(dns_server): def test_dns_server_receive(dns_server): """Test to make sure that the DNS Server correctly responds to a DNS Client request.""" - dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") + dns_server_service: DNSServer = dns_server.software_manager.software.get("dns-server") # register the web server in the domain controller dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) @@ -60,7 +60,7 @@ def test_dns_server_receive(dns_server): client.dns_server = IPv4Address("192.168.1.10") network = Network() network.connect(dns_server.network_interface[1], client.network_interface[1]) - dns_client: DNSClient = client.software_manager.software["DNSClient"] # noqa + dns_client: DNSClient = client.software_manager.software["dns-client"] # noqa dns_client.check_domain_exists("fake-domain.com") assert dns_client.check_domain_exists("fake-domain.com") is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index d3e679db..26aa5dd5 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -29,8 +29,8 @@ def ftp_client() -> Node: def test_create_ftp_client(ftp_client): assert ftp_client is not None - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") - assert ftp_client_service.name is "FTPClient" + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") + assert ftp_client_service.name is "ftp-client" assert ftp_client_service.port is PORT_LOOKUP["FTP"] assert ftp_client_service.protocol is PROTOCOL_LOOKUP["TCP"] @@ -51,7 +51,7 @@ def test_ftp_client_store_file(ftp_client): status_code=FTPStatusCode.OK, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") ftp_client_service.receive(response) assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") @@ -65,7 +65,7 @@ def test_ftp_should_not_process_commands_if_service_not_running(ftp_client): status_code=FTPStatusCode.OK, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") ftp_client_service.stop() assert ftp_client_service.operating_state is ServiceOperatingState.STOPPED assert ftp_client_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR @@ -75,7 +75,7 @@ def test_ftp_tries_to_senf_file__that_does_not_exist(ftp_client): """Method send_file should return false if no file to send.""" assert ftp_client.file_system.get_file(folder_name="root", file_name="test.txt") is None - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") assert ftp_client_service.operating_state is ServiceOperatingState.RUNNING assert ( ftp_client_service.send_file( @@ -91,7 +91,7 @@ def test_ftp_tries_to_senf_file__that_does_not_exist(ftp_client): def test_offline_ftp_client_receives_request(ftp_client): """Receive should return false if the node the ftp client is installed on is offline.""" - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") ftp_client.power_off() for i in range(ftp_client.shut_down_duration + 1): @@ -111,7 +111,7 @@ def test_offline_ftp_client_receives_request(ftp_client): def test_receive_should_fail_if_payload_is_not_ftp(ftp_client): """Receive should return false if the node the ftp client is installed on is not an FTPPacket.""" - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") assert ftp_client_service.receive(payload=None) is False @@ -122,5 +122,5 @@ def test_receive_should_ignore_payload_with_none_status_code(ftp_client): ftp_command_args=PORT_LOOKUP["FTP"], status_code=None, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") assert ftp_client_service.receive(payload=payload) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 37c3d019..fb2f82fe 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -28,8 +28,8 @@ def ftp_server() -> Node: def test_create_ftp_server(ftp_server): assert ftp_server is not None - ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") - assert ftp_server_service.name is "FTPServer" + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("ftp-server") + assert ftp_server_service.name is "ftp-server" assert ftp_server_service.port is PORT_LOOKUP["FTP"] assert ftp_server_service.protocol is PROTOCOL_LOOKUP["TCP"] @@ -49,7 +49,7 @@ def test_ftp_server_store_file(ftp_server): packet_payload_size=24, ) - ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("ftp-server") ftp_server_service.receive(response) assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") @@ -63,7 +63,7 @@ def test_ftp_server_should_send_error_if_port_arg_is_invalid(ftp_server): packet_payload_size=24, ) - ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("ftp-server") assert ftp_server_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR @@ -71,7 +71,7 @@ def test_ftp_server_receives_non_ftp_packet(ftp_server): """Receive should return false if the service receives a non ftp packet.""" response: FTPPacket = None - ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("ftp-server") assert ftp_server_service.receive(response) is False @@ -87,7 +87,7 @@ def test_offline_ftp_server_receives_request(ftp_server): packet_payload_size=24, ) - ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("ftp-server") ftp_server_service.stop() assert ftp_server_service.operating_state is ServiceOperatingState.STOPPED assert ftp_server_service.receive(response) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 08bef92d..f0906de9 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -18,7 +18,7 @@ from primaite.simulator.network.protocols.ssh import ( SSHTransportMessage, SSHUserCredentials, ) -from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript +from primaite.simulator.system.applications.red_applications.ransomware_script import ransomware_script from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection, Terminal @@ -33,7 +33,7 @@ def terminal_on_computer() -> Tuple[Terminal, Computer]: hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0 ) computer.power_on() - terminal: Terminal = computer.software_manager.software.get("Terminal") + terminal: Terminal = computer.software_manager.software.get("terminal") return terminal, computer @@ -82,7 +82,7 @@ def wireless_wan_network(): ) router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) - # add ACL rule to allow SSH traffic + # add acl rule to allow SSH traffic router_1.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=21 ) @@ -142,25 +142,25 @@ def test_terminal_creation(terminal_on_computer): def test_terminal_install_default(): - """Terminal should be auto installed onto Nodes""" + """terminal should be auto installed onto Nodes""" computer = Computer(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0) computer.power_on() - assert computer.software_manager.software.get("Terminal") + assert computer.software_manager.software.get("terminal") def test_terminal_not_on_switch(): """Ensure terminal does not auto-install to switch""" test_switch = Switch(hostname="Test") - assert not test_switch.software_manager.software.get("Terminal") + assert not test_switch.software_manager.software.get("terminal") def test_terminal_send(basic_network): - """Test that Terminal can send valid commands.""" + """Test that terminal can send valid commands.""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") payload: SSHPacket = SSHPacket( @@ -178,7 +178,7 @@ def test_terminal_receive(basic_network): """Test that terminal can receive and process commands""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") folder_name = "Downloads" @@ -199,14 +199,14 @@ def test_terminal_receive(basic_network): def test_terminal_install(basic_network): - """Test that Terminal can successfully process an INSTALL request""" + """Test that terminal can successfully process an INSTALL request""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") payload: SSHPacket = SSHPacket( - payload=["software_manager", "application", "install", "RansomwareScript"], + payload=["software_manager", "application", "install", "ransomware-script"], transport_message=SSHTransportMessage.SSH_MSG_SERVICE_REQUEST, connection_message=SSHConnectionMessage.SSH_MSG_CHANNEL_OPEN, ) @@ -215,16 +215,16 @@ def test_terminal_install(basic_network): username="admin", password="admin", ip_address="192.168.0.11" ) - term_a_on_node_b.execute(["software_manager", "application", "install", "RansomwareScript"]) + term_a_on_node_b.execute(["software_manager", "application", "install", "ransomware-script"]) - assert computer_b.software_manager.software.get("RansomwareScript") + assert computer_b.software_manager.software.get("ransomware-script") def test_terminal_fail_when_closed(basic_network): - """Ensure Terminal won't attempt to send/receive when off""" + """Ensure terminal won't attempt to send/receive when off""" network: Network = basic_network computer: Computer = network.get_node_by_hostname("node_a") - terminal: Terminal = computer.software_manager.software.get("Terminal") + terminal: Terminal = computer.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") terminal.operating_state = ServiceOperatingState.STOPPED @@ -233,12 +233,12 @@ def test_terminal_fail_when_closed(basic_network): def test_terminal_disconnect(basic_network): - """Test Terminal disconnects""" + """Test terminal disconnects""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") - terminal_b: Terminal = computer_b.software_manager.software.get("Terminal") + terminal_b: Terminal = computer_b.software_manager.software.get("terminal") assert len(terminal_b._connections) == 0 @@ -256,10 +256,10 @@ def test_terminal_disconnect(basic_network): def test_terminal_ignores_when_off(basic_network): - """Terminal should ignore commands when not running""" + """terminal should ignore commands when not running""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") @@ -269,14 +269,14 @@ def test_terminal_ignores_when_off(basic_network): terminal_a.operating_state = ServiceOperatingState.STOPPED - assert not term_a_on_term_b.execute(["software_manager", "application", "install", "RansomwareScript"]) + assert not term_a_on_term_b.execute(["software_manager", "application", "install", "ransomware-script"]) def test_computer_remote_login_to_router(wireless_wan_network): """Test to confirm that a computer can SSH into a router.""" pc_a, _, router_1, _ = wireless_wan_network - pc_a_terminal: Terminal = pc_a.software_manager.software.get("Terminal") + pc_a_terminal: Terminal = pc_a.software_manager.software.get("terminal") assert len(pc_a_terminal._connections) == 0 @@ -284,18 +284,18 @@ def test_computer_remote_login_to_router(wireless_wan_network): assert len(pc_a_terminal._connections) == 1 - payload = ["software_manager", "application", "install", "RansomwareScript"] + payload = ["software_manager", "application", "install", "ransomware-script"] pc_a_on_router_1.execute(payload) - assert router_1.software_manager.software.get("RansomwareScript") + assert router_1.software_manager.software.get("ransomware-script") def test_router_remote_login_to_computer(wireless_wan_network): """Test to confirm that a router can ssh into a computer.""" pc_a, _, router_1, _ = wireless_wan_network - router_1_terminal: Terminal = router_1.software_manager.software.get("Terminal") + router_1_terminal: Terminal = router_1.software_manager.software.get("terminal") assert len(router_1_terminal._connections) == 0 @@ -303,21 +303,21 @@ def test_router_remote_login_to_computer(wireless_wan_network): assert len(router_1_terminal._connections) == 1 - payload = ["software_manager", "application", "install", "RansomwareScript"] + payload = ["software_manager", "application", "install", "ransomware-script"] router_1_on_pc_a.execute(payload) - assert pc_a.software_manager.software.get("RansomwareScript") + assert pc_a.software_manager.software.get("ransomware-script") def test_router_blocks_SSH_traffic(wireless_wan_network): - """Test to check that router will block SSH traffic if no ACL rule.""" + """Test to check that router will block SSH traffic if no acl rule.""" pc_a, _, router_1, _ = wireless_wan_network # Remove rule that allows SSH traffic. router_1.acl.remove_rule(position=21) - pc_a_terminal: Terminal = pc_a.software_manager.software.get("Terminal") + pc_a_terminal: Terminal = pc_a.software_manager.software.get("terminal") assert len(pc_a_terminal._connections) == 0 @@ -330,8 +330,8 @@ def test_SSH_across_network(wireless_wan_network): """Test to show ability to SSH across a network.""" pc_a, pc_b, router_1, router_2 = wireless_wan_network - terminal_a: Terminal = pc_a.software_manager.software.get("Terminal") - terminal_b: Terminal = pc_b.software_manager.software.get("Terminal") + terminal_a: Terminal = pc_a.software_manager.software.get("terminal") + terminal_b: Terminal = pc_b.software_manager.software.get("terminal") router_2.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=21 @@ -348,7 +348,7 @@ def test_multiple_remote_terminals_same_node(basic_network): """Test to check that multiple remote terminals can be spawned by one node.""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") assert len(terminal_a._connections) == 0 @@ -366,10 +366,10 @@ def test_terminal_rejects_commands_if_disconnect(basic_network): """Test to check terminal will ignore commands from disconnected connections""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") - terminal_b: Terminal = computer_b.software_manager.software.get("Terminal") + terminal_b: Terminal = computer_b.software_manager.software.get("terminal") remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") @@ -381,9 +381,9 @@ def test_terminal_rejects_commands_if_disconnect(basic_network): assert len(terminal_a._connections) == 0 assert len(terminal_b._connections) == 0 - assert remote_connection.execute(["software_manager", "application", "install", "RansomwareScript"]) is False + assert remote_connection.execute(["software_manager", "application", "install", "ransomware-script"]) is False - assert not computer_b.software_manager.software.get("RansomwareScript") + assert not computer_b.software_manager.software.get("ransomware-script") assert remote_connection.is_active is False @@ -392,9 +392,9 @@ def test_terminal_connection_timeout(basic_network): """Test that terminal_connections are affected by UserSession timeout.""" network: Network = basic_network computer_a: Computer = network.get_node_by_hostname("node_a") - terminal_a: Terminal = computer_a.software_manager.software.get("Terminal") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") computer_b: Computer = network.get_node_by_hostname("node_b") - terminal_b: Terminal = computer_b.software_manager.software.get("Terminal") + terminal_b: Terminal = computer_b.software_manager.software.get("terminal") remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index 606a195c..4bd8a7e3 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -25,14 +25,14 @@ def web_server() -> Server: ) node.power_on() node.software_manager.install(WebServer) - node.software_manager.software.get("WebServer").start() + node.software_manager.software.get("web-server").start() return node def test_create_web_server(web_server): assert web_server is not None - web_server_service: WebServer = web_server.software_manager.software.get("WebServer") - assert web_server_service.name is "WebServer" + web_server_service: WebServer = web_server.software_manager.software.get("web-server") + assert web_server_service.name is "web-server" assert web_server_service.port is PORT_LOOKUP["HTTP"] assert web_server_service.protocol is PROTOCOL_LOOKUP["TCP"] @@ -40,7 +40,7 @@ def test_create_web_server(web_server): def test_handling_get_request_not_found_path(web_server): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/fake-path") - web_server_service: WebServer = web_server.software_manager.software.get("WebServer") + web_server_service: WebServer = web_server.software_manager.software.get("web-server") response: HttpResponsePacket = web_server_service._handle_get_request(payload=payload) assert response.status_code == HttpStatusCode.NOT_FOUND @@ -49,7 +49,7 @@ def test_handling_get_request_not_found_path(web_server): def test_handling_get_request_home_page(web_server): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/") - web_server_service: WebServer = web_server.software_manager.software.get("WebServer") + web_server_service: WebServer = web_server.software_manager.software.get("web-server") response: HttpResponsePacket = web_server_service._handle_get_request(payload=payload) assert response.status_code == HttpStatusCode.OK diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 12cb736d..8c39c41d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -15,7 +15,7 @@ class TestSoftware(Service, discriminator="TestSoftware"): class ConfigSchema(Service.ConfigSchema): """ConfigSChema for TestSoftware.""" - type: str = "TestSoftware" + type: str = "test-software" config: "TestSoftware.ConfigSchema" = Field(default_factory=lambda: TestSoftware.ConfigSchema()) @@ -26,7 +26,7 @@ class TestSoftware(Service, discriminator="TestSoftware"): @pytest.fixture(scope="function") def software(file_system): return TestSoftware( - name="TestSoftware", + name="test-software", port=PORT_LOOKUP["ARP"], file_system=file_system, sys_log=SysLog(hostname="test_service"), From a310fb3b643f7fbf1b4baba8aee78dac9d1f794c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 3 Feb 2025 16:29:27 +0000 Subject: [PATCH 176/224] #3062 - Discriminator bugfixes --- src/primaite/game/game.py | 2 +- src/primaite/simulator/system/services/arp/arp.py | 2 +- .../_primaite/_simulator/_system/_services/test_terminal.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index a8e23d56..f42d6824 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -384,7 +384,7 @@ class PrimaiteGame: if service_class is not None: _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") new_node.software_manager.install(service_class, software_config=service_cfg.get("options", {})) - new_service = new_node.software_manager.software[service_class.__name__] + new_service = new_node.software_manager.software[service_type] # fixing duration for the service if "fixing_duration" in service_cfg.get("options", {}): diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index 0946f985..b0630d5d 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -33,7 +33,7 @@ class ARP(Service, discriminator="arp"): arp: Dict[IPV4Address, ARPEntry] = {} def __init__(self, **kwargs): - kwargs["name"] = "ARP" + kwargs["name"] = "arp" kwargs["port"] = PORT_LOOKUP["ARP"] kwargs["protocol"] = PROTOCOL_LOOKUP["UDP"] super().__init__(**kwargs) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index f0906de9..144037f5 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -18,7 +18,7 @@ from primaite.simulator.network.protocols.ssh import ( SSHTransportMessage, SSHUserCredentials, ) -from primaite.simulator.system.applications.red_applications.ransomware_script import ransomware_script +from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection, Terminal From d24f9872ffac89b6cf28830c073c598ef214cb2f Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 3 Feb 2025 17:06:12 +0000 Subject: [PATCH 177/224] #3075: C2C-E2E-Demo internal YAML changes. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 1966 +++++++++++++++-- 1 file changed, 1747 insertions(+), 219 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 625510b8..2c1a94c8 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -13,16 +13,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:19,828: Performing the PrimAITE first-time setup...\n", + "2025-02-03 16:04:19,829: Building the PrimAITE app directories...\n", + "2025-02-03 16:04:19,829: Building primaite_config.yaml...\n", + "2025-02-03 16:04:19,829: Rebuilding the demo notebooks...\n", + "/home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", + "2025-02-03 16:04:19,831: Reset example notebook: /home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", + "2025-02-03 16:04:19,836: Rebuilding the example notebooks...\n", + "2025-02-03 16:04:19,840: PrimAITE setup complete!\n" + ] + } + ], "source": [ "!primaite setup" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -36,8 +51,7 @@ "from primaite.simulator.system.applications.red_applications.c2.abstract_c2 import C2Command\n", "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", - "from primaite.simulator.network.hardware.nodes.host.server import Server\n", - "from primaite.game.agent.scripted_agents import probabilistic_agent" + "from primaite.simulator.network.hardware.nodes.host.server import Server" ] }, { @@ -53,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -63,24 +77,24 @@ " type: ProxyAgent\n", "\n", " action_space:\n", - " options:\n", - " nodes:\n", - " - node_name: web_server\n", - " applications:\n", - " - application_name: C2Beacon\n", - " - node_name: client_1\n", - " applications:\n", - " - application_name: C2Server\n", - " max_folders_per_node: 1\n", - " max_files_per_folder: 1\n", - " max_services_per_node: 2\n", - " max_nics_per_node: 8\n", - " max_acl_rules: 10\n", - " ip_list:\n", - " - 192.168.1.21\n", - " - 192.168.1.14\n", - " wildcard_list:\n", - " - 0.0.0.1\n", + " # options:\n", + " # nodes:\n", + " # - node_name: web_server\n", + " # applications:\n", + " # - application_name: C2Beacon\n", + " # - node_name: client_1\n", + " # applications:\n", + " # - application_name: C2Server\n", + " # max_folders_per_node: 1\n", + " # max_files_per_folder: 1\n", + " # max_services_per_node: 2\n", + " # max_nics_per_node: 8\n", + " # max_acl_rules: 10\n", + " # ip_list:\n", + " # - 192.168.1.21\n", + " # - 192.168.1.14\n", + " # wildcard_list:\n", + " # - 0.0.0.1\n", " action_map:\n", " 0:\n", " action: do_nothing\n", @@ -88,30 +102,30 @@ " 1:\n", " action: node_application_install\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " application_name: C2Beacon\n", " 2:\n", " action: configure_c2_beacon\n", " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency:\n", - " masquerade_protocol:\n", - " masquerade_port:\n", + " node_name: web_server\n", + " # config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " # keep_alive_frequency: 10\n", + " # masquerade_protocol: TCP\n", + " # masquerade_port: DNS\n", " 3:\n", " action: node_application_execute\n", " options:\n", - " node_id: 0\n", - " application_id: 0\n", + " node_name: web_server\n", + " application_name: C2Beacon\n", " 4:\n", " action: c2_server_terminal_command\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " ip_address:\n", - " account:\n", - " username: admin\n", - " password: admin\n", + " # account:\n", + " username: admin\n", + " password: admin\n", " commands:\n", " -\n", " - software_manager\n", @@ -121,44 +135,44 @@ " 5:\n", " action: c2_server_ransomware_configure\n", " options:\n", - " node_id: 1\n", - " config:\n", - " server_ip_address: 192.168.1.14\n", - " payload: ENCRYPT\n", + " node_name: client_1\n", + " # config:\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", " 6:\n", " action: c2_server_data_exfiltrate\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " target_file_name: \"database.db\"\n", " target_folder_name: \"database\"\n", " exfiltration_folder_name: \"spoils\"\n", " target_ip_address: 192.168.1.14\n", - " account:\n", - " username: admin\n", - " password: admin\n", + " # account:\n", + " username: admin\n", + " password: admin\n", "\n", " 7:\n", " action: c2_server_ransomware_launch\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " 8:\n", " action: configure_c2_beacon\n", " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency: 10\n", - " masquerade_protocol: TCP\n", - " masquerade_port: DNS\n", + " node_name: web_server\n", + " # config:\n", + " c2_server_ip_address: 192.168.10.21\n", + " # keep_alive_frequency: 10\n", + " # masquerade_protocol: TCP\n", + " # masquerade_port: DNS\n", " 9:\n", " action: configure_c2_beacon\n", " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.22\n", - " keep_alive_frequency:\n", - " masquerade_protocol:\n", - " masquerade_port:\n", + " node_name: web_server\n", + " # config:\n", + " c2_server_ip_address: 192.168.10.22\n", + " # keep_alive_frequency: 10\n", + " # masquerade_protocol: TCP\n", + " # masquerade_port: DNS\n", "\n", " reward_function:\n", " reward_components:\n", @@ -169,9 +183,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:24,734: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -197,9 +219,35 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------------------------------------------------------------------------------+\n", + "| client_1 Software Manager |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", + "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", @@ -249,9 +297,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | INSTALLING | UNUSED | None | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "env.step(1)\n", "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", @@ -291,9 +364,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "env.step(2)\n", "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", @@ -332,18 +437,54 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=2, action='node_application_execute', parameters={'node_name': 'web_server', 'application_name': 'C2Beacon'}, request=['network', 'node', 'web_server', 'application', 'C2Beacon', 'execute'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(3)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 1 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.1.12 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "c2_beacon.show()\n", "c2_server.show()" @@ -406,18 +547,59 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=3, action='c2_server_terminal_command', parameters={'node_name': 'client_1', 'ip_address': None, 'username': 'admin', 'password': 'admin', 'commands': [['software_manager', 'application', 'install', 'RansomwareScript']]}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'terminal_command', {'commands': [['software_manager', 'application', 'install', 'RansomwareScript']], 'ip_address': None, 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={0: RequestResponse(status='success', data={})}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(4)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------------------------------------------------------------------------------+\n", + "| client_1 Software Manager |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", + "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "client_1.software_manager.show()" ] @@ -456,18 +638,66 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=4, action='c2_server_ransomware_configure', parameters={'node_name': 'client_1', 'server_ip_address': '192.168.1.14', 'payload': 'ENCRYPT'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_configure', {'server_ip_address': '192.168.1.14', 'server_password': None, 'payload': 'ENCRYPT'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(5)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", + "| RansomwareScript | Application | RUNNING | GOOD | None | none |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "+------------------------------------+\n", + "| RansomwareScript Running Status |\n", + "+--------------------------+---------+\n", + "| Target Server IP Address | Payload |\n", + "+--------------------------+---------+\n", + "| 192.168.1.14 | ENCRYPT |\n", + "+--------------------------+---------+\n" + ] + } + ], "source": [ "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", "web_server.software_manager.show()\n", @@ -513,18 +743,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=5, action='c2_server_data_exfiltrate', parameters={'node_name': 'client_1', 'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'exfiltrate', {'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(6)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------------------------------------------------------------------------+\n", + "| client_1 File System |\n", + "+--------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+--------------------+---------+---------------+-----------------------+---------+\n", + "| root | 0 B | GOOD | NONE | False |\n", + "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", + "+--------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.file_system.show(full=True)" @@ -532,9 +792,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+---------------------------------------------------------------------------------+\n", + "| web_server File System |\n", + "+---------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+---------------------+---------+---------------+-----------------------+---------+\n", + "| primaite/index.html | 15.0 KB | GOOD | NONE | False |\n", + "| root | 0 B | GOOD | NONE | False |\n", + "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", + "+---------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "web_server.software_manager.file_system.show(full=True)" @@ -571,18 +847,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=6, action='c2_server_ransomware_launch', parameters={'node_name': 'client_1'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_launch'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(7)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------+\n", + "| database_server File System |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| database/database.db | 4.77 MB | CORRUPT | NONE | False |\n", + "| root | 0 B | GOOD | NONE | False |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "database_server: Server = env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -601,7 +907,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -697,52 +1003,52 @@ " 1:\n", " action: node_application_remove\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " application_name: C2Beacon\n", " 2:\n", " action: node_shutdown\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " 3:\n", " action: router_acl_add_rule\n", " options:\n", " target_router: router_1\n", " position: 1\n", - " permission: 2\n", - " source_ip_id: 2\n", - " dest_ip_id: 3\n", - " source_port_id: 2\n", - " dest_port_id: 2\n", - " protocol_id: 1\n", - " source_wildcard_id: 0\n", - " dest_wildcard_id: 0\n", + " permission: DENY\n", + " src_ip: 192.168.10.21\n", + " dst_ip: 192.168.1.12\n", + " src_port: HTTP\n", + " dst_port: HTTP\n", + " protocol_name: ALL\n", + " src_wildcard: NONE\n", + " dst_wildcard: NONE\n", "\n", "\n", - " options:\n", - " nodes:\n", - " - node_name: web_server\n", - " applications:\n", - " - application_name: C2Beacon\n", + " # options:\n", + " # nodes:\n", + " # - node_name: web_server\n", + " # applications:\n", + " # - application_name: C2Beacon\n", "\n", - " - node_name: database_server\n", - " folders:\n", - " - folder_name: database\n", - " files:\n", - " - file_name: database.db\n", - " services:\n", - " - service_name: DatabaseService\n", - " - node_name: router_1\n", + " # - node_name: database_server\n", + " # folders:\n", + " # - folder_name: database\n", + " # files:\n", + " # - file_name: database.db\n", + " # services:\n", + " # - service_name: DatabaseService\n", + " # - node_name: router_1\n", "\n", - " max_folders_per_node: 2\n", - " max_files_per_folder: 2\n", - " max_services_per_node: 2\n", - " max_nics_per_node: 8\n", - " max_acl_rules: 10\n", - " ip_list:\n", - " - 192.168.10.21\n", - " - 192.168.1.12\n", - " wildcard_list:\n", - " - 0.0.0.1\n", + " # max_folders_per_node: 2\n", + " # max_files_per_folder: 2\n", + " # max_services_per_node: 2\n", + " # max_nics_per_node: 8\n", + " # max_acl_rules: 10\n", + " # ip_list:\n", + " # - 192.168.10.21\n", + " # - 192.168.1.12\n", + " # wildcard_list:\n", + " # - 0.0.0.1\n", " reward_function:\n", " reward_components:\n", " - type: DUMMY\n", @@ -755,9 +1061,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:26,020: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -771,7 +1085,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -814,9 +1128,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:26,440: Resetting environment, episode 0, avg. reward: 0.0\n", + "2025-02-03 16:04:26,445: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_0.json\n" + ] + } + ], "source": [ "# Resetting the environment and capturing the default observation space.\n", "blue_env.reset()\n", @@ -825,9 +1148,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Setting up the C2 Suite via the simulation API.\n", "\n", @@ -848,7 +1182,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -858,9 +1192,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 2\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" ] @@ -880,9 +1231,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={0: RequestResponse(status='success', data={})})" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -893,9 +1255,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -904,7 +1277,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -914,9 +1287,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 7\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", + "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 0 -> 3\n", + "root['NODES']['HOST0']['users']['local_login']: 0 -> 1\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" ] @@ -932,7 +1324,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ @@ -948,16 +1340,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": {}, "outputs": [], "source": [ @@ -966,9 +1369,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 7\n", + "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", + "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n" + ] + } + ], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_exfil_obs, env.game.step_counter)" ] @@ -984,9 +1400,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -995,9 +1422,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", "blue_env.step(0)\n", @@ -1006,7 +1444,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "metadata": {}, "outputs": [], "source": [ @@ -1016,9 +1454,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 6\n", + "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_final_obs, blue_env.game.step_counter)" ] @@ -1034,7 +1493,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "metadata": {}, "outputs": [], "source": [ @@ -1066,16 +1525,200 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:27,571: Resetting environment, episode 1, avg. reward: 0.0\n", + "2025-02-03 16:04:27,574: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_1.json\n" + ] + }, + { + "data": { + "text/plain": [ + "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'ROUTER0': {'ACL': {1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 6: {'position': 5,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0}},\n", + " 'PORTS': {1: {'operating_status': 1},\n", + " 2: {'operating_status': 1},\n", + " 3: {'operating_status': 2}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'ICS': 0},\n", + " {})" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.reset()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "metadata": {}, "outputs": [], "source": [ @@ -1094,7 +1737,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "metadata": {}, "outputs": [], "source": [ @@ -1111,9 +1754,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "blue_env.step(0)\n", "web_server.software_manager.show()" @@ -1121,9 +1788,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 3\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -1137,9 +1821,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 44, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -1161,16 +1856,200 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 45, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:28,041: Resetting environment, episode 2, avg. reward: 0.0\n", + "2025-02-03 16:04:28,045: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_2.json\n" + ] + }, + { + "data": { + "text/plain": [ + "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'ROUTER0': {'ACL': {1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 6: {'position': 5,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0}},\n", + " 'PORTS': {1: {'operating_status': 1},\n", + " 2: {'operating_status': 1},\n", + " 3: {'operating_status': 2}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'ICS': 0},\n", + " {})" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.reset()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, "metadata": {}, "outputs": [], "source": [ @@ -1189,7 +2068,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 47, "metadata": {}, "outputs": [], "source": [ @@ -1206,9 +2085,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 48, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NodeOperatingState.SHUTTING_DOWN\n" + ] + } + ], "source": [ "web_server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "print(web_server.operating_state)" @@ -1216,18 +2103,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 49, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 2\n", + "root['NODES']['HOST0']['operating_status']: 1 -> 4\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", + "root['NODES']['HOST0']['NICS'][1]['nic_status']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 50, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", @@ -1251,16 +2168,200 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 51, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:28,560: Resetting environment, episode 3, avg. reward: 0.0\n", + "2025-02-03 16:04:28,564: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_3.json\n" + ] + }, + { + "data": { + "text/plain": [ + "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'ROUTER0': {'ACL': {1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 6: {'position': 5,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0}},\n", + " 'PORTS': {1: {'operating_status': 1},\n", + " 2: {'operating_status': 1},\n", + " 3: {'operating_status': 2}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'ICS': 0},\n", + " {})" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.reset()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ @@ -1279,7 +2380,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 53, "metadata": {}, "outputs": [], "source": [ @@ -1296,11 +2397,44 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 54, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+---------------------------------------------------------------+\n", + "| router_1 Network Interfaces |\n", + "+------+-------------------+-----------------+-------+----------+\n", + "| Port | MAC Address | Address | Speed | Status |\n", + "+------+-------------------+-----------------+-------+----------+\n", + "| 1 | dd:6e:95:d4:3f:74 | 192.168.1.1/24 | 100.0 | Enabled |\n", + "| 2 | 8b:79:07:fc:69:2c | 192.168.10.1/24 | 100.0 | Enabled |\n", + "| 3 | 1f:fd:c4:ae:7a:00 | 127.0.0.1/8 | 100.0 | Disabled |\n", + "| 4 | 7b:e3:bf:4b:76:e8 | 127.0.0.1/8 | 100.0 | Disabled |\n", + "| 5 | 4f:37:b0:6b:5d:44 | 127.0.0.1/8 | 100.0 | Disabled |\n", + "+------+-------------------+-----------------+-------+----------+\n", + "+------------------------------------------------------------------------------------------------------------------------+\n", + "| router_1 Access Control List |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| 1 | DENY | ANY | 192.168.10.21 | ANY | 80 | 192.168.1.12 | ANY | 80 | 0 |\n", + "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", + "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", + "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", + "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", + "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", + "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" + ] + } + ], "source": [ "router_1: Router = blue_env.game.simulation.network.get_node_by_hostname(\"router_1\")\n", + "router_1.show()\n", "router_1.acl.show()" ] }, @@ -1313,9 +2447,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 55, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.step(0)\n", "\n", @@ -1326,9 +2471,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 56, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+------------------------------------------------------------------------------------------------------------------------+\n", + "| router_1 Access Control List |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| 1 | DENY | ANY | 192.168.10.21 | ANY | 80 | 192.168.1.12 | ANY | 80 | 2 |\n", + "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", + "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", + "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", + "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", + "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", + "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" + ] + } + ], "source": [ "router_1.acl.show()" ] @@ -1342,18 +2508,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 57, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "web_server.software_manager.show()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 58, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------+\n", + "| database_server File System |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| database/database.db | 4.77 MB | GOOD | NONE | False |\n", + "| root | 0 B | GOOD | NONE | False |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -1361,9 +2567,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 59, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 3\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['NODES']['ROUTER0']['ACL'][1]['permission']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['source_ip_id']: 0 -> 7\n", + "root['NODES']['ROUTER0']['ACL'][1]['source_wildcard_id']: 0 -> 1\n", + "root['NODES']['ROUTER0']['ACL'][1]['source_port_id']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['dest_ip_id']: 0 -> 3\n", + "root['NODES']['ROUTER0']['ACL'][1]['dest_wildcard_id']: 0 -> 1\n", + "root['NODES']['ROUTER0']['ACL'][1]['dest_port_id']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['protocol_id']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -1427,9 +2657,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 60, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:29,610: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -1450,7 +2688,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 61, "metadata": {}, "outputs": [], "source": [ @@ -1478,9 +2716,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 62, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "env.step(2) # Agent Action Equivalent to c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "env.step(3) # Agent action Equivalent to c2_beacon.establish()\n", @@ -1497,9 +2756,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 63, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", "env.step(3)\n", @@ -1517,9 +2797,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 64, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "for i in range(6):\n", " env.step(0)\n", @@ -1542,9 +2836,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 65, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:30,011: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -1560,7 +2862,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 66, "metadata": {}, "outputs": [], "source": [ @@ -1584,9 +2886,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 67, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "c2_beacon.establish()\n", @@ -1604,9 +2920,72 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 68, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 4\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 5\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 6\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 7\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 8\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 9\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 10\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 11\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 12\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 13\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "for i in range(10):\n", " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", @@ -1622,9 +3001,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 69, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 0 | 1 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=1)\n", "c2_beacon.establish()\n", @@ -1640,9 +3033,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 70, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 14\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 15\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "# Comparing the OBS of the default frequency to a timestep frequency of 1\n", "for i in range(2):\n", @@ -1661,9 +3085,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 71, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 16\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 17\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 18\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 19\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 20\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 21\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 22\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=7)\n", "\n", @@ -1700,9 +3167,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 72, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-03 16:04:30,864: Resetting environment, episode 0, avg. reward: 0.0\n", + "2025-02-03 16:04:30,867: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_0.json\n" + ] + } + ], "source": [ "blue_config_env.reset()\n", "\n", @@ -1724,9 +3200,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 73, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 5\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "# Capturing default C2 Traffic\n", "for i in range(3):\n", @@ -1744,9 +3239,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 74, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 0 | 5 | udp | 53 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", "from primaite.utils.validation.port import PORT_LOOKUP\n", @@ -1759,9 +3268,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 75, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 10\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n" + ] + } + ], "source": [ "# Capturing UDP C2 Traffic\n", "for i in range(5):\n", From c1a5a26ffca6beb906bad50c0855d6a6dbc9252c Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 4 Feb 2025 10:21:56 +0000 Subject: [PATCH 178/224] #2887 - Actioning review comments --- src/primaite/simulator/core.py | 2 +- .../simulator/network/hardware/base.py | 8 ++++---- .../network/hardware/nodes/host/computer.py | 4 ++-- .../network/hardware/nodes/host/host_node.py | 4 ++-- .../network/hardware/nodes/host/server.py | 8 ++++---- .../network/hardware/nodes/network/firewall.py | 4 ++-- .../network/hardware/nodes/network/router.py | 18 +++++++++--------- .../network/hardware/nodes/network/switch.py | 4 ++-- .../hardware/nodes/network/wireless_router.py | 4 ++-- .../system/services/dns/dns_client.py | 4 ++-- .../system/services/ftp/ftp_server.py | 5 ++--- .../system/services/ntp/ntp_client.py | 4 +--- 12 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index dc4ae73b..567a0493 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -3,7 +3,7 @@ """Core of the PrimAITE Simulator.""" import warnings from abc import abstractmethod -from typing import Any, Callable, Dict, Iterable, List, Literal, Optional, Tuple, Union +from typing import Callable, Dict, Iterable, List, Literal, Optional, Tuple, Union from uuid import uuid4 from prettytable import PrettyTable diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 0564e1f3..36623a6f 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1525,16 +1525,13 @@ class Node(SimComponent, ABC): _identifier: ClassVar[str] = "unknown" """Identifier for this particular class, used for printing and logging. Each subclass redefines this.""" - config: Node.ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) - """Configuration items within Node""" - class ConfigSchema(BaseModel, ABC): """Configuration Schema for Node based classes.""" model_config = ConfigDict(arbitrary_types_allowed=True) """Configure pydantic to allow arbitrary types, let the instance have attributes not present in the model.""" - hostname: str = "default" + hostname: str "The node hostname on the network." revealed_to_red: bool = False @@ -1572,6 +1569,9 @@ class Node(SimComponent, ABC): operating_state: Any = None + config: ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) + """Configuration items within Node""" + @property def dns_server(self) -> Optional[IPv4Address]: """Convenience method to access the dns_server IP.""" diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index 85857a44..a0450443 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -37,11 +37,11 @@ class Computer(HostNode, identifier="computer"): SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "FTPClient": FTPClient} - config: "Computer.ConfigSchema" = Field(default_factory=lambda: Computer.ConfigSchema()) - class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Computer class.""" hostname: str = "Computer" + config: ConfigSchema = Field(default_factory=lambda: Computer.ConfigSchema()) + pass diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 424e39f1..1aa482db 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -330,8 +330,6 @@ class HostNode(Node, identifier="HostNode"): network_interface: Dict[int, NIC] = {} "The NICs on the node by port id." - config: HostNode.ConfigSchema = Field(default_factory=lambda: HostNode.ConfigSchema()) - class ConfigSchema(Node.ConfigSchema): """Configuration Schema for HostNode class.""" @@ -339,6 +337,8 @@ class HostNode(Node, identifier="HostNode"): subnet_mask: IPV4Address = "255.255.255.0" ip_address: IPV4Address + config: ConfigSchema = Field(default_factory=lambda: HostNode.ConfigSchema()) + def __init__(self, **kwargs): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=kwargs["config"].ip_address, subnet_mask=kwargs["config"].subnet_mask)) diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index bdf4e8c2..3a9fc2f9 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -33,22 +33,22 @@ class Server(HostNode, identifier="server"): * Web Browser """ - config: "Server.ConfigSchema" = Field(default_factory=lambda: Server.ConfigSchema()) - class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Server class.""" hostname: str = "server" + config: ConfigSchema = Field(default_factory=lambda: Server.ConfigSchema()) + class Printer(HostNode, identifier="printer"): """Printer? I don't even know her!.""" # TODO: Implement printer-specific behaviour - config: "Printer.ConfigSchema" = Field(default_factory=lambda: Printer.ConfigSchema()) - class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Printer class.""" hostname: str = "printer" + + config: ConfigSchema = Field(default_factory=lambda: Printer.ConfigSchema()) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 99dd48c4..c4ddea58 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -100,14 +100,14 @@ class Firewall(Router, identifier="firewall"): _identifier: str = "firewall" - config: "Firewall.ConfigSchema" = Field(default_factory=lambda: Firewall.ConfigSchema()) - class ConfigSchema(Router.ConfigSchema): """Configuration Schema for Firewall 'Nodes' within PrimAITE.""" hostname: str = "firewall" num_ports: int = 0 + config: ConfigSchema = Field(default_factory=lambda: Firewall.ConfigSchema()) + def __init__(self, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(kwargs["config"].hostname) diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index dd32fa31..7138cf4f 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -7,7 +7,7 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable -from pydantic import validate_call +from pydantic import Field, validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent @@ -1201,6 +1201,14 @@ class Router(NetworkNode, identifier="router"): RouteTable, RouterARP, and RouterICMP services. """ + class ConfigSchema(NetworkNode.ConfigSchema): + """Configuration Schema for Routers.""" + + hostname: str = "router" + num_ports: int = 5 + + config: ConfigSchema = Field(default_factory=lambda: Router.ConfigSchema()) + SYSTEM_SOFTWARE: ClassVar[Dict] = { "UserSessionManager": UserSessionManager, "UserManager": UserManager, @@ -1214,14 +1222,6 @@ class Router(NetworkNode, identifier="router"): acl: AccessControlList route_table: RouteTable - config: "Router.ConfigSchema" - - class ConfigSchema(NetworkNode.ConfigSchema): - """Configuration Schema for Routers.""" - - hostname: str = "router" - num_ports: int = 5 - def __init__(self, **kwargs): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(kwargs["config"].hostname) diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 3cb335f7..54e1d7ef 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -98,8 +98,6 @@ class Switch(NetworkNode, identifier="switch"): mac_address_table: Dict[str, SwitchPort] = {} "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." - config: "Switch.ConfigSchema" = Field(default_factory=lambda: Switch.ConfigSchema()) - class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Switch nodes within PrimAITE.""" @@ -107,6 +105,8 @@ class Switch(NetworkNode, identifier="switch"): num_ports: int = 24 "The number of ports on the switch. Default is 24." + config: ConfigSchema = Field(default_factory=lambda: Switch.ConfigSchema()) + def __init__(self, **kwargs): super().__init__(**kwargs) for i in range(1, kwargs["config"].num_ports + 1): diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 348c2aaa..2beb03d6 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -123,8 +123,6 @@ class WirelessRouter(Router, identifier="wireless_router"): network_interfaces: Dict[str, Union[RouterInterface, WirelessAccessPoint]] = {} network_interface: Dict[int, Union[RouterInterface, WirelessAccessPoint]] = {} - config: "WirelessRouter.ConfigSchema" = Field(default_factory=lambda: WirelessRouter.ConfigSchema()) - class ConfigSchema(Router.ConfigSchema): """Configuration Schema for WirelessRouter nodes within PrimAITE.""" @@ -132,6 +130,8 @@ class WirelessRouter(Router, identifier="wireless_router"): airspace: AirSpace num_ports: int = 0 + config: ConfigSchema = Field(default_factory=lambda: WirelessRouter.ConfigSchema()) + def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 4a1fd292..4f926a25 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -25,12 +25,12 @@ class DNSClient(Service, identifier="DNSClient"): """ConfigSchema for DNSClient.""" type: str = "DNSClient" - dns_server: Optional[IPV4Address] = None dns_server: Optional[IPv4Address] = None "The DNS Server the client sends requests to." - config: "DNSClient.ConfigSchema" = Field(default_factory=lambda: DNSClient.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: DNSClient.ConfigSchema()) + dns_cache: Dict[str, IPv4Address] = {} "A dict of known mappings between domain/URLs names and IPv4 addresses." diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index ebb93a7b..147d2dbb 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -20,17 +20,16 @@ class FTPServer(FTPServiceABC, identifier="FTPServer"): RFC 959: https://datatracker.ietf.org/doc/html/rfc959 """ - config: "FTPServer.ConfigSchema" = Field(default_factory=lambda: FTPServer.ConfigSchema()) - class ConfigSchema(FTPServiceABC.ConfigSchema): """ConfigSchema for FTPServer.""" type: str = "FTPServer" - server_password: Optional[str] = None server_password: Optional[str] = None """Password needed to connect to FTP server. Default is None.""" + config: ConfigSchema = Field(default_factory=lambda: FTPServer.ConfigSchema()) + def __init__(self, **kwargs): kwargs["name"] = "FTPServer" kwargs["port"] = PORT_LOOKUP["FTP"] diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 72e8f6c0..7c3efd25 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -9,7 +9,6 @@ from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP -from primaite.utils.validation.ipv4_address import IPV4Address from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) @@ -22,12 +21,11 @@ class NTPClient(Service, identifier="NTPClient"): """ConfigSchema for NTPClient.""" type: str = "NTPClient" - ntp_server_ip: Optional[IPV4Address] = None ntp_server_ip: Optional[IPv4Address] = None "The NTP server the client sends requests to." - config: "NTPClient.ConfigSchema" = Field(default_factory=lambda: NTPClient.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: NTPClient.ConfigSchema()) time: Optional[datetime] = None From 961136fb4213d2274800760ff220d79bf5f59382 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 4 Feb 2025 10:41:51 +0000 Subject: [PATCH 179/224] #2887 - Updates to extensible_nodes.rst --- docs/source/how_to_guides/extensible_nodes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 78ee550e..6651b618 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -53,4 +53,4 @@ class Router(NetworkNode, identifier="router"): Changes to YAML file. ===================== -Nodes defined within configuration YAML files for use with PrimAITE 3.X should still be compatible following these changes. +While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. From 454bd61fb27e2e8b8702d86c0e83806deae7cd0c Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 4 Feb 2025 10:50:53 +0000 Subject: [PATCH 180/224] #3075: Update internal YAML fragments in C2C-E2E-Demo notebook. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 1802 ++--------------- 1 file changed, 137 insertions(+), 1665 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 2c1a94c8..c2a87e45 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -13,24 +13,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:19,828: Performing the PrimAITE first-time setup...\n", - "2025-02-03 16:04:19,829: Building the PrimAITE app directories...\n", - "2025-02-03 16:04:19,829: Building primaite_config.yaml...\n", - "2025-02-03 16:04:19,829: Rebuilding the demo notebooks...\n", - "/home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", - "2025-02-03 16:04:19,831: Reset example notebook: /home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", - "2025-02-03 16:04:19,836: Rebuilding the example notebooks...\n", - "2025-02-03 16:04:19,840: PrimAITE setup complete!\n" - ] - } - ], + "outputs": [], "source": [ "!primaite setup" ] @@ -183,17 +168,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:24,734: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -219,35 +196,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------------------------------------------------------------------------+\n", - "| client_1 Software Manager |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", - "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", @@ -297,34 +248,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | INSTALLING | UNUSED | None | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(1)\n", "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", @@ -364,41 +290,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(2)\n", "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", @@ -437,54 +331,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=2, action='node_application_execute', parameters={'node_name': 'web_server', 'application_name': 'C2Beacon'}, request=['network', 'node', 'web_server', 'application', 'C2Beacon', 'execute'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(3)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 1 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.1.12 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.show()\n", "c2_server.show()" @@ -547,59 +405,18 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=3, action='c2_server_terminal_command', parameters={'node_name': 'client_1', 'ip_address': None, 'username': 'admin', 'password': 'admin', 'commands': [['software_manager', 'application', 'install', 'RansomwareScript']]}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'terminal_command', {'commands': [['software_manager', 'application', 'install', 'RansomwareScript']], 'ip_address': None, 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={0: RequestResponse(status='success', data={})}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(4)" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------------------------------------------------------------------------+\n", - "| client_1 Software Manager |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", - "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "client_1.software_manager.show()" ] @@ -638,66 +455,18 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=4, action='c2_server_ransomware_configure', parameters={'node_name': 'client_1', 'server_ip_address': '192.168.1.14', 'payload': 'ENCRYPT'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_configure', {'server_ip_address': '192.168.1.14', 'server_password': None, 'payload': 'ENCRYPT'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(5)" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", - "| RansomwareScript | Application | RUNNING | GOOD | None | none |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "+------------------------------------+\n", - "| RansomwareScript Running Status |\n", - "+--------------------------+---------+\n", - "| Target Server IP Address | Payload |\n", - "+--------------------------+---------+\n", - "| 192.168.1.14 | ENCRYPT |\n", - "+--------------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", "web_server.software_manager.show()\n", @@ -743,48 +512,18 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=5, action='c2_server_data_exfiltrate', parameters={'node_name': 'client_1', 'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'exfiltrate', {'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(6)" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------------------------------------------------------------------+\n", - "| client_1 File System |\n", - "+--------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+--------------------+---------+---------------+-----------------------+---------+\n", - "| root | 0 B | GOOD | NONE | False |\n", - "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", - "+--------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.file_system.show(full=True)" @@ -792,25 +531,9 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+---------------------------------------------------------------------------------+\n", - "| web_server File System |\n", - "+---------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+---------------------+---------+---------------+-----------------------+---------+\n", - "| primaite/index.html | 15.0 KB | GOOD | NONE | False |\n", - "| root | 0 B | GOOD | NONE | False |\n", - "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", - "+---------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "web_server.software_manager.file_system.show(full=True)" @@ -847,48 +570,18 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=6, action='c2_server_ransomware_launch', parameters={'node_name': 'client_1'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_launch'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(7)" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------+\n", - "| database_server File System |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| database/database.db | 4.77 MB | CORRUPT | NONE | False |\n", - "| root | 0 B | GOOD | NONE | False |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "database_server: Server = env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -907,7 +600,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1025,30 +718,30 @@ "\n", "\n", " # options:\n", - " # nodes:\n", - " # - node_name: web_server\n", - " # applications:\n", - " # - application_name: C2Beacon\n", + " # # nodes:\n", + " # node_name: web_server\n", + " # # applications:\n", + " # application_name: C2Beacon\n", "\n", - " # - node_name: database_server\n", - " # folders:\n", - " # - folder_name: database\n", - " # files:\n", - " # - file_name: database.db\n", - " # services:\n", - " # - service_name: DatabaseService\n", - " # - node_name: router_1\n", + " # node_name: database_server\n", + " # folders:\n", + " # - folder_name: database\n", + " # files:\n", + " # - file_name: database.db\n", + " # services:\n", + " # - service_name: DatabaseService\n", + " # node_name: router_1\n", "\n", - " # max_folders_per_node: 2\n", - " # max_files_per_folder: 2\n", - " # max_services_per_node: 2\n", - " # max_nics_per_node: 8\n", - " # max_acl_rules: 10\n", - " # ip_list:\n", - " # - 192.168.10.21\n", - " # - 192.168.1.12\n", - " # wildcard_list:\n", - " # - 0.0.0.1\n", + " # max_folders_per_node: 2\n", + " # max_files_per_folder: 2\n", + " # max_services_per_node: 2\n", + " # max_nics_per_node: 8\n", + " # max_acl_rules: 10\n", + " # ip_list:\n", + " # - 192.168.10.21\n", + " # - 192.168.1.12\n", + " # wildcard_list:\n", + " # - 0.0.0.1\n", " reward_function:\n", " reward_components:\n", " - type: DUMMY\n", @@ -1061,17 +754,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:26,020: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -1128,18 +813,9 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:26,440: Resetting environment, episode 0, avg. reward: 0.0\n", - "2025-02-03 16:04:26,445: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_0.json\n" - ] - } - ], + "outputs": [], "source": [ "# Resetting the environment and capturing the default observation space.\n", "blue_env.reset()\n", @@ -1148,20 +824,9 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Setting up the C2 Suite via the simulation API.\n", "\n", @@ -1192,26 +857,9 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 2\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" ] @@ -1231,20 +879,9 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={0: RequestResponse(status='success', data={})})" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -1255,20 +892,9 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -1287,28 +913,9 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 7\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", - "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 0 -> 3\n", - "root['NODES']['HOST0']['users']['local_login']: 0 -> 1\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" ] @@ -1340,20 +947,9 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options)" ] @@ -1369,22 +965,9 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 7\n", - "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", - "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_exfil_obs, env.game.step_counter)" ] @@ -1400,20 +983,9 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -1422,20 +994,9 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", "blue_env.step(0)\n", @@ -1454,30 +1015,9 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 6\n", - "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_final_obs, blue_env.game.step_counter)" ] @@ -1525,193 +1065,9 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:27,571: Resetting environment, episode 1, avg. reward: 0.0\n", - "2025-02-03 16:04:27,574: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_1.json\n" - ] - }, - { - "data": { - "text/plain": [ - "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'ROUTER0': {'ACL': {1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 6: {'position': 5,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0}},\n", - " 'PORTS': {1: {'operating_status': 1},\n", - " 2: {'operating_status': 1},\n", - " 3: {'operating_status': 2}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'ICS': 0},\n", - " {})" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.reset()" ] @@ -1754,33 +1110,9 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "blue_env.step(0)\n", "web_server.software_manager.show()" @@ -1788,26 +1120,9 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 3\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -1821,20 +1136,9 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -1856,193 +1160,9 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:28,041: Resetting environment, episode 2, avg. reward: 0.0\n", - "2025-02-03 16:04:28,045: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_2.json\n" - ] - }, - { - "data": { - "text/plain": [ - "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'ROUTER0': {'ACL': {1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 6: {'position': 5,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0}},\n", - " 'PORTS': {1: {'operating_status': 1},\n", - " 2: {'operating_status': 1},\n", - " 3: {'operating_status': 2}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'ICS': 0},\n", - " {})" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.reset()" ] @@ -2085,17 +1205,9 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "NodeOperatingState.SHUTTING_DOWN\n" - ] - } - ], + "outputs": [], "source": [ "web_server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "print(web_server.operating_state)" @@ -2103,48 +1215,18 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 2\n", - "root['NODES']['HOST0']['operating_status']: 1 -> 4\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", - "root['NODES']['HOST0']['NICS'][1]['nic_status']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] }, { "cell_type": "code", - "execution_count": 50, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", @@ -2168,193 +1250,9 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:28,560: Resetting environment, episode 3, avg. reward: 0.0\n", - "2025-02-03 16:04:28,564: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_3.json\n" - ] - }, - { - "data": { - "text/plain": [ - "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'ROUTER0': {'ACL': {1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 6: {'position': 5,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0}},\n", - " 'PORTS': {1: {'operating_status': 1},\n", - " 2: {'operating_status': 1},\n", - " 3: {'operating_status': 2}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'ICS': 0},\n", - " {})" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.reset()" ] @@ -2397,41 +1295,9 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+---------------------------------------------------------------+\n", - "| router_1 Network Interfaces |\n", - "+------+-------------------+-----------------+-------+----------+\n", - "| Port | MAC Address | Address | Speed | Status |\n", - "+------+-------------------+-----------------+-------+----------+\n", - "| 1 | dd:6e:95:d4:3f:74 | 192.168.1.1/24 | 100.0 | Enabled |\n", - "| 2 | 8b:79:07:fc:69:2c | 192.168.10.1/24 | 100.0 | Enabled |\n", - "| 3 | 1f:fd:c4:ae:7a:00 | 127.0.0.1/8 | 100.0 | Disabled |\n", - "| 4 | 7b:e3:bf:4b:76:e8 | 127.0.0.1/8 | 100.0 | Disabled |\n", - "| 5 | 4f:37:b0:6b:5d:44 | 127.0.0.1/8 | 100.0 | Disabled |\n", - "+------+-------------------+-----------------+-------+----------+\n", - "+------------------------------------------------------------------------------------------------------------------------+\n", - "| router_1 Access Control List |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| 1 | DENY | ANY | 192.168.10.21 | ANY | 80 | 192.168.1.12 | ANY | 80 | 0 |\n", - "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", - "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", - "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", - "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", - "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", - "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "router_1: Router = blue_env.game.simulation.network.get_node_by_hostname(\"router_1\")\n", "router_1.show()\n", @@ -2447,20 +1313,9 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.step(0)\n", "\n", @@ -2471,30 +1326,9 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------------------------------------------------------------------------------------------------------------------+\n", - "| router_1 Access Control List |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| 1 | DENY | ANY | 192.168.10.21 | ANY | 80 | 192.168.1.12 | ANY | 80 | 2 |\n", - "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", - "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", - "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", - "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", - "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", - "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "router_1.acl.show()" ] @@ -2508,58 +1342,18 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "web_server.software_manager.show()" ] }, { "cell_type": "code", - "execution_count": 58, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------+\n", - "| database_server File System |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| database/database.db | 4.77 MB | GOOD | NONE | False |\n", - "| root | 0 B | GOOD | NONE | False |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -2567,33 +1361,9 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 3\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['NODES']['ROUTER0']['ACL'][1]['permission']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['source_ip_id']: 0 -> 7\n", - "root['NODES']['ROUTER0']['ACL'][1]['source_wildcard_id']: 0 -> 1\n", - "root['NODES']['ROUTER0']['ACL'][1]['source_port_id']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['dest_ip_id']: 0 -> 3\n", - "root['NODES']['ROUTER0']['ACL'][1]['dest_wildcard_id']: 0 -> 1\n", - "root['NODES']['ROUTER0']['ACL'][1]['dest_port_id']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['protocol_id']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -2657,17 +1427,9 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:29,610: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -2716,30 +1478,9 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(2) # Agent Action Equivalent to c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "env.step(3) # Agent action Equivalent to c2_beacon.establish()\n", @@ -2756,30 +1497,9 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", "env.step(3)\n", @@ -2797,23 +1517,9 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "for i in range(6):\n", " env.step(0)\n", @@ -2836,17 +1542,9 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:30,011: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -2886,23 +1584,9 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "c2_beacon.establish()\n", @@ -2920,72 +1604,9 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 4\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 5\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 6\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 7\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 8\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 9\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 10\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 11\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 12\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 13\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "for i in range(10):\n", " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", @@ -3001,23 +1622,9 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 0 | 1 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=1)\n", "c2_beacon.establish()\n", @@ -3033,40 +1640,9 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 14\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 15\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "# Comparing the OBS of the default frequency to a timestep frequency of 1\n", "for i in range(2):\n", @@ -3085,52 +1661,9 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 16\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 17\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 18\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 19\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 20\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 21\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 22\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=7)\n", "\n", @@ -3167,18 +1700,9 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-03 16:04:30,864: Resetting environment, episode 0, avg. reward: 0.0\n", - "2025-02-03 16:04:30,867: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-03/16-04-20/agent_actions/episode_0.json\n" - ] - } - ], + "outputs": [], "source": [ "blue_config_env.reset()\n", "\n", @@ -3200,28 +1724,9 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 5\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "# Capturing default C2 Traffic\n", "for i in range(3):\n", @@ -3239,23 +1744,9 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 0 | 5 | udp | 53 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", "from primaite.utils.validation.port import PORT_LOOKUP\n", @@ -3268,28 +1759,9 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 10\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "# Capturing UDP C2 Traffic\n", "for i in range(5):\n", From 05946431ca5eac277e6269a8d2c172cd0b084cf8 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 4 Feb 2025 11:19:13 +0000 Subject: [PATCH 181/224] #2887 - Correct type in documentation --- docs/source/how_to_guides/extensible_nodes.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 6651b618..043d0f06 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -18,8 +18,7 @@ Node classes all inherit from the base Node Class, though new classes should inh The use of an `__init__` method is not necessary, as configurable variables for the class should be specified within the `config` of the class, and passed at run time via your YAML configuration using the `from_config` method. - -An example of how additional Node classes is below, taken from `router.py` withing PrimAITE. +An example of how additional Node classes is below, taken from `router.py` within PrimAITE. .. code-block:: Python From 99e38fbbc2277c981abd904be44ea9311843cfc9 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 4 Feb 2025 14:25:26 +0000 Subject: [PATCH 182/224] #2887 - Removal of un-necessary code and cleanup following review comments --- src/primaite/simulator/network/creation.py | 3 +-- src/primaite/simulator/network/hardware/base.py | 8 -------- .../simulator/network/hardware/nodes/network/switch.py | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 5e0d0ce8..009ac861 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -167,7 +167,6 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): # Optionally include a router in the LAN if config.include_router: default_gateway = IPv4Address(f"192.168.{config.subnet_base}.1") - # router = Router(hostname=f"router_{config.lan_name}", start_up_duration=0) router = Router.from_config( config={"hostname": f"router_{config.lan_name}", "type": "router", "start_up_duration": 0} ) @@ -230,7 +229,7 @@ class OfficeLANAdder(NetworkNodeAdder, identifier="office_lan"): "type": "computer", "hostname": f"pc_{i}_{config.lan_name}", "ip_address": f"192.168.{config.subnet_base}.{i+config.pcs_ip_block_start-1}", - "default_gateway": "192.168.10.1", + "default_gateway": default_gateway, "start_up_duration": 0, } pc = Computer.from_config(config=pc_cfg) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 36623a6f..29d46164 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -2242,10 +2242,6 @@ class Node(SimComponent, ABC): for app_id in self.applications: self.applications[app_id].close() - # Turn off all processes in the node - # for process_id in self.processes: - # self.processes[process_id] - def _start_up_actions(self): """Actions to perform when the node is starting up.""" # Turn on all the services in the node @@ -2258,10 +2254,6 @@ class Node(SimComponent, ABC): print(f"Starting application:{self.applications[app_id].config.type}") self.applications[app_id].run() - # Turn off all processes in the node - # for process_id in self.processes: - # self.processes[process_id] - def _install_system_software(self) -> None: """Preinstall required software.""" for _, software_class in self.SYSTEM_SOFTWARE.items(): diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 54e1d7ef..8a9fdb24 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -109,7 +109,7 @@ class Switch(NetworkNode, identifier="switch"): def __init__(self, **kwargs): super().__init__(**kwargs) - for i in range(1, kwargs["config"].num_ports + 1): + for i in range(1, self.config.num_ports + 1): self.connect_nic(SwitchPort()) def _install_system_software(self): From 51bb3f5b07deede2506bb242eb8586e8307aedf4 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 4 Feb 2025 14:26:55 +0000 Subject: [PATCH 183/224] #2887 - Removal of un-necessary print statement that was used for debugging --- src/primaite/simulator/network/hardware/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 29d46164..6543d793 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -2250,8 +2250,6 @@ class Node(SimComponent, ABC): # Turn on all the applications in the node for app_id in self.applications: - print(app_id) - print(f"Starting application:{self.applications[app_id].config.type}") self.applications[app_id].run() def _install_system_software(self) -> None: From 24161bb3fc038ed946ee1bebf57711d1b443e65e Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 4 Feb 2025 15:06:23 +0000 Subject: [PATCH 184/224] #2887 - Removal of commented out code --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c1d44aec..a1fde7dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -246,7 +246,6 @@ def example_network() -> Network: switch_1 = Switch.from_config(config=switch_1_cfg) - # switch_1 = Switch(hostname="switch_1", num_ports=8, start_up_duration=0) switch_1.power_on() network.connect(endpoint_a=router_1.network_interface[1], endpoint_b=switch_1.network_interface[8]) @@ -254,7 +253,6 @@ def example_network() -> Network: # Switch 2 switch_2_config = {"hostname": "switch_2", "type": "switch", "num_ports": 8, "start_up_duration": 0} - # switch_2 = Switch(hostname="switch_2", num_ports=8, start_up_duration=0) switch_2 = Switch.from_config(config=switch_2_config) switch_2.power_on() network.connect(endpoint_a=router_1.network_interface[2], endpoint_b=switch_2.network_interface[8]) From f7c6ee3df43bc1d46af90dbac0f321a1e911e524 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 4 Feb 2025 15:45:19 +0000 Subject: [PATCH 185/224] #3075: C2C-E2E-Demo wildcard_list changes. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 1822 +++++++++++++++-- 1 file changed, 1645 insertions(+), 177 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index c2a87e45..b8d1423f 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -13,9 +13,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:30,830: Performing the PrimAITE first-time setup...\n", + "2025-02-04 15:37:30,830: Building the PrimAITE app directories...\n", + "2025-02-04 15:37:30,830: Building primaite_config.yaml...\n", + "2025-02-04 15:37:30,830: Rebuilding the demo notebooks...\n", + "/home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", + "2025-02-04 15:37:30,832: Reset example notebook: /home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", + "2025-02-04 15:37:30,838: Rebuilding the example notebooks...\n", + "2025-02-04 15:37:30,841: PrimAITE setup complete!\n" + ] + } + ], "source": [ "!primaite setup" ] @@ -62,24 +77,6 @@ " type: ProxyAgent\n", "\n", " action_space:\n", - " # options:\n", - " # nodes:\n", - " # - node_name: web_server\n", - " # applications:\n", - " # - application_name: C2Beacon\n", - " # - node_name: client_1\n", - " # applications:\n", - " # - application_name: C2Server\n", - " # max_folders_per_node: 1\n", - " # max_files_per_folder: 1\n", - " # max_services_per_node: 2\n", - " # max_nics_per_node: 8\n", - " # max_acl_rules: 10\n", - " # ip_list:\n", - " # - 192.168.1.21\n", - " # - 192.168.1.14\n", - " # wildcard_list:\n", - " # - 0.0.0.1\n", " action_map:\n", " 0:\n", " action: do_nothing\n", @@ -93,11 +90,7 @@ " action: configure_c2_beacon\n", " options:\n", " node_name: web_server\n", - " # config:\n", " c2_server_ip_address: 192.168.10.21\n", - " # keep_alive_frequency: 10\n", - " # masquerade_protocol: TCP\n", - " # masquerade_port: DNS\n", " 3:\n", " action: node_application_execute\n", " options:\n", @@ -108,7 +101,6 @@ " options:\n", " node_name: client_1\n", " ip_address:\n", - " # account:\n", " username: admin\n", " password: admin\n", " commands:\n", @@ -121,7 +113,6 @@ " action: c2_server_ransomware_configure\n", " options:\n", " node_name: client_1\n", - " # config:\n", " server_ip_address: 192.168.1.14\n", " payload: ENCRYPT\n", " 6:\n", @@ -132,7 +123,6 @@ " target_folder_name: \"database\"\n", " exfiltration_folder_name: \"spoils\"\n", " target_ip_address: 192.168.1.14\n", - " # account:\n", " username: admin\n", " password: admin\n", "\n", @@ -144,20 +134,12 @@ " action: configure_c2_beacon\n", " options:\n", " node_name: web_server\n", - " # config:\n", " c2_server_ip_address: 192.168.10.21\n", - " # keep_alive_frequency: 10\n", - " # masquerade_protocol: TCP\n", - " # masquerade_port: DNS\n", " 9:\n", " action: configure_c2_beacon\n", " options:\n", " node_name: web_server\n", - " # config:\n", " c2_server_ip_address: 192.168.10.22\n", - " # keep_alive_frequency: 10\n", - " # masquerade_protocol: TCP\n", - " # masquerade_port: DNS\n", "\n", " reward_function:\n", " reward_components:\n", @@ -168,9 +150,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:35,293: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -196,9 +186,35 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------------------------------------------------------------------------------+\n", + "| client_1 Software Manager |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", + "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", @@ -248,9 +264,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | INSTALLING | UNUSED | None | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "env.step(1)\n", "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", @@ -290,9 +331,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "env.step(2)\n", "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", @@ -331,18 +404,54 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=2, action='node_application_execute', parameters={'node_name': 'web_server', 'application_name': 'C2Beacon'}, request=['network', 'node', 'web_server', 'application', 'C2Beacon', 'execute'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(3)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 1 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.1.12 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "c2_beacon.show()\n", "c2_server.show()" @@ -405,18 +514,59 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=3, action='c2_server_terminal_command', parameters={'node_name': 'client_1', 'ip_address': None, 'username': 'admin', 'password': 'admin', 'commands': [['software_manager', 'application', 'install', 'RansomwareScript']]}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'terminal_command', {'commands': [['software_manager', 'application', 'install', 'RansomwareScript']], 'ip_address': None, 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={0: RequestResponse(status='success', data={})}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(4)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------------------------------------------------------------------------------+\n", + "| client_1 Software Manager |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", + "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", + "+---------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "client_1.software_manager.show()" ] @@ -455,18 +605,66 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=4, action='c2_server_ransomware_configure', parameters={'node_name': 'client_1', 'server_ip_address': '192.168.1.14', 'payload': 'ENCRYPT'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_configure', {'server_ip_address': '192.168.1.14', 'server_password': None, 'payload': 'ENCRYPT'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(5)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", + "| RansomwareScript | Application | RUNNING | GOOD | None | none |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "+------------------------------------+\n", + "| RansomwareScript Running Status |\n", + "+--------------------------+---------+\n", + "| Target Server IP Address | Payload |\n", + "+--------------------------+---------+\n", + "| 192.168.1.14 | ENCRYPT |\n", + "+--------------------------+---------+\n" + ] + } + ], "source": [ "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", "web_server.software_manager.show()\n", @@ -512,18 +710,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=5, action='c2_server_data_exfiltrate', parameters={'node_name': 'client_1', 'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'exfiltrate', {'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(6)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------------------------------------------------------------------------------+\n", + "| client_1 File System |\n", + "+--------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+--------------------+---------+---------------+-----------------------+---------+\n", + "| root | 0 B | GOOD | NONE | False |\n", + "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", + "+--------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.file_system.show(full=True)" @@ -531,9 +759,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+---------------------------------------------------------------------------------+\n", + "| web_server File System |\n", + "+---------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+---------------------+---------+---------------+-----------------------+---------+\n", + "| primaite/index.html | 15.0 KB | GOOD | NONE | False |\n", + "| root | 0 B | GOOD | NONE | False |\n", + "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", + "+---------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "web_server.software_manager.file_system.show(full=True)" @@ -570,18 +814,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(0,\n", + " 0.0,\n", + " False,\n", + " False,\n", + " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=6, action='c2_server_ransomware_launch', parameters={'node_name': 'client_1'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_launch'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.step(7)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------+\n", + "| database_server File System |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| database/database.db | 4.77 MB | CORRUPT | NONE | False |\n", + "| root | 0 B | GOOD | NONE | False |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "database_server: Server = env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -600,7 +874,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -713,35 +987,9 @@ " src_port: HTTP\n", " dst_port: HTTP\n", " protocol_name: ALL\n", - " src_wildcard: NONE\n", - " dst_wildcard: NONE\n", + " src_wildcard: 0.0.0.1\n", + " dst_wildcard: 0.0.0.1\n", "\n", - "\n", - " # options:\n", - " # # nodes:\n", - " # node_name: web_server\n", - " # # applications:\n", - " # application_name: C2Beacon\n", - "\n", - " # node_name: database_server\n", - " # folders:\n", - " # - folder_name: database\n", - " # files:\n", - " # - file_name: database.db\n", - " # services:\n", - " # - service_name: DatabaseService\n", - " # node_name: router_1\n", - "\n", - " # max_folders_per_node: 2\n", - " # max_files_per_folder: 2\n", - " # max_services_per_node: 2\n", - " # max_nics_per_node: 8\n", - " # max_acl_rules: 10\n", - " # ip_list:\n", - " # - 192.168.10.21\n", - " # - 192.168.1.12\n", - " # wildcard_list:\n", - " # - 0.0.0.1\n", " reward_function:\n", " reward_components:\n", " - type: DUMMY\n", @@ -754,9 +1002,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:36,689: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -813,9 +1069,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:37,079: Resetting environment, episode 0, avg. reward: 0.0\n", + "2025-02-04 15:37:37,082: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_0.json\n" + ] + } + ], "source": [ "# Resetting the environment and capturing the default observation space.\n", "blue_env.reset()\n", @@ -824,9 +1089,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Setting up the C2 Suite via the simulation API.\n", "\n", @@ -857,9 +1133,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 2\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" ] @@ -879,9 +1172,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={0: RequestResponse(status='success', data={})})" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -892,9 +1196,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -913,9 +1228,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 7\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", + "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 0 -> 3\n", + "root['NODES']['HOST0']['users']['local_login']: 0 -> 1\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" ] @@ -947,9 +1281,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options)" ] @@ -965,9 +1310,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 7\n", + "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", + "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n" + ] + } + ], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_exfil_obs, env.game.step_counter)" ] @@ -983,9 +1341,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -994,9 +1363,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='success', data={})" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", "blue_env.step(0)\n", @@ -1015,9 +1395,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 6\n", + "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_final_obs, blue_env.game.step_counter)" ] @@ -1065,9 +1466,193 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:38,317: Resetting environment, episode 1, avg. reward: 0.0\n", + "2025-02-04 15:37:38,321: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_1.json\n" + ] + }, + { + "data": { + "text/plain": [ + "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'ROUTER0': {'ACL': {1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 6: {'position': 5,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0}},\n", + " 'PORTS': {1: {'operating_status': 1},\n", + " 2: {'operating_status': 1},\n", + " 3: {'operating_status': 2}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'ICS': 0},\n", + " {})" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.reset()" ] @@ -1110,9 +1695,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 42, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "blue_env.step(0)\n", "web_server.software_manager.show()" @@ -1120,9 +1729,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 3\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -1136,9 +1762,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 44, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -1160,9 +1797,193 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 45, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:38,778: Resetting environment, episode 2, avg. reward: 0.0\n", + "2025-02-04 15:37:38,781: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_2.json\n" + ] + }, + { + "data": { + "text/plain": [ + "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'ROUTER0': {'ACL': {1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 6: {'position': 5,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0}},\n", + " 'PORTS': {1: {'operating_status': 1},\n", + " 2: {'operating_status': 1},\n", + " 3: {'operating_status': 2}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'ICS': 0},\n", + " {})" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.reset()" ] @@ -1205,9 +2026,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 48, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NodeOperatingState.SHUTTING_DOWN\n" + ] + } + ], "source": [ "web_server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "print(web_server.operating_state)" @@ -1215,18 +2044,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 49, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 2\n", + "root['NODES']['HOST0']['operating_status']: 1 -> 4\n", + "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", + "root['NODES']['HOST0']['NICS'][1]['nic_status']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 50, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", @@ -1250,9 +2109,193 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 51, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:39,257: Resetting environment, episode 3, avg. reward: 0.0\n", + "2025-02-04 15:37:39,260: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_3.json\n" + ] + }, + { + "data": { + "text/plain": [ + "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 1,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", + " 'health_status': 0,\n", + " 'num_executions': 0},\n", + " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", + " 'FOLDERS': {1: {'health_status': 0,\n", + " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", + " 'NICS': {1: {'nic_status': 1,\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", + " 53: {'inbound': 0, 'outbound': 0},\n", + " 21: {'inbound': 0, 'outbound': 0}}}}},\n", + " 'num_file_creations': 0,\n", + " 'num_file_deletions': 0,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0},\n", + " 'operating_status': 1},\n", + " 'ROUTER0': {'ACL': {1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 6: {'position': 5,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_wildcard_id': 0,\n", + " 'source_port_id': 0,\n", + " 'dest_ip_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'protocol_id': 0}},\n", + " 'PORTS': {1: {'operating_status': 1},\n", + " 2: {'operating_status': 1},\n", + " 3: {'operating_status': 2}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'ICS': 0},\n", + " {})" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.reset()" ] @@ -1295,12 +2338,43 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 54, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+---------------------------------------------------------------+\n", + "| router_1 Network Interfaces |\n", + "+------+-------------------+-----------------+-------+----------+\n", + "| Port | MAC Address | Address | Speed | Status |\n", + "+------+-------------------+-----------------+-------+----------+\n", + "| 1 | dd:bc:17:75:a2:c4 | 192.168.1.1/24 | 100.0 | Enabled |\n", + "| 2 | 00:f2:f5:65:51:75 | 192.168.10.1/24 | 100.0 | Enabled |\n", + "| 3 | de:7d:ac:0e:58:e1 | 127.0.0.1/8 | 100.0 | Disabled |\n", + "| 4 | 8a:b4:55:12:c7:9e | 127.0.0.1/8 | 100.0 | Disabled |\n", + "| 5 | a5:ef:c6:53:97:c4 | 127.0.0.1/8 | 100.0 | Disabled |\n", + "+------+-------------------+-----------------+-------+----------+\n", + "+------------------------------------------------------------------------------------------------------------------------+\n", + "| router_1 Access Control List |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| 1 | DENY | ANY | 192.168.10.21 | 0.0.0.1 | 80 | 192.168.1.12 | 0.0.0.1 | 80 | 0 |\n", + "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", + "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", + "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", + "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", + "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", + "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" + ] + } + ], "source": [ "router_1: Router = blue_env.game.simulation.network.get_node_by_hostname(\"router_1\")\n", - "router_1.show()\n", "router_1.acl.show()" ] }, @@ -1313,9 +2387,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 55, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "blue_env.step(0)\n", "\n", @@ -1326,9 +2411,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 56, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+------------------------------------------------------------------------------------------------------------------------+\n", + "| router_1 Access Control List |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", + "| 1 | DENY | ANY | 192.168.10.21 | 0.0.0.1 | 80 | 192.168.1.12 | 0.0.0.1 | 80 | 2 |\n", + "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", + "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", + "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", + "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", + "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", + "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", + "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" + ] + } + ], "source": [ "router_1.acl.show()" ] @@ -1342,18 +2448,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 57, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-------------------------------------------------------------------------------------+\n", + "| web_server Software Manager |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| Name | Type | Operating State | Health State | Port | Protocol |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n", + "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", + "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", + "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", + "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", + "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", + "| NMAP | Application | RUNNING | GOOD | None | none |\n", + "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", + "| UserManager | Service | RUNNING | GOOD | None | none |\n", + "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", + "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", + "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", + "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", + "+--------------------+-------------+-----------------+--------------+------+----------+\n" + ] + } + ], "source": [ "web_server.software_manager.show()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 58, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------+\n", + "| database_server File System |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| File Path | Size | Health status | Visible health status | Deleted |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n", + "| database/database.db | 4.77 MB | GOOD | NONE | False |\n", + "| root | 0 B | GOOD | NONE | False |\n", + "+----------------------+---------+---------------+-----------------------+---------+\n" + ] + } + ], "source": [ "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -1361,9 +2507,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 59, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 3\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", + "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", + "root['NODES']['ROUTER0']['ACL'][1]['permission']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['source_ip_id']: 0 -> 7\n", + "root['NODES']['ROUTER0']['ACL'][1]['source_wildcard_id']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['source_port_id']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['dest_ip_id']: 0 -> 3\n", + "root['NODES']['ROUTER0']['ACL'][1]['dest_wildcard_id']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['dest_port_id']: 0 -> 2\n", + "root['NODES']['ROUTER0']['ACL'][1]['protocol_id']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", + "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", + "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" + ] + } + ], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -1427,9 +2597,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 60, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:40,175: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -1478,9 +2656,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 62, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "env.step(2) # Agent Action Equivalent to c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "env.step(3) # Agent action Equivalent to c2_beacon.establish()\n", @@ -1497,9 +2696,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 63, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", "env.step(3)\n", @@ -1517,9 +2737,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 64, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-----------------------------------------------------------------------------------------------------+\n", + "| C2Server Running Status |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n", + "| False | None | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "for i in range(6):\n", " env.step(0)\n", @@ -1542,9 +2776,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 65, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:40,588: PrimaiteGymEnv RNG seed = None\n" + ] + } + ], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -1584,9 +2826,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 67, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "c2_beacon.establish()\n", @@ -1604,9 +2860,72 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 68, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 4\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 5\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 6\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 7\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 8\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 9\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 10\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 11\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 12\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 13\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "for i in range(10):\n", " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", @@ -1622,9 +2941,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 69, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 0 | 1 | tcp | 80 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=1)\n", "c2_beacon.establish()\n", @@ -1640,9 +2973,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 70, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 14\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 15\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "# Comparing the OBS of the default frequency to a timestep frequency of 1\n", "for i in range(2):\n", @@ -1661,9 +3025,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 71, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 16\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 17\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 18\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 19\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 20\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 21\n", + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 22\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=7)\n", "\n", @@ -1700,9 +3107,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 72, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-04 15:37:41,322: Resetting environment, episode 0, avg. reward: 0.0\n", + "2025-02-04 15:37:41,325: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_0.json\n" + ] + } + ], "source": [ "blue_config_env.reset()\n", "\n", @@ -1724,9 +3140,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 73, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 5\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", + "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", + "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" + ] + } + ], "source": [ "# Capturing default C2 Traffic\n", "for i in range(3):\n", @@ -1744,9 +3179,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 74, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", + "| C2Beacon Running Status |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", + "| True | 192.168.10.21 | 0 | 5 | udp | 53 |\n", + "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" + ] + } + ], "source": [ "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", "from primaite.utils.validation.port import PORT_LOOKUP\n", @@ -1759,9 +3208,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 75, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Observation space differences\n", + "-----------------------------\n", + "Step 10\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", + "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", + "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n" + ] + } + ], "source": [ "# Capturing UDP C2 Traffic\n", "for i in range(5):\n", From 4a472c5c757e5d3d6652de78787b5845717b9289 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 5 Feb 2025 10:12:13 +0000 Subject: [PATCH 186/224] #3062 - Remove discriminators from abstract classes and fix remaining old discriminator names --- src/primaite/game/agent/actions/abstract.py | 2 -- src/primaite/game/agent/actions/acl.py | 10 +++------- src/primaite/game/agent/actions/application.py | 4 +--- src/primaite/game/agent/actions/file.py | 4 +--- src/primaite/game/agent/actions/folder.py | 4 +--- src/primaite/game/agent/actions/host_nic.py | 4 +--- src/primaite/game/agent/actions/network.py | 7 +++---- src/primaite/game/agent/actions/node.py | 14 +++++--------- src/primaite/game/agent/actions/service.py | 7 +++---- src/primaite/game/agent/actions/session.py | 8 +++----- src/primaite/game/agent/interface.py | 10 +++++----- src/primaite/game/agent/rewards.py | 6 +++--- .../game/agent/scripted_agents/abstract_tap.py | 8 ++++---- .../agent/scripted_agents/data_manipulation_bot.py | 2 -- src/primaite/game/game.py | 4 ++-- src/primaite/simulator/network/networks.py | 4 ++-- .../system/applications/database_client.py | 2 +- src/primaite/simulator/system/applications/nmap.py | 2 +- tests/integration_tests/game_layer/test_actions.py | 4 ++-- tests/integration_tests/network/test_broadcast.py | 6 +++--- .../system/test_service_listening_on_ports.py | 4 ++-- .../_system/_applications/test_web_browser.py | 2 +- .../_system/_services/test_dns_client.py | 2 +- .../_system/_services/test_dns_server.py | 2 +- .../_system/_services/test_ftp_client.py | 2 +- .../_system/_services/test_ftp_server.py | 2 +- .../_simulator/_system/_services/test_terminal.py | 2 +- .../_system/_services/test_web_server.py | 6 +++--- 28 files changed, 55 insertions(+), 79 deletions(-) diff --git a/src/primaite/game/agent/actions/abstract.py b/src/primaite/game/agent/actions/abstract.py index 9e55cf09..1c039ed3 100644 --- a/src/primaite/game/agent/actions/abstract.py +++ b/src/primaite/game/agent/actions/abstract.py @@ -12,8 +12,6 @@ from primaite.interface.request import RequestFormat class AbstractAction(BaseModel, ABC): """Base class for actions.""" - config: "AbstractAction.ConfigSchema" - class ConfigSchema(BaseModel, ABC): """Base configuration schema for Actions.""" diff --git a/src/primaite/game/agent/actions/acl.py b/src/primaite/game/agent/actions/acl.py index 7a5cc188..fb59574d 100644 --- a/src/primaite/game/agent/actions/acl.py +++ b/src/primaite/game/agent/actions/acl.py @@ -21,9 +21,7 @@ __all__ = ( class ACLAddRuleAbstractAction(AbstractAction, ABC): """Base abstract class for ACL add rule actions.""" - config: ConfigSchema = "ACLAddRuleAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Configuration Schema base for ACL add rule abstract actions.""" src_ip: Union[IPV4Address, Literal["ALL"]] @@ -37,12 +35,10 @@ class ACLAddRuleAbstractAction(AbstractAction, ABC): dst_wildcard: Union[IPV4Address, Literal["NONE"]] -class ACLRemoveRuleAbstractAction(AbstractAction, discriminator="acl-remove-rule-abstract-action"): +class ACLRemoveRuleAbstractAction(AbstractAction, ABC): """Base abstract class for acl remove rule actions.""" - config: ConfigSchema = "ACLRemoveRuleAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Configuration Schema base for ACL remove rule abstract actions.""" position: int diff --git a/src/primaite/game/agent/actions/application.py b/src/primaite/game/agent/actions/application.py index a7452de0..9651b600 100644 --- a/src/primaite/game/agent/actions/application.py +++ b/src/primaite/game/agent/actions/application.py @@ -23,9 +23,7 @@ class NodeApplicationAbstractAction(AbstractAction, ABC): inherit from this base class. """ - config: "NodeApplicationAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base Configuration schema for Node Application actions.""" node_name: str diff --git a/src/primaite/game/agent/actions/file.py b/src/primaite/game/agent/actions/file.py index b9ebe3bd..a2fcd3e2 100644 --- a/src/primaite/game/agent/actions/file.py +++ b/src/primaite/game/agent/actions/file.py @@ -24,9 +24,7 @@ class NodeFileAbstractAction(AbstractAction, ABC): only three parameters can inherit from this base class. """ - config: "NodeFileAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Configuration Schema for NodeFileAbstractAction.""" node_name: str diff --git a/src/primaite/game/agent/actions/folder.py b/src/primaite/game/agent/actions/folder.py index d5731527..80be0cd5 100644 --- a/src/primaite/game/agent/actions/folder.py +++ b/src/primaite/game/agent/actions/folder.py @@ -22,9 +22,7 @@ class NodeFolderAbstractAction(AbstractAction, ABC): this base class. """ - config: "NodeFolderAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base configuration schema for NodeFolder actions.""" node_name: str diff --git a/src/primaite/game/agent/actions/host_nic.py b/src/primaite/game/agent/actions/host_nic.py index f96714b2..d192a757 100644 --- a/src/primaite/game/agent/actions/host_nic.py +++ b/src/primaite/game/agent/actions/host_nic.py @@ -16,9 +16,7 @@ class HostNICAbstractAction(AbstractAction, ABC): base class. """ - config: "HostNICAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base Configuration schema for HostNIC actions.""" node_name: str diff --git a/src/primaite/game/agent/actions/network.py b/src/primaite/game/agent/actions/network.py index 74d427d0..22fc2c2d 100644 --- a/src/primaite/game/agent/actions/network.py +++ b/src/primaite/game/agent/actions/network.py @@ -1,5 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from abc import ABC from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction @@ -8,12 +9,10 @@ from primaite.interface.request import RequestFormat __all__ = ("NetworkPortEnableAction", "NetworkPortDisableAction") -class NetworkPortAbstractAction(AbstractAction, discriminator="network-port-abstract"): +class NetworkPortAbstractAction(AbstractAction, ABC): """Base class for Network port actions.""" - config: "NetworkPortAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base configuration schema for NetworkPort actions.""" target_nodename: str diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index be59986f..19639c21 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from abc import abstractmethod +from abc import ABC, abstractmethod from typing import ClassVar, List, Optional, Union from primaite.game.agent.actions.manager import AbstractAction @@ -18,16 +18,14 @@ __all__ = ( ) -class NodeAbstractAction(AbstractAction, discriminator="node-abstract"): +class NodeAbstractAction(AbstractAction, ABC): """ Abstract base class for node actions. Any action which applies to a node and uses node_name as its only parameter can inherit from this base class. """ - config: "NodeAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base Configuration schema for Node actions.""" node_name: str @@ -83,12 +81,10 @@ class NodeResetAction(NodeAbstractAction, discriminator="node-reset"): verb: ClassVar[str] = "reset" -class NodeNMAPAbstractAction(AbstractAction, discriminator="node-nmap-abstract-action"): +class NodeNMAPAbstractAction(AbstractAction, ABC): """Base class for NodeNMAP actions.""" - config: "NodeNMAPAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base Configuration Schema for NodeNMAP actions.""" target_ip_address: Union[str, List[str]] diff --git a/src/primaite/game/agent/actions/service.py b/src/primaite/game/agent/actions/service.py index bb7b689c..dfe1f91d 100644 --- a/src/primaite/game/agent/actions/service.py +++ b/src/primaite/game/agent/actions/service.py @@ -1,4 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from abc import ABC from typing import ClassVar from primaite.game.agent.actions.manager import AbstractAction @@ -17,15 +18,13 @@ __all__ = ( ) -class NodeServiceAbstractAction(AbstractAction, discriminator="node-service-abstract"): +class NodeServiceAbstractAction(AbstractAction, ABC): """Abstract Action for Node Service related actions. Any actions which use node_name and service_name can inherit from this class. """ - config: "NodeServiceAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): node_name: str service_name: str verb: ClassVar[str] diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 872e5bf4..58a8a555 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from abc import abstractmethod +from abc import ABC, abstractmethod from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -11,12 +11,10 @@ __all__ = ( ) -class NodeSessionAbstractAction(AbstractAction, discriminator="node-session-abstract"): +class NodeSessionAbstractAction(AbstractAction, ABC): """Base class for NodeSession actions.""" - config: "NodeSessionAbstractAction.ConfigSchema" - - class ConfigSchema(AbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema, ABC): """Base configuration schema for NodeSessionAbstractActions.""" node_name: str diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 6e6c1bec..a55cd3ff 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -68,7 +68,7 @@ class AbstractAgent(BaseModel, ABC): ) reward_function: RewardFunction.ConfigSchema = Field(default_factory=lambda: RewardFunction.ConfigSchema()) - config: "AbstractAgent.ConfigSchema" = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) + config: ConfigSchema = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) logger: AgentLog = AgentLog(agent_name="Abstract_Agent") history: List[AgentHistoryItem] = [] @@ -161,16 +161,16 @@ class AbstractAgent(BaseModel, ABC): return agent_class(config=config) -class AbstractScriptedAgent(AbstractAgent, discriminator="abstract-scripted-agent"): +class AbstractScriptedAgent(AbstractAgent, ABC): """Base class for actors which generate their own behaviour.""" - config: "AbstractScriptedAgent.ConfigSchema" = Field(default_factory=lambda: AbstractScriptedAgent.ConfigSchema()) - - class ConfigSchema(AbstractAgent.ConfigSchema): + class ConfigSchema(AbstractAgent.ConfigSchema, ABC): """Configuration Schema for AbstractScriptedAgents.""" type: str = "AbstractScriptedAgent" + config: ConfigSchema = Field(default_factory=lambda: AbstractScriptedAgent.ConfigSchema()) + @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """Return an action to be taken in the environment.""" diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 59f7a133..39f6fcfb 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -43,16 +43,16 @@ _LOGGER = getLogger(__name__) WhereType = Optional[Iterable[Union[str, int]]] -class AbstractReward(BaseModel): +class AbstractReward(BaseModel, ABC): """Base class for reward function components.""" - config: "AbstractReward.ConfigSchema" - class ConfigSchema(BaseModel, ABC): """Config schema for AbstractReward.""" type: str = "" + config: ConfigSchema + _registry: ClassVar[Dict[str, Type["AbstractReward"]]] = {} def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: diff --git a/src/primaite/game/agent/scripted_agents/abstract_tap.py b/src/primaite/game/agent/scripted_agents/abstract_tap.py index b0a8ae29..679f69fa 100644 --- a/src/primaite/game/agent/scripted_agents/abstract_tap.py +++ b/src/primaite/game/agent/scripted_agents/abstract_tap.py @@ -2,7 +2,7 @@ from __future__ import annotations import random -from abc import abstractmethod +from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple from gymnasium.core import ObsType @@ -13,18 +13,18 @@ from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent __all__ = "AbstractTAPAgent" -class AbstractTAPAgent(PeriodicAgent, discriminator="abstract-tap"): +class AbstractTAPAgent(PeriodicAgent, ABC): """Base class for TAP agents to inherit from.""" config: "AbstractTAPAgent.ConfigSchema" = Field(default_factory=lambda: AbstractTAPAgent.ConfigSchema()) next_execution_timestep: int = 0 - class AgentSettingsSchema(PeriodicAgent.AgentSettingsSchema): + class AgentSettingsSchema(PeriodicAgent.AgentSettingsSchema, ABC): """Schema for the `agent_settings` part of the agent config.""" possible_starting_nodes: List[str] = Field(default_factory=list) - class ConfigSchema(PeriodicAgent.ConfigSchema): + class ConfigSchema(PeriodicAgent.ConfigSchema, ABC): """Configuration schema for Abstract TAP agents.""" type: str = "abstract-tap" diff --git a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py index fc0b1869..7a88bd12 100644 --- a/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py +++ b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py @@ -6,8 +6,6 @@ from pydantic import Field from primaite.game.agent.scripted_agents.random_agent import PeriodicAgent -__all__ = "DataManipulationAgent" - class DataManipulationAgent(PeriodicAgent, discriminator="red-database-corrupting-agent"): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index e8ea4625..fba822d8 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -288,8 +288,8 @@ class PrimaiteGame: if "folder_restore_duration" in defaults_config: new_node.file_system._default_folder_restore_duration = defaults_config["folder_restore_duration"] - if "users" in node_cfg and new_node.software_manager.software.get("UserManager"): - user_manager: UserManager = new_node.software_manager.software["UserManager"] # noqa + if "users" in node_cfg and new_node.software_manager.software.get("user-manager"): + user_manager: UserManager = new_node.software_manager.software["user-manager"] # noqa for user_cfg in node_cfg["users"]: user_manager.add_user(**user_cfg, bypass_can_perform_action=True) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 21a0041e..5e677679 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -170,13 +170,13 @@ def arcd_uc2_network() -> Network: client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) client_1.software_manager.install(DatabaseClient) - db_client_1: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client_1: DatabaseClient = client_1.software_manager.software.get("database-client") db_client_1.configure(server_ip_address=IPv4Address("192.168.1.14")) db_client_1.run() web_browser_1 = client_1.software_manager.software.get("web-browser") web_browser_1.target_url = "http://arcd.com/users/" client_1.software_manager.install(DataManipulationBot) - db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") + db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("data-manipulation-bot") db_manipulation_bot.configure( server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE", diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 108687b9..14f2db21 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -37,7 +37,7 @@ class DatabaseClientConnection(BaseModel): @property def client(self) -> Optional[DatabaseClient]: """The DatabaseClient that holds this connection.""" - return self.parent_node.software_manager.software.get("DatabaseClient") + return self.parent_node.software_manager.software.get("database-client") def query(self, sql: str) -> bool: """ diff --git a/src/primaite/simulator/system/applications/nmap.py b/src/primaite/simulator/system/applications/nmap.py index 93de43bb..90debcd6 100644 --- a/src/primaite/simulator/system/applications/nmap.py +++ b/src/primaite/simulator/system/applications/nmap.py @@ -70,7 +70,7 @@ class NMAP(Application, discriminator="nmap"): } def __init__(self, **kwargs): - kwargs["name"] = "NMAP" + kwargs["name"] = "nmap" kwargs["port"] = PORT_LOOKUP["NONE"] kwargs["protocol"] = PROTOCOL_LOOKUP["NONE"] super().__init__(**kwargs) diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 4ae3dd6e..03b94ab7 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -678,8 +678,8 @@ def test_firewall_acl_add_remove_rule_integration(): assert firewall.external_outbound_acl.acl[1].action.name == "DENY" assert firewall.external_outbound_acl.acl[1].src_ip_address == IPv4Address("192.168.20.10") assert firewall.external_outbound_acl.acl[1].dst_ip_address == IPv4Address("192.168.0.10") - assert firewall.external_outbound_acl.acl[1].dst_port == PORT_LOOKUP["NONE"] - assert firewall.external_outbound_acl.acl[1].src_port == PORT_LOOKUP["NONE"] + assert firewall.external_outbound_acl.acl[1].dst_port is None + assert firewall.external_outbound_acl.acl[1].src_port is None assert firewall.external_outbound_acl.acl[1].protocol == PROTOCOL_LOOKUP["NONE"] env.step(12) # Remove ACL rule from External Outbound diff --git a/tests/integration_tests/network/test_broadcast.py b/tests/integration_tests/network/test_broadcast.py index b1bbfc63..50a1173c 100644 --- a/tests/integration_tests/network/test_broadcast.py +++ b/tests/integration_tests/network/test_broadcast.py @@ -27,7 +27,7 @@ class BroadcastTestService(Service, discriminator="broadcast-test-service"): def __init__(self, **kwargs): # Set default service properties for broadcasting - kwargs["name"] = "BroadcastService" + kwargs["name"] = "broadcast-test-service" kwargs["port"] = PORT_LOOKUP["HTTP"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) @@ -127,7 +127,7 @@ def broadcast_network() -> Network: server_1.power_on() server_1.software_manager.install(BroadcastTestService) - service: BroadcastTestService = server_1.software_manager.software["BroadcastService"] + service: BroadcastTestService = server_1.software_manager.software["broadcast-test-service"] service.start() switch_1: Switch = Switch.from_config( @@ -153,7 +153,7 @@ def broadcast_service_and_clients( "broadcast-test-client" ] service: BroadcastTestService = broadcast_network.get_node_by_hostname("server_1").software_manager.software[ - "broadcast-service" + "broadcast-test-service" ] return service, client_1, client_2 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 4bf5c7ba..9c25d4f9 100644 --- a/tests/integration_tests/system/test_service_listening_on_ports.py +++ b/tests/integration_tests/system/test_service_listening_on_ports.py @@ -22,7 +22,7 @@ class _DatabaseListener(Service, discriminator="database-listener"): listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} config: "_DatabaseListener.ConfigSchema" = Field(default_factory=lambda: _DatabaseListener.ConfigSchema()) - name: str = "DatabaseListener" + name: str = "database-listener" protocol: str = PROTOCOL_LOOKUP["TCP"] port: int = PORT_LOOKUP["NONE"] listen_on_ports: Set[int] = {PORT_LOOKUP["POSTGRES_SERVER"]} @@ -45,7 +45,7 @@ def test_http_listener(client_server): server_db.start() server.software_manager.install(_DatabaseListener) - server_db_listener: _DatabaseListener = server.software_manager.software["DatabaseListener"] + server_db_listener: _DatabaseListener = server.software_manager.software["database-listener"] server_db_listener.start() computer.software_manager.install(DatabaseClient) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index 6cffc5c0..8a901f2b 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -46,7 +46,7 @@ def test_create_web_client(): computer.power_on() # Web Browser should be pre-installed in computer web_browser: WebBrowser = computer.software_manager.software.get("web-browser") - assert web_browser.name is "web-browser" + assert web_browser.name == "web-browser" assert web_browser.port is PORT_LOOKUP["HTTP"] assert web_browser.protocol is PROTOCOL_LOOKUP["TCP"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 71ed7140..195632ee 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -29,7 +29,7 @@ def dns_client() -> Computer: def test_create_dns_client(dns_client): assert dns_client is not None dns_client_service: DNSClient = dns_client.software_manager.software.get("dns-client") - assert dns_client_service.name is "dns-client" + assert dns_client_service.name == "dns-client" assert dns_client_service.port is PORT_LOOKUP["DNS"] assert dns_client_service.protocol is PROTOCOL_LOOKUP["TCP"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 72304df3..006a7f2e 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -33,7 +33,7 @@ def dns_server() -> Node: def test_create_dns_server(dns_server): assert dns_server is not None dns_server_service: DNSServer = dns_server.software_manager.software.get("dns-server") - assert dns_server_service.name is "dns-server" + assert dns_server_service.name == "dns-server" assert dns_server_service.port is PORT_LOOKUP["DNS"] assert dns_server_service.protocol is PROTOCOL_LOOKUP["TCP"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 4a0f362b..91369f6c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -32,7 +32,7 @@ def ftp_client() -> Node: def test_create_ftp_client(ftp_client): assert ftp_client is not None ftp_client_service: FTPClient = ftp_client.software_manager.software.get("ftp-client") - assert ftp_client_service.name is "ftp-client" + assert ftp_client_service.name == "ftp-client" assert ftp_client_service.port is PORT_LOOKUP["FTP"] assert ftp_client_service.protocol is PROTOCOL_LOOKUP["TCP"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 2527a401..77ba5cd4 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -31,7 +31,7 @@ def ftp_server() -> Node: def test_create_ftp_server(ftp_server): assert ftp_server is not None ftp_server_service: FTPServer = ftp_server.software_manager.software.get("ftp-server") - assert ftp_server_service.name is "ftp-server" + assert ftp_server_service.name == "ftp-server" assert ftp_server_service.port is PORT_LOOKUP["FTP"] assert ftp_server_service.protocol is PROTOCOL_LOOKUP["TCP"] diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 12d12ea8..602118fd 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -364,7 +364,7 @@ def test_SSH_across_network(): pc_a = network.get_node_by_hostname("client_1") router_1 = network.get_node_by_hostname("router_1") - terminal_a: Terminal = pc_a.software_manager.software.get("Terminal") + terminal_a: Terminal = pc_a.software_manager.software.get("terminal") router_1.acl.add_rule( action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=21 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index ad6afa82..30e0f9eb 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -34,9 +34,9 @@ def web_server() -> Server: def test_create_web_server(web_server): assert web_server is not None web_server_service: WebServer = web_server.software_manager.software.get("web-server") - assert web_server_service.name is "web-server" - assert web_server_service.port is PORT_LOOKUP["HTTP"] - assert web_server_service.protocol is PROTOCOL_LOOKUP["TCP"] + assert web_server_service.name == "web-server" + assert web_server_service.port == PORT_LOOKUP["HTTP"] + assert web_server_service.protocol == PROTOCOL_LOOKUP["TCP"] def test_handling_get_request_not_found_path(web_server): From 3a29593f600ba3fa1b9b74806333bf7934e4303f Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 5 Feb 2025 11:50:38 +0000 Subject: [PATCH 187/224] 3075: Fix bug in notebook helper function. --- ...a-Manipulation-Customising-Red-Agent.ipynb | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index de6d3fe5..14938c02 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -6,7 +6,7 @@ "source": [ "# Customising UC2 Red Agents\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This notebook will go over some examples of how red agent behaviour can be varied by changing its configuration parameters.\n", "\n", @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -36,13 +36,12 @@ "from primaite.game.agent.interface import AgentHistoryItem\n", "from primaite.session.environment import PrimaiteGymEnv\n", "import yaml\n", - "from pprint import pprint\n", - "from primaite.game.agent.scripted_agents import probabilistic_agent, data_manipulation_bot" + "from pprint import pprint" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -69,7 +68,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -80,9 +79,7 @@ " if red_action == 'do_nothing':\n", " red_str = 'DO NOTHING'\n", " elif red_action == 'node_application_execute':\n", - " client = \"client 1\" if red_info.parameters['node_name'] == 0 else \"client 2\"\n", - "\n", - " red_str = f\"ATTACK from {client}\"\n", + " red_str = f\"ATTACK from {red_info.parameters['node_name']}\"\n", " return red_str" ] }, @@ -245,7 +242,7 @@ "outputs": [], "source": [ "change = yaml.safe_load(\"\"\"\n", - " possible_start_nodes: [client_1, client_2]\n", + " possible_start_nodes: [client_1]\n", " target_application: DataManipulationBot\n", " start_step: 25\n", " frequency: 20\n", @@ -315,6 +312,10 @@ "outputs": [], "source": [ "change = yaml.safe_load(\"\"\"\n", + " agent_settings:\n", + " possible_start_nodes: [client_1]\n", + " target_application: DataManipulationBot\n", + "\n", " action_space:\n", " action_map:\n", " 0:\n", From a4c19608a69251c0dabff77c69fb02fab544b3c7 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 5 Feb 2025 11:51:22 +0000 Subject: [PATCH 188/224] 3075: Pre-commit tidy up. --- docs/source/how_to_guides/extensible_nodes.rst | 2 +- src/primaite/game/game.py | 1 - src/primaite/notebooks/Action-masking.ipynb | 15 +++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 043d0f06..4819cbb2 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -52,4 +52,4 @@ class Router(NetworkNode, identifier="router"): Changes to YAML file. ===================== -While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. +While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f819fa05..e9941a12 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -404,7 +404,6 @@ class PrimaiteGame: agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: - new_agent = AbstractAgent.from_config(agent_cfg) game.agents[agent_cfg["ref"]] = new_agent if isinstance(new_agent, ProxyAgent): diff --git a/src/primaite/notebooks/Action-masking.ipynb b/src/primaite/notebooks/Action-masking.ipynb index 64015080..74504878 100644 --- a/src/primaite/notebooks/Action-masking.ipynb +++ b/src/primaite/notebooks/Action-masking.ipynb @@ -6,7 +6,7 @@ "source": [ "# Action Masking\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "PrimAITE environments support action masking. The action mask shows which of the agent's actions are applicable with the current environment state. For example, a node can only be turned on if it is currently turned off." ] @@ -22,14 +22,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.config.load import data_manipulation_config_path\n", - "from prettytable import PrettyTable\n", - "from primaite.game.agent.scripted_agents import probabilistic_agent\n" + "from prettytable import PrettyTable" ] }, { @@ -104,7 +103,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -117,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -157,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -168,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ From c1abbfe58c453c7568226ac90f20d6238d8b36a6 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 5 Feb 2025 15:04:41 +0000 Subject: [PATCH 189/224] bugfix - Make node schemas stricter --- .../multi_lan_internet_network_example.yaml | 6 ++--- src/primaite/game/game.py | 9 ++++--- .../simulator/network/hardware/base.py | 6 ++++- .../network/hardware/nodes/host/computer.py | 3 ++- .../network/hardware/nodes/host/host_node.py | 7 ++++- .../network/hardware/nodes/host/server.py | 4 +++ .../hardware/nodes/network/firewall.py | 3 ++- .../hardware/nodes/network/network_node.py | 7 ++++- .../network/hardware/nodes/network/router.py | 27 ++++++++++++------- .../network/hardware/nodes/network/switch.py | 3 ++- .../hardware/nodes/network/wireless_router.py | 13 +++++---- .../extensions/nodes/giga_switch.py | 5 +++- .../extensions/nodes/super_computer.py | 5 +++- ...ndwidth_load_checks_before_transmission.py | 2 +- .../network/test_wireless_router.py | 4 +-- .../_network/_hardware/nodes/test_router.py | 1 - .../_system/_services/test_terminal.py | 2 +- 17 files changed, 73 insertions(+), 34 deletions(-) diff --git a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml index 508018aa..cbd1c01e 100644 --- a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml +++ b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml @@ -102,8 +102,7 @@ simulation: subnet_mask: 255.255.255.252 default_gateway: 8.8.8.1 services: - - ref: dns_server - type: dns-server + - type: dns-server options: domain_mapping: sometech.ai: 94.10.180.6 @@ -196,8 +195,7 @@ simulation: default_gateway: 94.10.180.5 dns_server: 8.8.8.2 services: - - ref: web_server - type: web-server + - type: web-server applications: - type: database-client options: diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index fba822d8..f6ba841e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -14,6 +14,7 @@ from primaite.simulator.network.creation import NetworkNodeAdder from primaite.simulator.network.hardware.base import NetworkInterface, Node, NodeOperatingState, UserManager from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.network.switch import Switch +from primaite.simulator.network.hardware.nodes.network.wireless_router import WirelessRouter from primaite.simulator.network.nmne import NMNEConfig from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.application import Application @@ -268,9 +269,11 @@ class PrimaiteGame: new_node = None if n_type in Node._registry: - if n_type == "wireless-router": - node_cfg["airspace"] = net.airspace - new_node = Node._registry[n_type].from_config(config=node_cfg) + n_class = Node._registry[n_type] + if issubclass(n_class, WirelessRouter): + new_node = n_class(config=n_class.ConfigSchema(**node_cfg), airspace=net.airspace) + else: + new_node = Node._registry[n_type].from_config(config=node_cfg) else: msg = f"invalid node type {n_type} in config" _LOGGER.error(msg) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 0eee77f6..8653359a 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1529,9 +1529,11 @@ class Node(SimComponent, ABC): class ConfigSchema(BaseModel, ABC): """Configuration Schema for Node based classes.""" - model_config = ConfigDict(arbitrary_types_allowed=True) + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") """Configure pydantic to allow arbitrary types, let the instance have attributes not present in the model.""" + type: str + hostname: str "The node hostname on the network." @@ -1570,6 +1572,8 @@ class Node(SimComponent, ABC): operating_state: Any = None + users: Any = None # Temporary to appease "extra=forbid" + config: ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) """Configuration items within Node""" diff --git a/src/primaite/simulator/network/hardware/nodes/host/computer.py b/src/primaite/simulator/network/hardware/nodes/host/computer.py index e8892016..bee172d9 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/computer.py +++ b/src/primaite/simulator/network/hardware/nodes/host/computer.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from typing import ClassVar, Dict +from typing import ClassVar, Dict, Literal from pydantic import Field @@ -40,6 +40,7 @@ class Computer(HostNode, discriminator="computer"): class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Computer class.""" + type: Literal["computer"] = "computer" hostname: str = "Computer" config: ConfigSchema = Field(default_factory=lambda: Computer.ConfigSchema()) diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 6d73e4fd..76d9167c 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -2,7 +2,7 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Optional +from typing import Any, ClassVar, Dict, Literal, Optional from pydantic import Field @@ -333,9 +333,14 @@ class HostNode(Node, discriminator="host-node"): class ConfigSchema(Node.ConfigSchema): """Configuration Schema for HostNode class.""" + type: Literal["host-node"] hostname: str = "HostNode" subnet_mask: IPV4Address = "255.255.255.0" ip_address: IPV4Address + services: Any = None # temporarily unset to appease extra="forbid" + applications: Any = None # temporarily unset to appease extra="forbid" + folders: Any = None # temporarily unset to appease extra="forbid" + network_interfaces: Any = None # temporarily unset to appease extra="forbid" config: ConfigSchema = Field(default_factory=lambda: HostNode.ConfigSchema()) diff --git a/src/primaite/simulator/network/hardware/nodes/host/server.py b/src/primaite/simulator/network/hardware/nodes/host/server.py index ae43533b..09c1708b 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/server.py +++ b/src/primaite/simulator/network/hardware/nodes/host/server.py @@ -1,5 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from typing import Literal + from pydantic import Field from primaite.simulator.network.hardware.nodes.host.host_node import HostNode @@ -36,6 +38,7 @@ class Server(HostNode, discriminator="server"): class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Server class.""" + type: Literal["server"] = "server" hostname: str = "server" config: ConfigSchema = Field(default_factory=lambda: Server.ConfigSchema()) @@ -49,6 +52,7 @@ class Printer(HostNode, discriminator="printer"): class ConfigSchema(HostNode.ConfigSchema): """Configuration Schema for Printer class.""" + type: Literal["printer"] = "printer" hostname: str = "printer" config: ConfigSchema = Field(default_factory=lambda: Printer.ConfigSchema()) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index eddc2a2f..2cbc23d2 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Dict, Final, Union +from typing import Dict, Final, Literal, Union from prettytable import MARKDOWN, PrettyTable from pydantic import Field, validate_call @@ -103,6 +103,7 @@ class Firewall(Router, discriminator="firewall"): class ConfigSchema(Router.ConfigSchema): """Configuration Schema for Firewall 'Nodes' within PrimAITE.""" + type: Literal["firewall"] = "firewall" hostname: str = "firewall" num_ports: int = 0 diff --git a/src/primaite/simulator/network/hardware/nodes/network/network_node.py b/src/primaite/simulator/network/hardware/nodes/network/network_node.py index 388b57d7..e5e77402 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/network_node.py +++ b/src/primaite/simulator/network/hardware/nodes/network/network_node.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import abstractmethod -from typing import Optional +from typing import Any, Optional from primaite.simulator.network.hardware.base import NetworkInterface, Node from primaite.simulator.network.transmission.data_link_layer import Frame @@ -16,6 +16,11 @@ class NetworkNode(Node, discriminator="network-node"): provide functionality for receiving and processing frames received on their network interfaces. """ + class ConfigSchema(Node.ConfigSchema): + """Config schema for Node baseclass.""" + + num_ports: Any = None # temporarily unset to appease extra="forbid" + @abstractmethod def receive_frame(self, frame: Frame, from_network_interface: NetworkInterface): """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index a7a4f62c..3b35600b 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -4,7 +4,7 @@ from __future__ import annotations import secrets from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union +from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable from pydantic import Field, validate_call @@ -1204,8 +1204,13 @@ class Router(NetworkNode, discriminator="router"): class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Routers.""" + type: Literal["router"] = "router" hostname: str = "router" num_ports: int = 5 + acl: Any = None # temporarily unset to appease extra="forbid" + routes: Any = None # temporarily unset to appease extra="forbid" + ports: Any = None # temporarily unset to appease extra="forbid" + default_route: Any = None # temporarily unset to appease extra="forbid" config: ConfigSchema = Field(default_factory=lambda: Router.ConfigSchema()) @@ -1625,16 +1630,20 @@ class Router(NetworkNode, discriminator="router"): :return: Configured router. :rtype: Router """ + ports = config.pop("ports", None) + acl = config.pop("acl", None) + routes = config.pop("routes", None) + default_route = config.pop("default_route", None) router = Router(config=Router.ConfigSchema(**config)) - if "ports" in config: - for port_num, port_cfg in config["ports"].items(): + if ports: + for port_num, port_cfg in ports.items(): router.configure_port( port=port_num, ip_address=port_cfg["ip_address"], subnet_mask=IPv4Address(port_cfg.get("subnet_mask", "255.255.255.0")), ) - if "acl" in config: - for r_num, r_cfg in config["acl"].items(): + if acl: + for r_num, r_cfg in acl.items(): router.acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else PORT_LOOKUP[p], @@ -1646,16 +1655,16 @@ class Router(NetworkNode, discriminator="router"): dst_wildcard_mask=r_cfg.get("dst_wildcard_mask"), position=r_num, ) - if "routes" in config: - for route in config.get("routes"): + if routes: + for route in routes: router.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric", 0)), ) - if "default_route" in config: - next_hop_ip_address = config["default_route"].get("next_hop_ip_address", None) + if default_route: + next_hop_ip_address = default_route.get("next_hop_ip_address", None) if next_hop_ip_address: router.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address) router.operating_state = ( diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index dc7e4f56..1f2bc135 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from typing import Dict, Optional +from typing import Dict, Literal, Optional from prettytable import MARKDOWN, PrettyTable from pydantic import Field @@ -101,6 +101,7 @@ class Switch(NetworkNode, discriminator="switch"): class ConfigSchema(NetworkNode.ConfigSchema): """Configuration Schema for Switch nodes within PrimAITE.""" + type: Literal["switch"] = "switch" hostname: str = "Switch" num_ports: int = 24 "The number of ports on the switch. Default is 24." diff --git a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py index 2314323f..eab75ff1 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from ipaddress import IPv4Address -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Literal, Optional, Union from pydantic import Field, validate_call @@ -126,10 +126,13 @@ class WirelessRouter(Router, discriminator="wireless-router"): class ConfigSchema(Router.ConfigSchema): """Configuration Schema for WirelessRouter nodes within PrimAITE.""" + type: Literal["wireless-router"] = "wireless-router" hostname: str = "WirelessRouter" - airspace: AirSpace num_ports: int = 0 + router_interface: Any = None # temporarily unset to appease extra="forbid" + wireless_access_point: Any = None # temporarily unset to appease extra="forbid" + airspace: AirSpace config: ConfigSchema = Field(default_factory=lambda: WirelessRouter.ConfigSchema()) def __init__(self, **kwargs): @@ -137,7 +140,7 @@ class WirelessRouter(Router, discriminator="wireless-router"): self.connect_nic( WirelessAccessPoint( - ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", airspace=kwargs["config"].airspace + ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", airspace=self.airspace ) ) @@ -236,7 +239,7 @@ class WirelessRouter(Router, discriminator="wireless-router"): ) @classmethod - def from_config(cls, config: Dict, **kwargs) -> "WirelessRouter": + def from_config(cls, config: Dict, airspace: AirSpace) -> "WirelessRouter": """Generate the wireless router from config. Schema: @@ -263,7 +266,7 @@ class WirelessRouter(Router, discriminator="wireless-router"): :return: WirelessRouter instance. :rtype: WirelessRouter """ - router = cls(config=cls.ConfigSchema(**config)) + router = cls(config=cls.ConfigSchema(**config), airspace=airspace) router.operating_state = ( NodeOperatingState.ON if not (p := config.get("operating_state")) else NodeOperatingState[p.upper()] ) diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index 86da0610..d9599618 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from typing import Dict +from typing import Dict, Literal from prettytable import MARKDOWN, PrettyTable @@ -18,6 +18,9 @@ class GigaSwitch(NetworkNode, discriminator="gigaswitch"): :ivar num_ports: The number of ports on the switch. Default is 24. """ + class ConfigSchema(NetworkNode.ConfigSchema): + type: Literal["gigaswitch"] = "gigaswitch" + num_ports: int = 24 "The number of ports on the switch." network_interfaces: Dict[str, SwitchPort] = {} diff --git a/tests/integration_tests/extensions/nodes/super_computer.py b/tests/integration_tests/extensions/nodes/super_computer.py index c4ad61ae..4418e352 100644 --- a/tests/integration_tests/extensions/nodes/super_computer.py +++ b/tests/integration_tests/extensions/nodes/super_computer.py @@ -1,5 +1,5 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from typing import ClassVar, Dict +from typing import ClassVar, Dict, Literal from primaite.simulator.network.hardware.nodes.host.host_node import HostNode, NIC from primaite.simulator.system.services.ftp.ftp_client import FTPClient @@ -34,6 +34,9 @@ class SuperComputer(HostNode, discriminator="supercomputer"): * Web Browser """ + class ConfigSchema(HostNode.ConfigSchema): + type: Literal["supercomputer"] = "supercomputer" + SYSTEM_SOFTWARE: ClassVar[Dict] = {**HostNode.SYSTEM_SOFTWARE, "ftp-client": FTPClient} def __init__(self, **kwargs): diff --git a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py index 6f3e7546..479473d1 100644 --- a/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py +++ b/tests/integration_tests/network/test_bandwidth_load_checks_before_transmission.py @@ -16,7 +16,7 @@ def test_wireless_link_loading(wireless_wan_network): # Configure Router 2 ACLs router_2.acl.add_rule(action=ACLAction.PERMIT, position=1) - airspace = router_1.config.airspace + airspace = router_1.airspace client.software_manager.install(FTPClient) ftp_client: FTPClient = client.software_manager.software.get("ftp-client") diff --git a/tests/integration_tests/network/test_wireless_router.py b/tests/integration_tests/network/test_wireless_router.py index 487736e7..74b97c2f 100644 --- a/tests/integration_tests/network/test_wireless_router.py +++ b/tests/integration_tests/network/test_wireless_router.py @@ -32,7 +32,7 @@ def wireless_wan_network(): # Configure Router 1 router_1 = WirelessRouter.from_config( - config={"type": "wireless_router", "hostname": "router_1", "start_up_duration": 0, "airspace": network.airspace} + config={"type": "wireless-router", "hostname": "router_1", "start_up_duration": 0}, airspace=network.airspace ) router_1.power_on() network.add_node(router_1) @@ -63,7 +63,7 @@ def wireless_wan_network(): # Configure Router 2 router_2: WirelessRouter = WirelessRouter.from_config( - config={"type": "wireless_router", "hostname": "router_2", "start_up_duration": 0, "airspace": network.airspace} + config={"type": "wireless-router", "hostname": "router_2", "start_up_duration": 0}, airspace=network.airspace ) router_2.power_on() network.add_node(router_2) diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py index e9d16533..e1a910b8 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_router.py @@ -8,7 +8,6 @@ from primaite.utils.validation.port import PORT_LOOKUP def test_wireless_router_from_config(): cfg = { - "ref": "router_1", "type": "router", "hostname": "router_1", "num_ports": 6, diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 602118fd..32fcae9a 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -95,7 +95,7 @@ def wireless_wan_network(): # Configure Router 1 router_1 = WirelessRouter.from_config( - config={"type": "wireless_router", "hostname": "router_1", "start_up_duration": 0, "airspace": network.airspace} + config={"type": "wireless-router", "hostname": "router_1", "start_up_duration": 0}, airspace=network.airspace ) router_1.power_on() network.add_node(router_1) From 09bdfa3ae1ba87dc084872d88f2f6d9050a55bf3 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 5 Feb 2025 15:11:04 +0000 Subject: [PATCH 190/224] bugfix - Restore wireless networks so they're not broken --- src/primaite/game/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f6ba841e..1427776e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -271,7 +271,7 @@ class PrimaiteGame: if n_type in Node._registry: n_class = Node._registry[n_type] if issubclass(n_class, WirelessRouter): - new_node = n_class(config=n_class.ConfigSchema(**node_cfg), airspace=net.airspace) + new_node = n_class.from_config(config=node_cfg, airspace=net.airspace) else: new_node = Node._registry[n_type].from_config(config=node_cfg) else: From b22ba65eb69644a25ad6451d3fadfe87e5c8112d Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 5 Feb 2025 16:40:59 +0000 Subject: [PATCH 191/224] #3075: Further notebook updates. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 1792 ++--------------- .../Data-Manipulation-E2E-Demonstration.ipynb | 6 +- .../Getting-Information-Out-Of-PrimAITE.ipynb | 4 +- ...ege-Escalation-and-Data-Loss-Example.ipynb | 11 +- .../notebooks/Requests-and-Responses.ipynb | 22 +- .../notebooks/Terminal-Processing.ipynb | 43 +- .../Training-an-RLLIB-MARL-System.ipynb | 31 +- .../notebooks/Training-an-RLLib-Agent.ipynb | 31 +- .../notebooks/Training-an-SB3-Agent.ipynb | 2 +- .../notebooks/Using-Episode-Schedules.ipynb | 18 +- src/primaite/notebooks/multi-processing.ipynb | 15 +- .../simulator/network/hardware/base.py | 8 +- 12 files changed, 224 insertions(+), 1759 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index b8d1423f..d3c414d7 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -13,31 +13,16 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:30,830: Performing the PrimAITE first-time setup...\n", - "2025-02-04 15:37:30,830: Building the PrimAITE app directories...\n", - "2025-02-04 15:37:30,830: Building primaite_config.yaml...\n", - "2025-02-04 15:37:30,830: Rebuilding the demo notebooks...\n", - "/home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", - "2025-02-04 15:37:30,832: Reset example notebook: /home/nick/primaite/4.0.0a1-dev/notebooks/example_notebooks/Command-and-Control-E2E-Demonstration.ipynb\n", - "2025-02-04 15:37:30,838: Rebuilding the example notebooks...\n", - "2025-02-04 15:37:30,841: PrimAITE setup complete!\n" - ] - } - ], + "outputs": [], "source": [ "!primaite setup" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -67,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -150,17 +135,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:35,293: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -186,35 +163,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------------------------------------------------------------------------+\n", - "| client_1 Software Manager |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", - "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", @@ -264,34 +215,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | INSTALLING | UNUSED | None | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(1)\n", "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", @@ -331,41 +257,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(2)\n", "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", @@ -404,54 +298,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=2, action='node_application_execute', parameters={'node_name': 'web_server', 'application_name': 'C2Beacon'}, request=['network', 'node', 'web_server', 'application', 'C2Beacon', 'execute'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(3)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 1 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.1.12 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.show()\n", "c2_server.show()" @@ -514,59 +372,18 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=3, action='c2_server_terminal_command', parameters={'node_name': 'client_1', 'ip_address': None, 'username': 'admin', 'password': 'admin', 'commands': [['software_manager', 'application', 'install', 'RansomwareScript']]}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'terminal_command', {'commands': [['software_manager', 'application', 'install', 'RansomwareScript']], 'ip_address': None, 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={0: RequestResponse(status='success', data={})}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(4)" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------------------------------------------------------------------------+\n", - "| client_1 Software Manager |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| FTPClient | Service | RUNNING | GOOD | 21 | tcp |\n", - "| DataManipulationBot | Application | RUNNING | GOOD | None | none |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Server | Application | RUNNING | GOOD | None | tcp |\n", - "+---------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "client_1.software_manager.show()" ] @@ -605,66 +422,18 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=4, action='c2_server_ransomware_configure', parameters={'node_name': 'client_1', 'server_ip_address': '192.168.1.14', 'payload': 'ENCRYPT'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_configure', {'server_ip_address': '192.168.1.14', 'server_password': None, 'payload': 'ENCRYPT'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(5)" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", - "| RansomwareScript | Application | RUNNING | GOOD | None | none |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "+------------------------------------+\n", - "| RansomwareScript Running Status |\n", - "+--------------------------+---------+\n", - "| Target Server IP Address | Payload |\n", - "+--------------------------+---------+\n", - "| 192.168.1.14 | ENCRYPT |\n", - "+--------------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", "web_server.software_manager.show()\n", @@ -710,48 +479,18 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=5, action='c2_server_data_exfiltrate', parameters={'node_name': 'client_1', 'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'exfiltrate', {'target_file_name': 'database.db', 'target_folder_name': 'database', 'exfiltration_folder_name': 'spoils', 'target_ip_address': '192.168.1.14', 'username': 'admin', 'password': 'admin'}], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(6)" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+--------------------------------------------------------------------------------+\n", - "| client_1 File System |\n", - "+--------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+--------------------+---------+---------------+-----------------------+---------+\n", - "| root | 0 B | GOOD | NONE | False |\n", - "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", - "+--------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.file_system.show(full=True)" @@ -759,25 +498,9 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+---------------------------------------------------------------------------------+\n", - "| web_server File System |\n", - "+---------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+---------------------+---------+---------------+-----------------------+---------+\n", - "| primaite/index.html | 15.0 KB | GOOD | NONE | False |\n", - "| root | 0 B | GOOD | NONE | False |\n", - "| spoils/database.db | 4.77 MB | GOOD | NONE | False |\n", - "+---------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "web_server: Computer = env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "web_server.software_manager.file_system.show(full=True)" @@ -814,48 +537,18 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(0,\n", - " 0.0,\n", - " False,\n", - " False,\n", - " {'agent_actions': {'CustomC2Agent': AgentHistoryItem(timestep=6, action='c2_server_ransomware_launch', parameters={'node_name': 'client_1'}, request=['network', 'node', 'client_1', 'application', 'C2Server', 'ransomware_launch'], response=RequestResponse(status='success', data={}), reward=0.0, reward_info={})}})" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.step(7)" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------+\n", - "| database_server File System |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| database/database.db | 4.77 MB | CORRUPT | NONE | False |\n", - "| root | 0 B | GOOD | NONE | False |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "database_server: Server = env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -874,7 +567,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1002,17 +695,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:36,689: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -1026,7 +711,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1069,18 +754,9 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:37,079: Resetting environment, episode 0, avg. reward: 0.0\n", - "2025-02-04 15:37:37,082: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_0.json\n" - ] - } - ], + "outputs": [], "source": [ "# Resetting the environment and capturing the default observation space.\n", "blue_env.reset()\n", @@ -1089,20 +765,9 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Setting up the C2 Suite via the simulation API.\n", "\n", @@ -1123,7 +788,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1133,26 +798,9 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 2\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(default_obs, c2_configuration_obs, blue_env.game.step_counter)" ] @@ -1172,20 +820,9 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={0: RequestResponse(status='success', data={})})" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -1196,20 +833,9 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -1218,7 +844,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1228,28 +854,9 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 7\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 0 -> 1\n", - "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 0 -> 3\n", - "root['NODES']['HOST0']['users']['local_login']: 0 -> 1\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(default_obs, c2_ransomware_obs, env.game.step_counter)" ] @@ -1265,7 +872,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1281,27 +888,16 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "c2_server.send_command(given_command=C2Command.DATA_EXFILTRATION, command_options=exfil_options)" ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1310,22 +906,9 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 7\n", - "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", - "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_exfil_obs, env.game.step_counter)" ] @@ -1341,20 +924,9 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Configuring the RansomwareScript\n", "ransomware_config = {\"server_ip_address\": \"192.168.1.14\", \"payload\": \"ENCRYPT\"}\n", @@ -1363,20 +935,9 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='success', data={})" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Waiting for the ransomware to finish installing and then launching the RansomwareScript.\n", "blue_env.step(0)\n", @@ -1385,7 +946,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1395,30 +956,9 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 6\n", - "root['NODES']['HOST0']['APPLICATIONS'][2]['operating_status']: 3 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST1']['users']['remote_sessions']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(c2_ransomware_obs, c2_final_obs, blue_env.game.step_counter)" ] @@ -1434,7 +974,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1466,200 +1006,16 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:38,317: Resetting environment, episode 1, avg. reward: 0.0\n", - "2025-02-04 15:37:38,321: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_1.json\n" - ] - }, - { - "data": { - "text/plain": [ - "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'ROUTER0': {'ACL': {1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 6: {'position': 5,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0}},\n", - " 'PORTS': {1: {'operating_status': 1},\n", - " 2: {'operating_status': 1},\n", - " 3: {'operating_status': 2}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'ICS': 0},\n", - " {})" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.reset()" ] }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1678,7 +1034,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1695,33 +1051,9 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "blue_env.step(0)\n", "web_server.software_manager.show()" @@ -1729,26 +1061,9 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 3\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -1762,20 +1077,9 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", @@ -1797,200 +1101,16 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:38,778: Resetting environment, episode 2, avg. reward: 0.0\n", - "2025-02-04 15:37:38,781: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_2.json\n" - ] - }, - { - "data": { - "text/plain": [ - "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'ROUTER0': {'ACL': {1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 6: {'position': 5,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0}},\n", - " 'PORTS': {1: {'operating_status': 1},\n", - " 2: {'operating_status': 1},\n", - " 3: {'operating_status': 2}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'ICS': 0},\n", - " {})" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.reset()" ] }, { "cell_type": "code", - "execution_count": 46, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2009,7 +1129,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2026,17 +1146,9 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "NodeOperatingState.SHUTTING_DOWN\n" - ] - } - ], + "outputs": [], "source": [ "web_server = blue_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "print(web_server.operating_state)" @@ -2044,48 +1156,18 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 2\n", - "root['NODES']['HOST0']['operating_status']: 1 -> 4\n", - "root['NODES']['HOST0']['APPLICATIONS'][1]['operating_status']: 1 -> 0\n", - "root['NODES']['HOST0']['NICS'][1]['nic_status']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] }, { "cell_type": "code", - "execution_count": 50, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", @@ -2109,200 +1191,16 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:39,257: Resetting environment, episode 3, avg. reward: 0.0\n", - "2025-02-04 15:37:39,260: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_3.json\n" - ] - }, - { - "data": { - "text/plain": [ - "({'NODES': {'HOST0': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST1': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 1,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST2': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'HOST3': {'APPLICATIONS': {1: {'operating_status': 0,\n", - " 'health_status': 0,\n", - " 'num_executions': 0},\n", - " 2: {'operating_status': 0, 'health_status': 0, 'num_executions': 0}},\n", - " 'FOLDERS': {1: {'health_status': 0,\n", - " 'FILES': {1: {'health_status': 0, 'num_access': 0}}}},\n", - " 'NICS': {1: {'nic_status': 1,\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {80: {'inbound': 0, 'outbound': 0},\n", - " 53: {'inbound': 0, 'outbound': 0},\n", - " 21: {'inbound': 0, 'outbound': 0}}}}},\n", - " 'num_file_creations': 0,\n", - " 'num_file_deletions': 0,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0},\n", - " 'operating_status': 1},\n", - " 'ROUTER0': {'ACL': {1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 6: {'position': 5,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_wildcard_id': 0,\n", - " 'source_port_id': 0,\n", - " 'dest_ip_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'protocol_id': 0}},\n", - " 'PORTS': {1: {'operating_status': 1},\n", - " 2: {'operating_status': 1},\n", - " 3: {'operating_status': 2}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}},\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'ICS': 0},\n", - " {})" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.reset()" ] }, { "cell_type": "code", - "execution_count": 52, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2321,7 +1219,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2338,41 +1236,9 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+---------------------------------------------------------------+\n", - "| router_1 Network Interfaces |\n", - "+------+-------------------+-----------------+-------+----------+\n", - "| Port | MAC Address | Address | Speed | Status |\n", - "+------+-------------------+-----------------+-------+----------+\n", - "| 1 | dd:bc:17:75:a2:c4 | 192.168.1.1/24 | 100.0 | Enabled |\n", - "| 2 | 00:f2:f5:65:51:75 | 192.168.10.1/24 | 100.0 | Enabled |\n", - "| 3 | de:7d:ac:0e:58:e1 | 127.0.0.1/8 | 100.0 | Disabled |\n", - "| 4 | 8a:b4:55:12:c7:9e | 127.0.0.1/8 | 100.0 | Disabled |\n", - "| 5 | a5:ef:c6:53:97:c4 | 127.0.0.1/8 | 100.0 | Disabled |\n", - "+------+-------------------+-----------------+-------+----------+\n", - "+------------------------------------------------------------------------------------------------------------------------+\n", - "| router_1 Access Control List |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| 1 | DENY | ANY | 192.168.10.21 | 0.0.0.1 | 80 | 192.168.1.12 | 0.0.0.1 | 80 | 0 |\n", - "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", - "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", - "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", - "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", - "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", - "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "router_1: Router = blue_env.game.simulation.network.get_node_by_hostname(\"router_1\")\n", "router_1.acl.show()" @@ -2387,20 +1253,9 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "RequestResponse(status='failure', data={'Reason': 'Command sent to the C2 Beacon but no response was ever received.'})" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "blue_env.step(0)\n", "\n", @@ -2411,30 +1266,9 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------------------------------------------------------------------------------------------------------------------+\n", - "| router_1 Access Control List |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| Index | Action | Protocol | Src IP | Src Wildcard | Src Port | Dst IP | Dst Wildcard | Dst Port | Matched |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n", - "| 1 | DENY | ANY | 192.168.10.21 | 0.0.0.1 | 80 | 192.168.1.12 | 0.0.0.1 | 80 | 2 |\n", - "| 18 | PERMIT | ANY | ANY | ANY | 5432 | ANY | ANY | 5432 | 0 |\n", - "| 19 | PERMIT | ANY | ANY | ANY | 53 | ANY | ANY | 53 | 0 |\n", - "| 20 | PERMIT | ANY | ANY | ANY | 21 | ANY | ANY | 21 | 0 |\n", - "| 21 | PERMIT | ANY | ANY | ANY | 80 | ANY | ANY | 80 | 4 |\n", - "| 22 | PERMIT | ANY | ANY | ANY | 219 | ANY | ANY | 219 | 10 |\n", - "| 23 | PERMIT | icmp | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "| 24 | DENY | ANY | ANY | ANY | ANY | ANY | ANY | ANY | 0 |\n", - "+-------+--------+----------+---------------+--------------+----------+--------------+--------------+----------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "router_1.acl.show()" ] @@ -2448,58 +1282,18 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-------------------------------------------------------------------------------------+\n", - "| web_server Software Manager |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| Name | Type | Operating State | Health State | Port | Protocol |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n", - "| ARP | Service | RUNNING | GOOD | 219 | udp |\n", - "| ICMP | Service | RUNNING | GOOD | None | icmp |\n", - "| DNSClient | Service | RUNNING | GOOD | 53 | tcp |\n", - "| NTPClient | Service | RUNNING | GOOD | 123 | udp |\n", - "| WebBrowser | Application | RUNNING | GOOD | 80 | tcp |\n", - "| NMAP | Application | RUNNING | GOOD | None | none |\n", - "| UserSessionManager | Service | RUNNING | GOOD | None | none |\n", - "| UserManager | Service | RUNNING | GOOD | None | none |\n", - "| Terminal | Service | RUNNING | GOOD | 22 | tcp |\n", - "| WebServer | Service | RUNNING | GOOD | 80 | tcp |\n", - "| DatabaseClient | Application | RUNNING | GOOD | 5432 | tcp |\n", - "| C2Beacon | Application | RUNNING | GOOD | None | tcp |\n", - "+--------------------+-------------+-----------------+--------------+------+----------+\n" - ] - } - ], + "outputs": [], "source": [ "web_server.software_manager.show()" ] }, { "cell_type": "code", - "execution_count": 58, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------+\n", - "| database_server File System |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| File Path | Size | Health status | Visible health status | Deleted |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n", - "| database/database.db | 4.77 MB | GOOD | NONE | False |\n", - "| root | 0 B | GOOD | NONE | False |\n", - "+----------------------+---------+---------------+-----------------------+---------+\n" - ] - } - ], + "outputs": [], "source": [ "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" @@ -2507,33 +1301,9 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 3\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['inbound']: 1 -> 0\n", - "root['NODES']['HOST1']['NICS'][1]['TRAFFIC']['tcp'][21]['outbound']: 4 -> 0\n", - "root['NODES']['ROUTER0']['ACL'][1]['permission']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['source_ip_id']: 0 -> 7\n", - "root['NODES']['ROUTER0']['ACL'][1]['source_wildcard_id']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['source_port_id']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['dest_ip_id']: 0 -> 3\n", - "root['NODES']['ROUTER0']['ACL'][1]['dest_wildcard_id']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['dest_port_id']: 0 -> 2\n", - "root['NODES']['ROUTER0']['ACL'][1]['protocol_id']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 1 -> 0\n", - "root['LINKS'][5]['PROTOCOLS']['ALL']: 4 -> 0\n", - "root['LINKS'][6]['PROTOCOLS']['ALL']: 4 -> 0\n" - ] - } - ], + "outputs": [], "source": [ "display_obs_diffs(pre_blue_action_obs, post_blue_action_obs, blue_env.game.step_counter)" ] @@ -2597,17 +1367,9 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:40,175: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -2628,7 +1390,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2656,30 +1418,9 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(2) # Agent Action Equivalent to c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "env.step(3) # Agent action Equivalent to c2_beacon.establish()\n", @@ -2696,30 +1437,9 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "env.step(9) # Equivalent of to c2_beacon.configure(c2_server_ip_address=\"192.168.10.22\")\n", "env.step(3)\n", @@ -2737,23 +1457,9 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+-----------------------------------------------------------------------------------------------------+\n", - "| C2Server Running Status |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n", - "| False | None | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "for i in range(6):\n", " env.step(0)\n", @@ -2776,17 +1482,9 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:40,588: PrimaiteGymEnv RNG seed = None\n" - ] - } - ], + "outputs": [], "source": [ "with open(data_manipulation_config_path()) as f:\n", " cfg = yaml.safe_load(f)\n", @@ -2802,7 +1500,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -2826,23 +1524,9 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 0 | 5 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "c2_beacon.establish()\n", @@ -2860,72 +1544,9 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 4\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 5\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 6\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 7\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 8\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 9\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 10\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 11\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 12\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 13\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "for i in range(10):\n", " keep_alive_obs, _, _, _, _ = blue_config_env.step(0)\n", @@ -2941,23 +1562,9 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 0 | 1 | tcp | 80 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=1)\n", "c2_beacon.establish()\n", @@ -2973,40 +1580,9 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 14\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 15\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "# Comparing the OBS of the default frequency to a timestep frequency of 1\n", "for i in range(2):\n", @@ -3025,52 +1601,9 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 16\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 17\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 18\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 19\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 20\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 21\n", - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 22\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\", keep_alive_frequency=7)\n", "\n", @@ -3107,18 +1640,9 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-04 15:37:41,322: Resetting environment, episode 0, avg. reward: 0.0\n", - "2025-02-04 15:37:41,325: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-04/15-37-31/agent_actions/episode_0.json\n" - ] - } - ], + "outputs": [], "source": [ "blue_config_env.reset()\n", "\n", @@ -3140,28 +1664,9 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 5\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 0 -> 1\n", - "root['LINKS'][1]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][2]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][4]['PROTOCOLS']['ALL']: 0 -> 1\n", - "root['LINKS'][8]['PROTOCOLS']['ALL']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "# Capturing default C2 Traffic\n", "for i in range(3):\n", @@ -3179,23 +1684,9 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+----------------------------------------------------------------------------------------------------------------------------------------------------+\n", - "| C2Beacon Running Status |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| C2 Connection Active | C2 Remote Connection | Keep Alive Inactivity | Keep Alive Frequency | Current Masquerade Protocol | Current Masquerade Port |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n", - "| True | 192.168.10.21 | 0 | 5 | udp | 53 |\n", - "+----------------------+----------------------+-----------------------+----------------------+-----------------------------+-------------------------+\n" - ] - } - ], + "outputs": [], "source": [ "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", "from primaite.utils.validation.port import PORT_LOOKUP\n", @@ -3208,28 +1699,9 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Observation space differences\n", - "-----------------------------\n", - "Step 10\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", - "root['NODES']['HOST0']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['inbound']: 1 -> 0\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['tcp'][80]['outbound']: 1 -> 0\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['inbound']: 0 -> 1\n", - "root['NODES']['HOST2']['NICS'][1]['TRAFFIC']['udp'][53]['outbound']: 0 -> 1\n" - ] - } - ], + "outputs": [], "source": [ "# Capturing UDP C2 Traffic\n", "for i in range(5):\n", diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 4a34619f..41b75e43 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -382,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, @@ -394,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "tags": [] }, @@ -450,7 +450,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb index d7d60d76..c7fdafad 100644 --- a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb +++ b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb @@ -6,7 +6,7 @@ "source": [ "# Getting information out of PrimAITE\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n" + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n" ] }, { @@ -32,8 +32,6 @@ "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.simulator.network.hardware.nodes.host.computer import Computer\n", "from notebook.services.config import ConfigManager\n", - "from primaite.game.agent.scripted_agents import probabilistic_agent\n", - "\n", "\n", "cm = ConfigManager().update('notebook', {'limit_output': 50}) # limit output lines to 50 - for neatness\n", "\n", diff --git a/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb index 35d3813a..cbb898ea 100644 --- a/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb +++ b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb @@ -6,7 +6,7 @@ "source": [ "# Simulating Privilege Escalation and Data Loss Using SSH and ACLs Manipulation\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "## Overview\n", "\n", @@ -62,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, @@ -77,7 +77,8 @@ "from primaite.simulator.network.hardware.nodes.host.server import Server\n", "from primaite.simulator.system.applications.database_client import DatabaseClient\n", "from primaite.simulator.system.applications.web_browser import WebBrowser\n", - "from primaite.simulator.system.services.database.database_service import DatabaseService\n" + "from primaite.simulator.system.services.database.database_service import DatabaseService\n", + "from primaite.simulator.network.hardware.nodes.network import firewall\n" ] }, { @@ -89,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "tags": [] }, @@ -112,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "tags": [] }, diff --git a/src/primaite/notebooks/Requests-and-Responses.ipynb b/src/primaite/notebooks/Requests-and-Responses.ipynb index 83aed07c..01a6cffa 100644 --- a/src/primaite/notebooks/Requests-and-Responses.ipynb +++ b/src/primaite/notebooks/Requests-and-Responses.ipynb @@ -6,7 +6,7 @@ "source": [ "# Requests and Responses\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "Agents interact with the PrimAITE simulation via the Request system.\n" ] @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -47,17 +47,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "sim = Simulation()\n", "sim.network.add_node(\n", - " HostNode(\n", - " hostname=\"client\",\n", - " ip_address='10.0.0.1',\n", - " subnet_mask='255.255.255.0',\n", - " operating_state=NodeOperatingState.ON)\n", + " HostNode.from_config(\n", + " config = {\n", + " 'type': \"hostnode\",\n", + " 'hostname': \"client\",\n", + " 'ip_address': '10.0.0.1',\n", + " 'subnet_mask': '255.255.255.0',\n", + " 'operating_state': NodeOperatingState.ON,\n", + " }\n", + " )\n", ")\n", "client = sim.network.get_node_by_hostname('client')\n" ] @@ -210,7 +214,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb index 9aa4e96a..48318c13 100644 --- a/src/primaite/notebooks/Terminal-Processing.ipynb +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -39,12 +39,43 @@ "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", "from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection\n", "\n", + "# print(dir(Computer))\n", + "# node_a = Computer.from_config(\n", + "# config = {\n", + "# \"type\": \"computer\",\n", + "# \"hostname\": \"node_a\",\n", + "# \"ip_address\": \"192.168.0.10\",\n", + "# \"subnet_mask\": \"255.255.255.0\",\n", + "# \"startup_duration\": 0,\n", + "# }\n", + "# )\n", + "# print(f\"{node_a=}\")\n", + "\n", "def basic_network() -> Network:\n", " \"\"\"Utility function for creating a default network to demonstrate Terminal functionality\"\"\"\n", " network = Network()\n", - " node_a = Computer(hostname=\"node_a\", ip_address=\"192.168.0.10\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", + " # node_a = Computer(hostname=\"node_a\", ip_address=\"192.168.0.10\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", + " node_a = Computer.from_config(\n", + " config = {\n", + " \"type\": \"computer\",\n", + " \"hostname\": \"node_a\",\n", + " \"ip_address\": \"192.168.0.10\",\n", + " \"subnet_mask\": \"255.255.255.0\",\n", + " \"startup_duration\": 0,\n", + " }\n", + " )\n", + " print(f\"{node_a=}\")\n", " node_a.power_on()\n", - " node_b = Computer(hostname=\"node_b\", ip_address=\"192.168.0.11\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", + " # node_b = Computer(hostname=\"node_b\", ip_address=\"192.168.0.11\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", + " node_b = Computer.from_config(\n", + " config = {\n", + " \"type\": \"computer\",\n", + " \"hostname\": \"node_b\",\n", + " \"ip_address\": \"192.168.0.11\",\n", + " \"subnet_mask\": \"255.255.255.0\",\n", + " \"startup_duration\": 0,\n", + " }\n", + " )\n", " node_b.power_on()\n", " network.connect(node_a.network_interface[1], node_b.network_interface[1])\n", " return network" @@ -84,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -211,7 +242,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -225,7 +256,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb index dadb399e..87d9c377 100644 --- a/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb +++ b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb @@ -6,7 +6,7 @@ "source": [ "# Train a Multi agent system using RLLIB\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This notebook will demonstrate how to use the `PrimaiteRayMARLEnv` to train a very basic system with two PPO agents." ] @@ -57,7 +57,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -103,20 +103,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "*** SIGTERM received at time=1737996337 on cpu 7 ***\n", - "PC: @ 0x7f3649b0fe2e (unknown) epoll_wait\n", - " @ 0x7f3649a2c520 (unknown) (unknown)\n", - "[2025-01-27 16:45:37,381 E 117142 117142] logging.cc:440: *** SIGTERM received at time=1737996337 on cpu 7 ***\n", - "[2025-01-27 16:45:37,381 E 117142 117142] logging.cc:440: PC: @ 0x7f3649b0fe2e (unknown) epoll_wait\n", - "[2025-01-27 16:45:37,381 E 117142 117142] logging.cc:440: @ 0x7f3649a2c520 (unknown) (unknown)\n" - ] - } - ], + "outputs": [], "source": [ "eval = algo.evaluate()" ] @@ -127,18 +114,6 @@ "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb index 64a9e7ab..79740bca 100644 --- a/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb @@ -6,7 +6,7 @@ "source": [ "# Train a Single agent system using RLLib\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This notebook will demonstrate how to use PrimaiteRayEnv to train a basic PPO agent." ] @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -98,20 +98,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "*** SIGTERM received at time=1737996055 on cpu 1 ***\n", - "PC: @ 0x7f6e254a6e2e (unknown) epoll_wait\n", - " @ 0x7f6e253c3520 (unknown) (unknown)\n", - "[2025-01-27 16:40:55,343 E 114171 114171] logging.cc:440: *** SIGTERM received at time=1737996055 on cpu 1 ***\n", - "[2025-01-27 16:40:55,343 E 114171 114171] logging.cc:440: PC: @ 0x7f6e254a6e2e (unknown) epoll_wait\n", - "[2025-01-27 16:40:55,344 E 114171 114171] logging.cc:440: @ 0x7f6e253c3520 (unknown) (unknown)\n" - ] - } - ], + "outputs": [], "source": [ "eval = algo.evaluate()" ] @@ -122,18 +109,6 @@ "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index d3492f92..10328989 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -6,7 +6,7 @@ "source": [ "# Training an SB3 Agent\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This notebook will demonstrate how to use primaite to create and train a PPO agent, using a pre-defined configuration file." ] diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index 311fe4fb..44305266 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -6,7 +6,7 @@ "source": [ "# Using Episode Schedules\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "PrimAITE supports the ability to use different variations on a scenario at different episodes. This can be used to increase \n", "domain randomisation to prevent overfitting, or to set up curriculum learning to train agents to perform more complicated tasks.\n", @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -325,7 +325,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -413,6 +413,18 @@ "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/multi-processing.ipynb b/src/primaite/notebooks/multi-processing.ipynb index 798bf3ff..e56bf362 100644 --- a/src/primaite/notebooks/multi-processing.ipynb +++ b/src/primaite/notebooks/multi-processing.ipynb @@ -6,7 +6,7 @@ "source": [ "# Simple multi-processing demonstration\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This notebook uses SubprocVecEnv from SB3." ] @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -37,13 +37,12 @@ "from stable_baselines3 import PPO\n", "from stable_baselines3.common.utils import set_random_seed\n", "from stable_baselines3.common.vec_env import SubprocVecEnv\n", - "from primaite.session.environment import PrimaiteGymEnv\n", - "from primaite.game.agent.scripted_agents import probabilistic_agent\n" + "from primaite.session.environment import PrimaiteGymEnv\n" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -52,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -69,7 +68,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -90,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 6543d793..cc3d4150 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1197,7 +1197,7 @@ class UserSessionManager(Service, identifier="UserSessionManager"): """Request should take the form [username, password, remote_ip_address].""" username, password, remote_ip_address = request response = RequestResponse.from_bool(self.remote_login(username, password, remote_ip_address)) - response.data = {"remote_hostname": self.parent.hostname, "username": username} + response.data = {"remote_hostname": self.parent.config.hostname, "username": username} return response rm.add_request("remote_login", RequestType(func=_remote_login)) @@ -1230,7 +1230,7 @@ class UserSessionManager(Service, identifier="UserSessionManager"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.parent.hostname} User Sessions" + table.title = f"{self.parent.config.hostname} User Sessions" def _add_session_to_table(user_session: UserSession): """ @@ -1627,9 +1627,7 @@ class Node(SimComponent, ABC): dns_server=kwargs["config"].dns_server, ) super().__init__(**kwargs) - self.operating_state = ( - NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else NodeOperatingState[p.upper()] - ) + self.operating_state = NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else p self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager From 7b2a9c1d2a6d64d870de17fa0a3c8afc15641919 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Wed, 5 Feb 2025 16:46:25 +0000 Subject: [PATCH 192/224] #3075: Pre-commit changes. --- src/primaite/notebooks/Requests-and-Responses.ipynb | 4 ++-- src/primaite/notebooks/Terminal-Processing.ipynb | 4 ++-- src/primaite/notebooks/Training-an-SB3-Agent.ipynb | 10 +++++----- src/primaite/notebooks/Using-Episode-Schedules.ipynb | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/primaite/notebooks/Requests-and-Responses.ipynb b/src/primaite/notebooks/Requests-and-Responses.ipynb index 01a6cffa..48f1cf3e 100644 --- a/src/primaite/notebooks/Requests-and-Responses.ipynb +++ b/src/primaite/notebooks/Requests-and-Responses.ipynb @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -47,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb index 48318c13..dc870db5 100644 --- a/src/primaite/notebooks/Terminal-Processing.ipynb +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -115,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb index 10328989..1bec0183 100644 --- a/src/primaite/notebooks/Training-an-SB3-Agent.ipynb +++ b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -101,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -133,7 +133,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/primaite/notebooks/Using-Episode-Schedules.ipynb b/src/primaite/notebooks/Using-Episode-Schedules.ipynb index 44305266..4bc49a70 100644 --- a/src/primaite/notebooks/Using-Episode-Schedules.ipynb +++ b/src/primaite/notebooks/Using-Episode-Schedules.ipynb @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -325,7 +325,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ From 1a8c3b9471e591659eb69853a4865b1a23708ad8 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 6 Feb 2025 16:42:26 +0000 Subject: [PATCH 193/224] #3075: Fix notebooks after test changes. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 82 +++++++++---------- ...a-Manipulation-Customising-Red-Agent.ipynb | 16 ++-- .../Data-Manipulation-E2E-Demonstration.ipynb | 4 +- ...ege-Escalation-and-Data-Loss-Example.ipynb | 30 +++---- .../notebooks/Requests-and-Responses.ipynb | 13 +-- .../notebooks/Terminal-Processing.ipynb | 26 ++---- .../simulator/network/hardware/base.py | 4 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../configs/basic_switched_network.yaml | 6 +- 9 files changed, 86 insertions(+), 97 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 17b7b79e..474dadff 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -59,30 +59,30 @@ "custom_c2_agent = \"\"\"\n", " - ref: CustomC2Agent\n", " team: RED\n", - " type: ProxyAgent\n", + " type: proxy-agent\n", "\n", " action_space:\n", " action_map:\n", " 0:\n", - " action: do_nothing\n", + " action: do-nothing\n", " options: {}\n", " 1:\n", - " action: node_application_install\n", + " action: node-application-install\n", " options:\n", " node_name: web_server\n", - " application_name: C2Beacon\n", + " application_name: c2-beacon\n", " 2:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", " node_name: web_server\n", " c2_server_ip_address: 192.168.10.21\n", " 3:\n", - " action: node_application_execute\n", + " action: node-application-execute\n", " options:\n", " node_name: web_server\n", - " application_name: C2Beacon\n", + " application_name: c2-beacon\n", " 4:\n", - " action: c2_server_terminal_command\n", + " action: c2-server-terminal-command\n", " options:\n", " node_name: client_1\n", " ip_address:\n", @@ -93,7 +93,7 @@ " - software_manager\n", " - application\n", " - install\n", - " - RansomwareScript\n", + " - ransomware-script\n", " 5:\n", " action: c2-server-ransomware-configure\n", " options:\n", @@ -101,7 +101,7 @@ " server_ip_address: 192.168.1.14\n", " payload: ENCRYPT\n", " 6:\n", - " action: c2_server_data_exfiltrate\n", + " action: c2-server-data-exfiltrate\n", " options:\n", " node_name: client_1\n", " target_file_name: \"database.db\"\n", @@ -112,23 +112,23 @@ " password: admin\n", "\n", " 7:\n", - " action: c2_server_ransomware_launch\n", + " action: c2-server-ransomware-launch\n", " options:\n", " node_name: client_1\n", " 8:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", " node_name: web_server\n", " c2_server_ip_address: 192.168.10.21\n", " 9:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", " node_name: web_server\n", " c2_server_ip_address: 192.168.10.22\n", "\n", " reward_function:\n", " reward_components:\n", - " - type: DUMMY\n", + " - type: dummy\n", "\"\"\"\n", "c2_agent_yaml = yaml.safe_load(custom_c2_agent)" ] @@ -169,7 +169,7 @@ "source": [ "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server.run()\n", "client_1.software_manager.show()" ] @@ -262,7 +262,7 @@ "outputs": [], "source": [ "env.step(2)\n", - "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", "web_server.software_manager.show()\n", "c2_beacon.show()" ] @@ -435,7 +435,7 @@ "metadata": {}, "outputs": [], "source": [ - "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", + "ransomware_script: RansomwareScript = web_server.software_manager.software[\"ransomware-script\"]\n", "web_server.software_manager.show()\n", "ransomware_script.show()" ] @@ -574,20 +574,20 @@ "custom_blue_agent_yaml = \"\"\"\n", " - ref: defender\n", " team: BLUE\n", - " type: ProxyAgent\n", + " type: proxy-agent\n", "\n", " observation_space:\n", - " type: CUSTOM\n", + " type: custom\n", " options:\n", " components:\n", - " - type: NODES\n", - " label: NODES\n", + " - type: nodes\n", + " label: nodes\n", " options:\n", " hosts:\n", " - hostname: web_server\n", " applications:\n", - " - application_name: C2Beacon\n", - " - application_name: RansomwareScript\n", + " - application_name: c2-beacon\n", + " - application_name: ransomware-script\n", " folders:\n", " - folder_name: exfiltration_folder\n", " files:\n", @@ -637,7 +637,7 @@ " - UDP\n", " num_rules: 10\n", "\n", - " - type: LINKS\n", + " - type: links\n", " label: LINKS\n", " options:\n", " link_references:\n", @@ -651,26 +651,26 @@ " - switch_2:eth-1<->client_1:eth-1\n", " - switch_2:eth-2<->client_2:eth-1\n", " - switch_2:eth-7<->security_suite:eth-2\n", - " - type: \"NONE\"\n", + " - type: \"none\"\n", " label: ICS\n", " options: {}\n", "\n", " action_space:\n", " action_map:\n", " 0:\n", - " action: do_nothing\n", + " action: do-nothing\n", " options: {}\n", " 1:\n", - " action: node_application_remove\n", + " action: node-application-remove\n", " options:\n", " node_name: web_server\n", - " application_name: C2Beacon\n", + " application_name: c2-beacon\n", " 2:\n", - " action: node_shutdown\n", + " action: node-shutdown\n", " options:\n", " node_name: web_server\n", " 3:\n", - " action: router_acl_add_rule\n", + " action: router-acl-add-rule\n", " options:\n", " target_router: router_1\n", " position: 1\n", @@ -685,7 +685,7 @@ "\n", " reward_function:\n", " reward_components:\n", - " - type: DUMMY\n", + " - type: dummy\n", "\n", " agent_settings:\n", " flatten_obs: False\n", @@ -776,12 +776,12 @@ "\n", "# Installing the C2 Server.\n", "client_1.software_manager.install(C2Server)\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server.run()\n", "\n", "# Installing the C2 Beacon.\n", "web_server.software_manager.install(C2Beacon)\n", - "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", "c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", "c2_beacon.establish()" ] @@ -825,7 +825,7 @@ "outputs": [], "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", - "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"ransomware-script\"]],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" @@ -984,11 +984,11 @@ " web_server: Server = given_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "\n", " client_1.software_manager.install(C2Server)\n", - " c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + " c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", " c2_server.run()\n", "\n", " web_server.software_manager.install(C2Beacon)\n", - " c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + " c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", " c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", " c2_beacon.establish()\n", "\n", @@ -1086,7 +1086,7 @@ " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" ] }, @@ -1174,7 +1174,7 @@ " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" ] }, @@ -1396,16 +1396,16 @@ "source": [ "web_server: Server = c2_config_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "web_server.software_manager.install(C2Beacon)\n", - "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", "\n", "client_1: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", - "c2_server_1: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server_1: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server_1.run()\n", "\n", "client_2: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_2\")\n", "client_2.software_manager.install(C2Server)\n", - "c2_server_2: C2Server = client_2.software_manager.software[\"C2Server\"]\n", + "c2_server_2: C2Server = client_2.software_manager.software[\"c2-server\"]\n", "c2_server_2.run()" ] }, diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 14938c02..2e0a0d40 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -76,9 +76,9 @@ " # parse the info dict form step output and write out what the red agent is doing\n", " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", - " if red_action == 'do_nothing':\n", + " if red_action == 'do-nothing':\n", " red_str = 'DO NOTHING'\n", - " elif red_action == 'node_application_execute':\n", + " elif red_action == 'node-application-execute':\n", " red_str = f\"ATTACK from {red_info.parameters['node_name']}\"\n", " return red_str" ] @@ -369,18 +369,18 @@ "change = yaml.safe_load(\"\"\"\n", " applications:\n", " - ref: data_manipulation_bot\n", - " type: DataManipulationBot\n", + " type: data-manipulation-bot\n", " options:\n", " port_scan_p_of_success: 1.0\n", " data_manipulation_p_of_success: 1.0\n", " payload: \"DELETE\"\n", " server_ip: 192.168.1.14\n", " - ref: client_1_web_browser\n", - " type: WebBrowser\n", + " type: web-browser\n", " options:\n", " target_url: http://arcd.com/users/\n", " - ref: client_1_database_client\n", - " type: DatabaseClient\n", + " type: database-client\n", " options:\n", " db_server_ip: 192.168.1.14\n", "\"\"\")\n", @@ -414,18 +414,18 @@ "change = yaml.safe_load(\"\"\"\n", " applications:\n", " - ref: data_manipulation_bot\n", - " type: DataManipulationBot\n", + " type: data-manipulation-bot\n", " options:\n", " port_scan_p_of_success: 0.0\n", " data_manipulation_p_of_success: 0.0\n", " payload: \"DELETE\"\n", " server_ip: 192.168.1.14\n", " - ref: client_1_web_browser\n", - " type: WebBrowser\n", + " type: web-browser\n", " options:\n", " target_url: http://arcd.com/users/\n", " - ref: client_1_database_client\n", - " type: DatabaseClient\n", + " type: database-client\n", " options:\n", " db_server_ip: 192.168.1.14\n", "\"\"\")\n", diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 41b75e43..2d3f99b2 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -458,9 +458,9 @@ " # parse the info dict form step output and write out what the red agent is doing\n", " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", - " if red_action == 'do_nothing':\n", + " if red_action == 'do-nothing':\n", " red_str = 'DO NOTHING'\n", - " elif red_action == 'node_application_execute':\n", + " elif red_action == 'node-application-execute':\n", " client = \"client 1\" if red_info.parameters['node_name'] == 0 else \"client 2\"\n", " red_str = f\"ATTACK from {client}\"\n", " return red_str" diff --git a/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb index cbb898ea..deb38eea 100644 --- a/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb +++ b/src/primaite/notebooks/Privilege-Escalation-and-Data-Loss-Example.ipynb @@ -120,11 +120,11 @@ "outputs": [], "source": [ "some_tech_jnr_dev_pc: Computer = game.simulation.network.get_node_by_hostname(\"some_tech_jnr_dev_pc\")\n", - "some_tech_jnr_dev_db_client: DatabaseClient = some_tech_jnr_dev_pc.software_manager.software[\"DatabaseClient\"]\n", - "some_tech_jnr_dev_web_browser: WebBrowser = some_tech_jnr_dev_pc.software_manager.software[\"WebBrowser\"]\n", + "some_tech_jnr_dev_db_client: DatabaseClient = some_tech_jnr_dev_pc.software_manager.software[\"database-client\"]\n", + "some_tech_jnr_dev_web_browser: WebBrowser = some_tech_jnr_dev_pc.software_manager.software[\"web-browser\"]\n", "some_tech_rt: Router = game.simulation.network.get_node_by_hostname(\"some_tech_rt\")\n", "some_tech_db_srv: Server = game.simulation.network.get_node_by_hostname(\"some_tech_db_srv\")\n", - "some_tech_db_service: DatabaseService = some_tech_db_srv.software_manager.software[\"DatabaseService\"]\n", + "some_tech_db_service: DatabaseService = some_tech_db_srv.software_manager.software[\"database-service\"]\n", "some_tech_storage_srv: Server = game.simulation.network.get_node_by_hostname(\"some_tech_storage_srv\")\n", "some_tech_web_srv: Server = game.simulation.network.get_node_by_hostname(\"some_tech_web_srv\")" ] @@ -211,7 +211,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"node_session_remote_login\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", + " \"service\", \"terminal\", \"node-session-remote-login\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] @@ -233,7 +233,7 @@ }, "outputs": [], "source": [ - "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"WebBrowser\", \"execute\"]\n", + "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"web-browser\", \"execute\"]\n", "game.simulation.apply_request(caos_action)" ] }, @@ -256,7 +256,7 @@ "metadata": {}, "outputs": [], "source": [ - "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"UserSessionManager\"][\"active_remote_sessions\"]" + "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"user-session-manager\"][\"active_remote_sessions\"]" ] }, { @@ -269,7 +269,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"node_session_remote_login\", \"admin\", \"admin\", str(some_tech_rt.network_interface[4].ip_address)\n", + " \"service\", \"terminal\", \"node-session-remote-login\", \"admin\", \"admin\", str(some_tech_rt.network_interface[4].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] @@ -282,7 +282,7 @@ }, "outputs": [], "source": [ - "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"UserSessionManager\"][\"active_remote_sessions\"]" + "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"user-session-manager\"][\"active_remote_sessions\"]" ] }, { @@ -315,7 +315,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"send_remote_command\", str(some_tech_rt.network_interface[4].ip_address),\n", + " \"service\", \"terminal\", \"send_remote_command\", str(some_tech_rt.network_interface[4].ip_address),\n", " {\n", " \"command\": [\n", " \"acl\", \"add_rule\", \"PERMIT\", \"TCP\",\n", @@ -368,7 +368,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"remote_logoff\", str(some_tech_rt.network_interface[4].ip_address)\n", + " \"service\", \"terminal\", \"remote_logoff\", str(some_tech_rt.network_interface[4].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] @@ -386,7 +386,7 @@ "metadata": {}, "outputs": [], "source": [ - "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"UserSessionManager\"][\"active_remote_sessions\"]" + "game.get_sim_state()[\"network\"][\"nodes\"][\"some_tech_rt\"][\"services\"][\"user-session-manager\"][\"active_remote_sessions\"]" ] }, { @@ -406,7 +406,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"node_session_remote_login\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", + " \"service\", \"terminal\", \"node-session-remote-login\", \"admin\", \"admin\", str(some_tech_storage_srv.network_interface[1].ip_address)\n", "]\n", "game.simulation.apply_request(caos_action)" ] @@ -421,7 +421,7 @@ "source": [ "caos_action = [\n", " \"network\", \"node\", \"some_tech_jnr_dev_pc\", \n", - " \"service\", \"Terminal\", \"send_remote_command\", str(some_tech_storage_srv.network_interface[1].ip_address),\n", + " \"service\", \"terminal\", \"send_remote_command\", str(some_tech_storage_srv.network_interface[1].ip_address),\n", " {\n", " \"command\": [\n", " \"file_system\", \"delete\", \"file\", db_backup_folder, \"database.db\"\n", @@ -476,7 +476,7 @@ }, "outputs": [], "source": [ - "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"WebBrowser\", \"execute\"]\n", + "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"web-browser\", \"execute\"]\n", "game.simulation.apply_request(caos_action)" ] }, @@ -535,7 +535,7 @@ }, "outputs": [], "source": [ - "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"WebBrowser\", \"execute\"]\n", + "caos_action = [\"network\", \"node\", \"some_tech_jnr_dev_pc\", \"application\", \"web-browser\", \"execute\"]\n", "game.simulation.apply_request(caos_action)" ] }, diff --git a/src/primaite/notebooks/Requests-and-Responses.ipynb b/src/primaite/notebooks/Requests-and-Responses.ipynb index 48f1cf3e..c29d41dc 100644 --- a/src/primaite/notebooks/Requests-and-Responses.ipynb +++ b/src/primaite/notebooks/Requests-and-Responses.ipynb @@ -52,14 +52,15 @@ "outputs": [], "source": [ "sim = Simulation()\n", + "\n", "sim.network.add_node(\n", " HostNode.from_config(\n", " config = {\n", - " 'type': \"hostnode\",\n", + " 'type': \"host-node\",\n", " 'hostname': \"client\",\n", " 'ip_address': '10.0.0.1',\n", " 'subnet_mask': '255.255.255.0',\n", - " 'operating_state': NodeOperatingState.ON,\n", + " 'operating_state': \"ON\",\n", " }\n", " )\n", ")\n", @@ -98,7 +99,7 @@ "outputs": [], "source": [ "response = sim.apply_request(\n", - " request=[\"network\", \"node\", \"client\", \"service\", \"DNSClient\", \"stop\"],\n", + " request=[\"network\", \"node\", \"client\", \"service\", \"dns-client\", \"stop\"],\n", " context={}\n", " )\n", "print(response)" @@ -118,7 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"DNS Client state: {client.software_manager.software.get('DNSClient').operating_state.name}\")" + "print(f\"DNS Client state: {client.software_manager.software.get('dns-client').operating_state.name}\")" ] }, { @@ -142,7 +143,7 @@ "outputs": [], "source": [ "response = sim.apply_request(\n", - " request=[\"network\", \"node\", \"client\", \"service\", \"NonExistentApplication\", \"stop\"],\n", + " request=[\"network\", \"node\", \"client\", \"service\", \"non-existent-application\", \"stop\"],\n", " context={}\n", " )\n", "print(response)" @@ -205,7 +206,7 @@ "outputs": [], "source": [ "response = sim.apply_request(\n", - " request=[\"network\", \"node\", \"client\", \"service\", \"DNSClient\", \"start\"],\n", + " request=[\"network\", \"node\", \"client\", \"service\", \"dns-client\", \"start\"],\n", " context={}\n", " )\n", "print(response)" diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb index dc870db5..5077732b 100644 --- a/src/primaite/notebooks/Terminal-Processing.ipynb +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -6,7 +6,7 @@ "source": [ "# Terminal Processing\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK" + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK" ] }, { @@ -39,41 +39,27 @@ "from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript\n", "from primaite.simulator.system.services.terminal.terminal import RemoteTerminalConnection\n", "\n", - "# print(dir(Computer))\n", - "# node_a = Computer.from_config(\n", - "# config = {\n", - "# \"type\": \"computer\",\n", - "# \"hostname\": \"node_a\",\n", - "# \"ip_address\": \"192.168.0.10\",\n", - "# \"subnet_mask\": \"255.255.255.0\",\n", - "# \"startup_duration\": 0,\n", - "# }\n", - "# )\n", - "# print(f\"{node_a=}\")\n", - "\n", "def basic_network() -> Network:\n", " \"\"\"Utility function for creating a default network to demonstrate Terminal functionality\"\"\"\n", " network = Network()\n", - " # node_a = Computer(hostname=\"node_a\", ip_address=\"192.168.0.10\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", " node_a = Computer.from_config(\n", " config = {\n", " \"type\": \"computer\",\n", " \"hostname\": \"node_a\",\n", " \"ip_address\": \"192.168.0.10\",\n", " \"subnet_mask\": \"255.255.255.0\",\n", - " \"startup_duration\": 0,\n", + " # \"startup_duration\": 0,\n", " }\n", " )\n", " print(f\"{node_a=}\")\n", " node_a.power_on()\n", - " # node_b = Computer(hostname=\"node_b\", ip_address=\"192.168.0.11\", subnet_mask=\"255.255.255.0\", start_up_duration=0)\n", " node_b = Computer.from_config(\n", " config = {\n", " \"type\": \"computer\",\n", " \"hostname\": \"node_b\",\n", " \"ip_address\": \"192.168.0.11\",\n", " \"subnet_mask\": \"255.255.255.0\",\n", - " \"startup_duration\": 0,\n", + " # \"startup_duration\": 0,\n", " }\n", " )\n", " node_b.power_on()\n", @@ -98,9 +84,9 @@ "source": [ "network: Network = basic_network()\n", "computer_a: Computer = network.get_node_by_hostname(\"node_a\")\n", - "terminal_a: Terminal = computer_a.software_manager.software.get(\"Terminal\")\n", + "terminal_a: Terminal = computer_a.software_manager.software.get(\"terminal\")\n", "computer_b: Computer = network.get_node_by_hostname(\"node_b\")\n", - "terminal_b: Terminal = computer_b.software_manager.software.get(\"Terminal\")" + "terminal_b: Terminal = computer_b.software_manager.software.get(\"terminal\")" ] }, { @@ -152,7 +138,7 @@ "metadata": {}, "outputs": [], "source": [ - "term_a_term_b_remote_connection.execute([\"software_manager\", \"application\", \"install\", \"RansomwareScript\"])" + "term_a_term_b_remote_connection.execute([\"software_manager\", \"application\", \"install\", \"ransomware-script\"])" ] }, { diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 4712af60..4c252050 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1632,7 +1632,9 @@ class Node(SimComponent, ABC): dns_server=kwargs["config"].dns_server, ) super().__init__(**kwargs) - self.operating_state = NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else p + self.operating_state = ( + NodeOperatingState.ON if not (p := kwargs["config"].operating_state) else NodeOperatingState[p.upper()] + ) self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 76d9167c..d38ba097 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -313,7 +313,7 @@ class HostNode(Node, discriminator="host-node"): """ SYSTEM_SOFTWARE: ClassVar[Dict] = { - "HostARP": HostARP, + "host-arp": HostARP, "icmp": ICMP, "dns-client": DNSClient, "ntp-client": NTPClient, diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index c9ac5f8d..4c6d2557 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -34,10 +34,10 @@ agents: action_space: action_map: 0: - action: do-nothing + action: do_nothing options: {} 1: - action: node-application-execute + action: node_application_execute options: node_name: client_2 application_name: web-browser @@ -109,7 +109,7 @@ agents: action_space: action_map: 0: - action: do-nothing + action: do_nothing options: {} reward_function: From fc66e125d64d882c78109d008e7f84039cc19e32 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 7 Feb 2025 10:08:18 +0000 Subject: [PATCH 194/224] Fix action schema for change password --- src/primaite/game/agent/actions/session.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 58a8a555..b7b56d17 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -63,8 +63,6 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="no class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="node-session-remote-logoff"): """Action which performs a remote session logout.""" - config: "NodeSessionsRemoteLogoutAction.ConfigSchema" - class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLogoutAction.""" @@ -78,14 +76,13 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="n return ["network", "node", config.node_name, "service", "terminal", config.verb, config.remote_ip] -class NodeAccountChangePasswordAction(NodeSessionAbstractAction, discriminator="node-account-change-password"): +class NodeAccountChangePasswordAction(AbstractAction, discriminator="node-account-change-password"): """Action which changes the password for a user.""" - config: "NodeAccountChangePasswordAction.ConfigSchema" - - class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for NodeAccountsChangePasswordAction.""" + node_name: str username: str current_password: str new_password: str From 96549e68aa9cddff6e1eecc9da35a288e503d021 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 10 Feb 2025 14:39:28 +0000 Subject: [PATCH 195/224] Merge remote-tracking branch 'origin/dev' into 4.0.0-dev --- CHANGELOG.md | 10 + docs/source/configuration/agents.rst | 3 +- .../nodes/common/common_node_attributes.rst | 33 ++ .../simulation/nodes/network_examples.rst | 12 +- .../configuration/simulation/nodes/router.rst | 6 +- .../how_to_guides/extensible_agents.rst | 6 +- .../source/how_to_guides/extensible_nodes.rst | 8 +- docs/source/request_system.rst | 2 + .../simulation_components/network/network.rst | 8 +- .../network/nodes/wireless_router.rst | 4 +- .../system/applications/c2_suite.rst | 2 +- .../applications/data_manipulation_bot.rst | 4 +- .../system/applications/database_client.rst | 2 +- .../system/applications/ransomware_script.rst | 2 +- .../system/applications/web_browser.rst | 2 +- .../system/services/database_service.rst | 2 +- .../system/services/dns_client.rst | 2 +- .../system/services/dns_server.rst | 2 +- .../system/services/ftp_client.rst | 2 +- .../system/services/ftp_server.rst | 2 +- .../system/services/ntp_client.rst | 2 +- .../system/services/ntp_server.rst | 2 +- .../system/services/terminal.rst | 131 ++++- .../system/services/web_server.rst | 2 +- .../simulation_components/system/software.rst | 2 +- src/primaite/game/agent/actions/node.py | 71 ++- src/primaite/game/agent/actions/session.py | 13 +- src/primaite/game/agent/interface.py | 54 +- .../observations/file_system_observations.py | 53 +- .../agent/observations/host_observations.py | 46 +- .../agent/observations/nic_observations.py | 49 +- .../agent/observations/node_observations.py | 18 +- .../agent/observations/observation_manager.py | 9 +- .../game/agent/observations/observations.py | 36 +- .../observations/software_observation.py | 78 ++- src/primaite/game/game.py | 25 +- ...ommand-and-Control-E2E-Demonstration.ipynb | 174 +++---- ...a-Manipulation-Customising-Red-Agent.ipynb | 56 +- .../Data-Manipulation-E2E-Demonstration.ipynb | 11 +- .../Getting-Information-Out-Of-PrimAITE.ipynb | 45 +- .../How-To-Use-Primaite-Dev-Mode.ipynb | 479 ++++++++++++++++++ .../notebooks/Requests-and-Responses.ipynb | 2 +- .../notebooks/Terminal-Processing.ipynb | 286 ++++++++++- src/primaite/session/environment.py | 30 +- .../simulator/network/hardware/base.py | 22 +- .../network/hardware/nodes/host/host_node.py | 19 +- .../hardware/nodes/network/firewall.py | 2 +- .../network/hardware/nodes/network/router.py | 21 +- .../network/transmission/data_link_layer.py | 2 +- .../simulator/system/services/arp/arp.py | 3 +- .../system/services/terminal/terminal.py | 78 ++- .../system/services/web_server/web_server.py | 55 +- .../configs/basic_switched_network.yaml | 26 +- .../configs/nodes_with_initial_files.yaml | 226 +++++++++ .../test_game_options_config.py | 41 +- .../test_node_file_system_config.py | 64 +++ .../extensions/nodes/giga_switch.py | 2 +- .../actions/test_terminal_actions.py | 54 +- .../actions/test_user_account_actions.py | 176 +++++++ .../test_file_system_observations.py | 32 ++ .../observations/test_nic_observations.py | 27 +- .../observations/test_node_observations.py | 2 + .../observations/test_obs_data_capture.py | 28 + .../test_software_observations.py | 38 +- .../game_layer/test_RNG_seed.py | 22 + .../game_layer/test_actions.py | 10 +- .../network/test_capture_nmne.py | 22 + tests/integration_tests/system/test_arp.py | 21 +- .../_game/_agent/test_observations.py | 227 ++++++++- .../_system/_services/test_ftp_client.py | 2 +- .../_system/_services/test_terminal.py | 57 +++ 71 files changed, 2700 insertions(+), 367 deletions(-) create mode 100644 src/primaite/notebooks/How-To-Use-Primaite-Dev-Mode.ipynb create mode 100644 tests/assets/configs/nodes_with_initial_files.yaml create mode 100644 tests/integration_tests/configuration_file_parsing/test_node_file_system_config.py create mode 100644 tests/integration_tests/game_layer/actions/test_user_account_actions.py create mode 100644 tests/integration_tests/game_layer/observations/test_obs_data_capture.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f87f54e..871b9923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [4.0.0] = TBC ### Added +- Log observation space data by episode and step. +- Added `show_history` method to Agents, allowing you to view actions taken by an agent per step. By default, `do-nothing` actions are omitted. +- New ``node-send-local-command`` action implemented which grants agents the ability to execute commands locally. (Previously limited to remote only) +- Added ability to set the observation threshold for NMNE, file access and application executions ### Changed - Agents now follow a common configuration format, simplifying the configuration of agents and their extensibilty. @@ -24,6 +28,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated tests that don't use YAMLs to still use the new action and agent schemas - Nodes now use a config schema and are extensible, allowing for plugin support. - Node tests have been updated to use the new node config schemas when not using YAML files. +- ACLs are no longer applied to layer-2 traffic. +- Random number seed values are recorded in simulation/seed.log if the seed is set in the config file + or `generate_seed_value` is set to `true`. +- ARP .show() method will now include the port number associated with each entry. +- Added `services_requires_scan` and `applications_requires_scan` to agent observation space config to allow the agents to be able to see actual health states of services and applications without requiring scans (Default `True`, set to `False` to allow agents to see actual health state without scanning). +- Updated the `Terminal` class to provide response information when sending remote command execution. ### Fixed - DNS client no longer fails to check its cache if a DNS server address is missing. diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index dce8da3a..ee84aede 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -21,7 +21,7 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo team: GREEN type: probabilistic-agent observation_space: - type: UC2GreenObservation + type: UC2GreenObservation # TODO: what action_space: reward_function: reward_components: @@ -160,3 +160,4 @@ If ``True``, gymnasium flattening will be performed on the observation space bef ----------------- Agents will record their action log for each step. This is a summary of what the agent did, along with response information from requests within the simulation. +A summary of the actions taken by the agent can be viewed using the `show_history()` function. By default, this will display all actions taken apart from ``DONOTHING``. diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst index 542b817b..e6d5da67 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -54,6 +54,39 @@ Optional. Default value is ``3``. The number of time steps required to occur in order for the node to cycle from ``ON`` to ``SHUTTING_DOWN`` and then finally ``OFF``. +``file_system`` +--------------- + +Optional. + +The file system of the node. This configuration allows nodes to be initialised with files and/or folders. + +The file system takes a list of folders and files. + +Example: + +.. code-block:: yaml + + simulation: + network: + nodes: + - hostname: client_1 + type: computer + ip_address: 192.168.10.11 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + file_system: + - empty_folder # example of an empty folder + - downloads: + - "test_1.txt" # files in the downloads folder + - "test_2.txt" + - root: + - passwords: # example of file with size and type + size: 69 # size in bytes + type: TXT # See FileType for list of available file types + +List of file types: :py:mod:`primaite.simulator.file_system.file_type.FileType` + ``users`` --------- diff --git a/docs/source/configuration/simulation/nodes/network_examples.rst b/docs/source/configuration/simulation/nodes/network_examples.rst index 4616139e..84ee4c60 100644 --- a/docs/source/configuration/simulation/nodes/network_examples.rst +++ b/docs/source/configuration/simulation/nodes/network_examples.rst @@ -1177,8 +1177,8 @@ ACLs permitting or denying traffic as per our configured ACL rules. some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv") some_tech_storage_srv.file_system.create_file(file_name="test.png") - pc_1_ftp_client: FTPClient = network.get_node_by_hostname("pc_1").software_manager.software["FTPClient"] - pc_2_ftp_client: FTPClient = network.get_node_by_hostname("pc_2").software_manager.software["FTPClient"] + pc_1_ftp_client: FTPClient = network.get_node_by_hostname("pc_1").software_manager.software["ftp-client"] + pc_2_ftp_client: FTPClient = network.get_node_by_hostname("pc_2").software_manager.software["ftp-client"] assert not pc_1_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -1224,7 +1224,7 @@ ACLs permitting or denying traffic as per our configured ACL rules. web_server: Server = network.get_node_by_hostname("some_tech_web_srv") - web_ftp_client: FTPClient = web_server.software_manager.software["FTPClient"] + web_ftp_client: FTPClient = web_server.software_manager.software["ftp-client"] assert not web_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -1269,7 +1269,7 @@ ACLs permitting or denying traffic as per our configured ACL rules. some_tech_storage_srv.file_system.create_file(file_name="test.png") some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc") - snr_dev_ftp_client: FTPClient = some_tech_snr_dev_pc.software_manager.software["FTPClient"] + snr_dev_ftp_client: FTPClient = some_tech_snr_dev_pc.software_manager.software["ftp-client"] assert snr_dev_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -1294,7 +1294,7 @@ ACLs permitting or denying traffic as per our configured ACL rules. some_tech_storage_srv.file_system.create_file(file_name="test.png") some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc") - jnr_dev_ftp_client: FTPClient = some_tech_jnr_dev_pc.software_manager.software["FTPClient"] + jnr_dev_ftp_client: FTPClient = some_tech_jnr_dev_pc.software_manager.software["ftp-client"] assert not jnr_dev_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, @@ -1337,7 +1337,7 @@ ACLs permitting or denying traffic as per our configured ACL rules. some_tech_storage_srv.file_system.create_file(file_name="test.png") some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1") - hr_ftp_client: FTPClient = some_tech_hr_pc.software_manager.software["FTPClient"] + hr_ftp_client: FTPClient = some_tech_hr_pc.software_manager.software["ftp-client"] assert not hr_ftp_client.request_file( dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address, diff --git a/docs/source/configuration/simulation/nodes/router.rst b/docs/source/configuration/simulation/nodes/router.rst index 4b41784c..ee278a98 100644 --- a/docs/source/configuration/simulation/nodes/router.rst +++ b/docs/source/configuration/simulation/nodes/router.rst @@ -74,7 +74,7 @@ The subnet mask setting for the port. ``acl`` ------- -Sets up the ACL rules for the router. +Sets up the ACL rules for the router to apply to layer-3 traffic. These are not applied to layer-2 traffic such as ARP. e.g. @@ -85,10 +85,6 @@ e.g. ... acl: 1: - action: PERMIT - src_port: ARP - dst_port: ARP - 2: action: PERMIT protocol: ICMP diff --git a/docs/source/how_to_guides/extensible_agents.rst b/docs/source/how_to_guides/extensible_agents.rst index 1d765417..3236c21a 100644 --- a/docs/source/how_to_guides/extensible_agents.rst +++ b/docs/source/how_to_guides/extensible_agents.rst @@ -46,17 +46,13 @@ The core features that should be implemented in any new agent are detailed below - ref: example_green_agent team: GREEN - type: ExampleAgent + type: example-agent action_space: action_map: 0: action: do-nothing options: {} - reward_function: - reward_components: - - type: dummy - agent_settings: start_step: 25 frequency: 20 diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 043d0f06..18d64ca8 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -26,9 +26,9 @@ class Router(NetworkNode, identifier="router"): """ Represents a network router within the simulation, managing routing and forwarding of IP packets across network interfaces.""" SYSTEM_SOFTWARE: ClassVar[Dict] = { - "UserSessionManager": UserSessionManager, - "UserManager": UserManager, - "Terminal": Terminal, + "user-session-manager": UserSessionManager, + "user-manager": UserManager, + "terminal": Terminal, } network_interfaces: Dict[str, RouterInterface] = {} @@ -52,4 +52,4 @@ class Router(NetworkNode, identifier="router"): Changes to YAML file. ===================== -While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. +While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index f0437705..93fc2a9f 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -2,6 +2,8 @@ © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +.. _request_system: + Request System ************** diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index 152b74b8..a6fe4070 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -97,19 +97,19 @@ we'll use the following Network that has a client, server, two switches, and a r network.connect(endpoint_a=switch_2.network_interface[1], endpoint_b=client_1.network_interface[1]) network.connect(endpoint_a=switch_1.network_interface[1], endpoint_b=server_1.network_interface[1]) -8. Add ACL rules on the Router to allow ARP and ICMP traffic. +8. Add an ACL rule on the Router to allow ICMP traffic. .. code-block:: python router_1.acl.add_rule( action=ACLAction.PERMIT, - src_port=Port["ARP"], - dst_port=Port["ARP"], + src_port=PORT_LOOKUP["ARP"], + dst_port=PORT_LOOKUP["ARP"], position=22 ) router_1.acl.add_rule( action=ACLAction.PERMIT, - protocol=IPProtocol["ICMP"], + protocol=PROTOCOL_LOOKUP["ICMP"], position=23 ) diff --git a/docs/source/simulation_components/network/nodes/wireless_router.rst b/docs/source/simulation_components/network/nodes/wireless_router.rst index d7207846..4078ffda 100644 --- a/docs/source/simulation_components/network/nodes/wireless_router.rst +++ b/docs/source/simulation_components/network/nodes/wireless_router.rst @@ -102,8 +102,8 @@ ICMP traffic, ensuring basic network connectivity and ping functionality. network.connect(pc_a.network_interface[1], router_1.router_interface) # Configure Router 1 ACLs - router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port["ARP"], dst_port=Port["ARP"], position=22) - router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol["ICMP"], position=23) + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=PROTOCOL_LOOKUP["ICMP"], position=23) # Configure PC B pc_b = Computer( diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 34175fc3..c780485a 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -183,7 +183,7 @@ Python # Example command: Installing and configuring Ransomware: ransomware_installation_command = { "commands": [ - ["software_manager","application","install","RansomwareScript"], + ["software_manager","application","install","ransomware-script"], ], "username": "admin", "password": "admin", diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 8e008504..3ddb8bca 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -77,7 +77,7 @@ Python network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) client_1.software_manager.install(DatabaseClient) client_1.software_manager.install(DataManipulationBot) - data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") + data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("data-manipulation-bot") data_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE") data_manipulation_bot.run() @@ -98,7 +98,7 @@ If not using the data manipulation bot manually, it needs to be used with a data type: red-database-corrupting-agent observation_space: - type: UC2RedObservation + type: uc2-red-observation #TODO what options: nodes: - node_name: client_1 diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 7087dedf..472b504c 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -59,7 +59,7 @@ Python # install DatabaseClient client.software_manager.install(DatabaseClient) - database_client: DatabaseClient = client.software_manager.software.get("DatabaseClient") + database_client: DatabaseClient = client.software_manager.software.get("database-sclient") # Configure the DatabaseClient database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) # address of the DatabaseService diff --git a/docs/source/simulation_components/system/applications/ransomware_script.rst b/docs/source/simulation_components/system/applications/ransomware_script.rst index 192618fc..a8975f32 100644 --- a/docs/source/simulation_components/system/applications/ransomware_script.rst +++ b/docs/source/simulation_components/system/applications/ransomware_script.rst @@ -62,7 +62,7 @@ Python network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) client_1.software_manager.install(DatabaseClient) client_1.software_manager.install(RansomwareScript) - RansomwareScript: RansomwareScript = client_1.software_manager.software.get("RansomwareScript") + RansomwareScript: RansomwareScript = client_1.software_manager.software.get("ransomware-script") RansomwareScript.configure(server_ip_address=IPv4Address("192.168.1.14")) RansomwareScript.execute() diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index c04e60af..659caa09 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -61,7 +61,7 @@ The :ref:`DNSClient` must be configured to use the :ref:`DNSServer`. The :ref:`D # Install WebBrowser on computer computer.software_manager.install(WebBrowser) - web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser: WebBrowser = computer.software_manager.software.get("web-browser") web_browser.run() # configure the WebBrowser diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index 961f2e45..c819a0f7 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -66,7 +66,7 @@ Python # Install DatabaseService on server server.software_manager.install(DatabaseService) - db_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_service: DatabaseService = server.software_manager.software.get("database-service") db_service.start() # configure DatabaseService diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index 17a1ed25..40762bfc 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -56,7 +56,7 @@ Python # Install DNSClient on server server.software_manager.install(DNSClient) - dns_client: DNSClient = server.software_manager.software.get("DNSClient") + dns_client: DNSClient = server.software_manager.software.get("dns-client") dns_client.start() # configure DatabaseService diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index 633221d5..ca0e3691 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -53,7 +53,7 @@ Python # Install DNSServer on server server.software_manager.install(DNSServer) - dns_server: DNSServer = server.software_manager.software.get("DNSServer") + dns_server: DNSServer = server.software_manager.software.get("dns-server") dns_server.start() # configure DatabaseService diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index d4375069..530b5aff 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -60,7 +60,7 @@ Python # Install FTPClient on server server.software_manager.install(FTPClient) - ftp_client: FTPClient = server.software_manager.software.get("FTPClient") + ftp_client: FTPClient = server.software_manager.software.get("ftp-client") ftp_client.start() diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index a5ad32fe..20dd6707 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -55,7 +55,7 @@ Python # Install FTPServer on server server.software_manager.install(FTPServer) - ftp_server: FTPServer = server.software_manager.software.get("FTPServer") + ftp_server: FTPServer = server.software_manager.software.get("ftp-server") ftp_server.start() ftp_server.server_password = "test" diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index 8c011cad..5406d9fc 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -53,7 +53,7 @@ Python # Install NTPClient on server server.software_manager.install(NTPClient) - ntp_client: NTPClient = server.software_manager.software.get("NTPClient") + ntp_client: NTPClient = server.software_manager.software.get("ntp-client") ntp_client.start() ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.0.10")) diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index c1d16d61..2c01dcaf 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -55,7 +55,7 @@ Python # Install NTPServer on server server.software_manager.install(NTPServer) - ntp_server: NTPServer = server.software_manager.software.get("NTPServer") + ntp_server: NTPServer = server.software_manager.software.get("ntp-server") ntp_server.start() diff --git a/docs/source/simulation_components/system/services/terminal.rst b/docs/source/simulation_components/system/services/terminal.rst index bc5cee48..5c9bad79 100644 --- a/docs/source/simulation_components/system/services/terminal.rst +++ b/docs/source/simulation_components/system/services/terminal.rst @@ -23,6 +23,14 @@ Key capabilities - Simulates common Terminal processes/commands. - Leverages the Service base class for install/uninstall, status tracking etc. +Usage +""""" + + - Pre-Installs on any `Node` component (with the exception of `Switches`). + - Terminal Clients connect, execute commands and disconnect from remote nodes. + - Ensures that users are logged in to the component before executing any commands. + - Service runs on SSH port 22 by default. + - Enables Agents to send commands both remotely and locally. Implementation """""""""""""" @@ -30,19 +38,112 @@ Implementation - Manages remote connections in a dictionary by session ID. - Processes commands, forwarding to the ``RequestManager`` or ``SessionManager`` where appropriate. - Extends Service class. - - A detailed guide on the implementation and functionality of the Terminal class can be found in the "Terminal-Processing" jupyter notebook. + +A detailed guide on the implementation and functionality of the Terminal class can be found in the "Terminal-Processing" jupyter notebook. + +Command Format +^^^^^^^^^^^^^^ + +Terminals implement their commands through leveraging the pre-existing :ref:`request_system`. + +Due to this Terminals will only accept commands passed within the ``RequestFormat``. + +:py:class:`primaite.game.interface.RequestFormat` + +For example, ``terminal`` command actions when used in ``yaml`` format are formatted as follows: + +.. code-block:: yaml + + command: + - "file_system" + - "create" + - "file" + - "downloads" + - "cat.png" + - "False + +This is then loaded from yaml into a dictionary containing the terminal command: + +.. code-block:: python + + {"command":["file_system", "create", "file", "downloads", "cat.png", "False"]} + +Which is then passed to the ``Terminals`` Request Manager to be executed. + +Game Layer Usage (Agents) +======================== + +The below code examples demonstrate how to use terminal related actions in yaml files. + +yaml +"""" + +``node-send-local-command`` +""""""""""""""""""""""""""" + +Agents can execute local commands without needing to perform a separate remote login action (``node-session-remote-login``). + +.. code-block:: yaml + + ... + ... + action: node-send-local-command + options: + node_id: 0 + username: admin + password: admin + command: # Example command - Creates a file called 'cat.png' in the downloads folder. + - "file_system" + - "create" + - "file" + - "downloads" + - "cat.png" + - "False" -Usage -""""" +``node-session-remote-login`` +""""""""""""""""" - - Pre-Installs on all ``Nodes`` (with the exception of ``Switches``). - - Terminal Clients connect, execute commands and disconnect from remote nodes. - - Ensures that users are logged in to the component before executing any commands. - - Service runs on SSH port 22 by default. +Agents are able to use the terminal to login into remote nodes via ``SSH`` which allows for agents to execute commands on remote hosts. + +.. code-block:: yaml + + ... + ... + action: node-session-remote-login + options: + node_id: 0 + username: admin + password: admin + remote_ip: 192.168.0.10 # Example Ip Address. (The remote host's IP that will be used by ssh) + + +``node-send-remote-command`` +"""""""""""""""""""""""""""" + +After remotely logging into another host, an agent can use the ``node-send-remote-command`` to execute commands across the network remotely. + +.. code-block:: yaml + + ... + ... + action: node-send-remote-command + options: + node_id: 0 + remote_ip: 192.168.0.10 + command: + - "file_system" + - "create" + - "file" + - "downloads" + - "cat.png" + - "False" + + + +Simulation Layer Usage +====================== -Usage -===== The below code examples demonstrate how to create a terminal, a remote terminal, and how to send a basic application install command to a remote node. @@ -65,7 +166,7 @@ Python operating_state=NodeOperatingState.ON, ) - terminal: Terminal = client.software_manager.software.get("Terminal") + terminal: Terminal = client.software_manager.software.get("terminal") Creating Remote Terminal Connection """"""""""""""""""""""""""""""""""" @@ -86,7 +187,7 @@ Creating Remote Terminal Connection node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) - terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + terminal_a: Terminal = node_a.software_manager.software.get("terminal") term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") @@ -112,12 +213,12 @@ Executing a basic application install command node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) - terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + terminal_a: Terminal = node_a.software_manager.software.get("terminal") term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") - term_a_term_b_remote_connection.execute(["software_manager", "application", "install", "RansomwareScript"]) + term_a_term_b_remote_connection.execute(["software_manager", "application", "install", "ransomware-script"]) @@ -140,7 +241,7 @@ Creating a folder on a remote node node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) - terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + terminal_a: Terminal = node_a.software_manager.software.get("terminal") term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") @@ -167,7 +268,7 @@ Disconnect from Remote Node node_b.power_on() network.connect(node_a.network_interface[1], node_b.network_interface[1]) - terminal_a: Terminal = node_a.software_manager.software.get("Terminal") + terminal_a: Terminal = node_a.software_manager.software.get("terminal") term_a_term_b_remote_connection: RemoteTerminalConnection = terminal_a.login(username="admin", password="Admin123!", ip_address="192.168.0.11") diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index bce42791..9d7f4d2f 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -56,7 +56,7 @@ Python # Install WebServer on server server.software_manager.install(WebServer) - web_server: WebServer = server.software_manager.software.get("WebServer") + web_server: WebServer = server.software_manager.software.get("web-server") web_server.start() Via Configuration diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index d28815bb..c2f3066b 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -30,7 +30,7 @@ See :ref:`Node Start up and Shut down` node.software_manager.install(WebServer) - web_server: WebServer = node.software_manager.software.get("WebServer") + web_server: WebServer = node.software_manager.software.get("web-server") assert web_server.operating_state is ServiceOperatingState.RUNNING # service is immediately ran after install node.power_off() diff --git a/src/primaite/game/agent/actions/node.py b/src/primaite/game/agent/actions/node.py index 19639c21..b1b6ec12 100644 --- a/src/primaite/game/agent/actions/node.py +++ b/src/primaite/game/agent/actions/node.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from abc import ABC, abstractmethod -from typing import ClassVar, List, Optional, Union +from typing import ClassVar, List, Literal, Optional, Union from primaite.game.agent.actions.manager import AbstractAction from primaite.interface.request import RequestFormat @@ -153,8 +153,6 @@ class NodeNMAPPortScanAction(NodeNMAPAbstractAction, discriminator="node-nmap-po class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, discriminator="node-network-service-recon"): """Action which performs an nmap network service recon (ping scan followed by port scan).""" - config: "NodeNetworkServiceReconAction.ConfigSchema" - class ConfigSchema(NodeNMAPAbstractAction.ConfigSchema): """Configuration schema for NodeNetworkServiceReconAction.""" @@ -179,3 +177,70 @@ class NodeNetworkServiceReconAction(NodeNMAPAbstractAction, discriminator="node- "show": config.show, }, ] + + +class NodeAccountsAddUserAction(AbstractAction, discriminator="node-account-add-user"): + class ConfigSchema(AbstractAction.ConfigSchema): + type: Literal["node-account-add-user"] = "node-account-add-user" + node_name: str + username: str + password: str + is_admin: bool + + @classmethod + @staticmethod + def form_request(config: ConfigSchema) -> RequestFormat: + return [ + "network", + "node", + config.node_name, + "service", + "user-manager", + "add_user", + config.username, + config.password, + config.is_admin, + ] + + +class NodeAccountsDisableUserAction(AbstractAction, discriminator="node-account-disable-user"): + class ConfigSchema(AbstractAction.ConfigSchema): + type: Literal["node-account-disable-user"] = "node-account-disable-user" + node_name: str + username: str + + @classmethod + @staticmethod + def form_request(config: ConfigSchema) -> RequestFormat: + return [ + "network", + "node", + config.node_name, + "service", + "user-manager", + "disable_user", + config.username, + ] + + +class NodeSendLocalCommandAction(AbstractAction, discriminator="node-send-local-command"): + class ConfigSchema(AbstractAction.ConfigSchema): + type: Literal["node-send-local-command"] = "node-send-local-command" + node_name: str + username: str + password: str + command: RequestFormat + + @staticmethod + def form_request(config: ConfigSchema) -> RequestFormat: + return [ + "network", + "node", + config.node_name, + "service", + "terminal", + "send_local_command", + config.username, + config.password, + {"command": config.command}, + ] diff --git a/src/primaite/game/agent/actions/session.py b/src/primaite/game/agent/actions/session.py index 58a8a555..63a45c5e 100644 --- a/src/primaite/game/agent/actions/session.py +++ b/src/primaite/game/agent/actions/session.py @@ -34,8 +34,6 @@ class NodeSessionAbstractAction(AbstractAction, ABC): class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="node-session-remote-login"): """Action which performs a remote session login.""" - config: "NodeSessionsRemoteLoginAction.ConfigSchema" - class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLoginAction.""" @@ -53,7 +51,7 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="no config.node_name, "service", "terminal", - "node-session-remote-login", + "node_session_remote_login", config.username, config.password, config.remote_ip, @@ -63,8 +61,6 @@ class NodeSessionsRemoteLoginAction(NodeSessionAbstractAction, discriminator="no class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="node-session-remote-logoff"): """Action which performs a remote session logout.""" - config: "NodeSessionsRemoteLogoutAction.ConfigSchema" - class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): """Configuration schema for NodeSessionsRemoteLogoutAction.""" @@ -78,14 +74,13 @@ class NodeSessionsRemoteLogoutAction(NodeSessionAbstractAction, discriminator="n return ["network", "node", config.node_name, "service", "terminal", config.verb, config.remote_ip] -class NodeAccountChangePasswordAction(NodeSessionAbstractAction, discriminator="node-account-change-password"): +class NodeAccountChangePasswordAction(AbstractAction, discriminator="node-account-change-password"): """Action which changes the password for a user.""" - config: "NodeAccountChangePasswordAction.ConfigSchema" - - class ConfigSchema(NodeSessionAbstractAction.ConfigSchema): + class ConfigSchema(AbstractAction.ConfigSchema): """Configuration schema for NodeAccountsChangePasswordAction.""" + node_name: str username: str current_password: str new_password: str diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index a55cd3ff..d06bd1d0 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -6,6 +6,7 @@ from abc import ABC, abstractmethod from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type, TYPE_CHECKING from gymnasium.core import ActType, ObsType +from prettytable import PrettyTable from pydantic import BaseModel, ConfigDict, Field from primaite.game.agent.actions import ActionManager @@ -42,6 +43,9 @@ class AgentHistoryItem(BaseModel): reward_info: Dict[str, Any] = {} + observation: Optional[ObsType] = None + """The observation space data for this step.""" + class AbstractAgent(BaseModel, ABC): """Base class for scripted and RL agents.""" @@ -67,6 +71,9 @@ class AbstractAgent(BaseModel, ABC): default_factory=lambda: ObservationManager.ConfigSchema() ) reward_function: RewardFunction.ConfigSchema = Field(default_factory=lambda: RewardFunction.ConfigSchema()) + thresholds: Optional[Dict] = {} + # TODO: this is only relevant to some observations, need to refactor the way thresholds are dealt with (#3085) + """A dict containing the observation thresholds.""" config: ConfigSchema = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) @@ -90,10 +97,42 @@ class AbstractAgent(BaseModel, ABC): def model_post_init(self, __context: Any) -> None: """Overwrite the default empty action, observation, and rewards with ones defined through the config.""" self.action_manager = ActionManager(config=self.config.action_space) + self.config.observation_space.options.thresholds = self.config.thresholds self.observation_manager = ObservationManager(config=self.config.observation_space) self.reward_function = RewardFunction(config=self.config.reward_function) return super().model_post_init(__context) + def add_agent_action(self, item: AgentHistoryItem, table: PrettyTable) -> PrettyTable: + """Update the given table with information from given AgentHistoryItem.""" + node, application = "unknown", "unknown" + if (node_id := item.parameters.get("node_id")) is not None: + node = self.action_manager.node_names[node_id] + if (application_id := item.parameters.get("application_id")) is not None: + application = self.action_manager.application_names[node_id][application_id] + if (application_name := item.parameters.get("application_name")) is not None: + application = application_name + table.add_row([item.timestep, item.action, node, application, item.response.status]) + return table + + def show_history(self, ignored_actions: Optional[list] = None): + """ + Print an agent action provided it's not the DONOTHING action. + + :param ignored_actions: OPTIONAL: List of actions to be ignored when displaying the history. + If not provided, defaults to ignore DONOTHING actions. + """ + if not ignored_actions: + ignored_actions = ["DONOTHING"] + table = PrettyTable() + table.field_names = ["Step", "Action", "Node", "Application", "Response"] + print(f"Actions for '{self.agent_name}':") + for item in self.history: + if item.action in ignored_actions: + pass + else: + table = self.add_agent_action(item=item, table=table) + print(table) + def update_observation(self, state: Dict) -> ObsType: """ Convert a state from the simulator into an observation for the agent using the observation space. @@ -140,12 +179,23 @@ class AbstractAgent(BaseModel, ABC): return request def process_action_response( - self, timestep: int, action: str, parameters: Dict[str, Any], request: RequestFormat, response: RequestResponse + self, + timestep: int, + action: str, + parameters: Dict[str, Any], + request: RequestFormat, + response: RequestResponse, + observation: ObsType, ) -> None: """Process the response from the most recent action.""" self.history.append( AgentHistoryItem( - timestep=timestep, action=action, parameters=parameters, request=request, response=response + timestep=timestep, + action=action, + parameters=parameters, + request=request, + response=response, + observation=observation, ) ) diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py index ed9dcd8f..a9e3a9aa 100644 --- a/src/primaite/game/agent/observations/file_system_observations.py +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -26,7 +26,13 @@ class FileObservation(AbstractObservation, discriminator="file"): file_system_requires_scan: Optional[bool] = None """If True, the file must be scanned to update the health state. Tf False, the true state is always shown.""" - def __init__(self, where: WhereType, include_num_access: bool, file_system_requires_scan: bool) -> None: + def __init__( + self, + where: WhereType, + include_num_access: bool, + file_system_requires_scan: bool, + thresholds: Optional[Dict] = {}, + ) -> None: """ Initialise a file observation instance. @@ -48,10 +54,36 @@ class FileObservation(AbstractObservation, discriminator="file"): if self.include_num_access: self.default_observation["num_access"] = 0 - # TODO: allow these to be configured in yaml - self.high_threshold = 10 - self.med_threshold = 5 - self.low_threshold = 0 + if thresholds.get("file_access") is None: + self.low_file_access_threshold = 0 + self.med_file_access_threshold = 5 + self.high_file_access_threshold = 10 + else: + self._set_file_access_threshold( + thresholds=[ + thresholds.get("file_access")["low"], + thresholds.get("file_access")["medium"], + thresholds.get("file_access")["high"], + ] + ) + + def _set_file_access_threshold(self, thresholds: List[int]): + """ + Method that validates and then sets the file access threshold. + + :param: thresholds: The file access threshold to validate and set. + """ + if self._validate_thresholds( + thresholds=[ + thresholds[0], + thresholds[1], + thresholds[2], + ], + threshold_identifier="file_access", + ): + self.low_file_access_threshold = thresholds[0] + self.med_file_access_threshold = thresholds[1] + self.high_file_access_threshold = thresholds[2] def _categorise_num_access(self, num_access: int) -> int: """ @@ -60,11 +92,11 @@ class FileObservation(AbstractObservation, discriminator="file"): :param num_access: Number of file accesses. :return: Bin number corresponding to the number of accesses. """ - if num_access > self.high_threshold: + if num_access > self.high_file_access_threshold: return 3 - elif num_access > self.med_threshold: + elif num_access > self.med_file_access_threshold: return 2 - elif num_access > self.low_threshold: + elif num_access > self.low_file_access_threshold: return 1 return 0 @@ -122,6 +154,7 @@ class FileObservation(AbstractObservation, discriminator="file"): where=parent_where + ["files", config.file_name], include_num_access=config.include_num_access, file_system_requires_scan=config.file_system_requires_scan, + thresholds=config.thresholds, ) @@ -149,6 +182,7 @@ class FolderObservation(AbstractObservation, discriminator="folder"): num_files: int, include_num_access: bool, file_system_requires_scan: bool, + thresholds: Optional[Dict] = {}, ) -> None: """ Initialise a folder observation instance. @@ -177,6 +211,7 @@ class FolderObservation(AbstractObservation, discriminator="folder"): where=None, include_num_access=include_num_access, file_system_requires_scan=self.file_system_requires_scan, + thresholds=thresholds, ) ) while len(self.files) > num_files: @@ -253,6 +288,7 @@ class FolderObservation(AbstractObservation, discriminator="folder"): for file_config in config.files: file_config.include_num_access = config.include_num_access file_config.file_system_requires_scan = config.file_system_requires_scan + file_config.thresholds = config.thresholds files = [FileObservation.from_config(config=f, parent_where=where) for f in config.files] return cls( @@ -261,4 +297,5 @@ class FolderObservation(AbstractObservation, discriminator="folder"): num_files=config.num_files, include_num_access=config.include_num_access, file_system_requires_scan=config.file_system_requires_scan, + thresholds=config.thresholds, ) diff --git a/src/primaite/game/agent/observations/host_observations.py b/src/primaite/game/agent/observations/host_observations.py index 17bcb983..9b979063 100644 --- a/src/primaite/game/agent/observations/host_observations.py +++ b/src/primaite/game/agent/observations/host_observations.py @@ -54,7 +54,15 @@ class HostObservation(AbstractObservation, discriminator="host"): """ If True, files and folders must be scanned to update the health state. If False, true state is always shown. """ - include_users: Optional[bool] = None + services_requires_scan: Optional[bool] = None + """ + If True, services must be scanned to update the health state. If False, true state is always shown. + """ + applications_requires_scan: Optional[bool] = None + """ + If True, applications must be scanned to update the health state. If False, true state is always shown. + """ + include_users: Optional[bool] = True """If True, report user session information.""" def __init__( @@ -73,6 +81,8 @@ class HostObservation(AbstractObservation, discriminator="host"): monitored_traffic: Optional[Dict], include_num_access: bool, file_system_requires_scan: bool, + services_requires_scan: bool, + applications_requires_scan: bool, include_users: bool, ) -> None: """ @@ -108,6 +118,12 @@ class HostObservation(AbstractObservation, discriminator="host"): :param file_system_requires_scan: If True, the files and folders must be scanned to update the health state. If False, the true state is always shown. :type file_system_requires_scan: bool + :param services_requires_scan: If True, services must be scanned to update the health state. + If False, the true state is always shown. + :type services_requires_scan: bool + :param applications_requires_scan: If True, applications must be scanned to update the health state. + If False, the true state is always shown. + :type applications_requires_scan: bool :param include_users: If True, report user session information. :type include_users: bool """ @@ -121,7 +137,7 @@ class HostObservation(AbstractObservation, discriminator="host"): # Ensure lists have lengths equal to specified counts by truncating or padding self.services: List[ServiceObservation] = services while len(self.services) < num_services: - self.services.append(ServiceObservation(where=None)) + self.services.append(ServiceObservation(where=None, services_requires_scan=services_requires_scan)) while len(self.services) > num_services: truncated_service = self.services.pop() msg = f"Too many services in Node observation space for node. Truncating service {truncated_service.where}" @@ -129,7 +145,9 @@ class HostObservation(AbstractObservation, discriminator="host"): self.applications: List[ApplicationObservation] = applications while len(self.applications) < num_applications: - self.applications.append(ApplicationObservation(where=None)) + self.applications.append( + ApplicationObservation(where=None, applications_requires_scan=applications_requires_scan) + ) while len(self.applications) > num_applications: truncated_application = self.applications.pop() msg = f"Too many applications in Node observation space for node. Truncating {truncated_application.where}" @@ -153,7 +171,13 @@ class HostObservation(AbstractObservation, discriminator="host"): self.nics: List[NICObservation] = network_interfaces while len(self.nics) < num_nics: - self.nics.append(NICObservation(where=None, include_nmne=include_nmne, monitored_traffic=monitored_traffic)) + self.nics.append( + NICObservation( + where=None, + include_nmne=include_nmne, + monitored_traffic=monitored_traffic, + ) + ) while len(self.nics) > num_nics: truncated_nic = self.nics.pop() msg = f"Too many network_interfaces in Node observation space for node. Truncating {truncated_nic.where}" @@ -269,8 +293,15 @@ class HostObservation(AbstractObservation, discriminator="host"): folder_config.include_num_access = config.include_num_access folder_config.num_files = config.num_files folder_config.file_system_requires_scan = config.file_system_requires_scan + folder_config.thresholds = config.thresholds for nic_config in config.network_interfaces: nic_config.include_nmne = config.include_nmne + nic_config.thresholds = config.thresholds + for service_config in config.services: + service_config.services_requires_scan = config.services_requires_scan + for application_config in config.applications: + application_config.applications_requires_scan = config.applications_requires_scan + application_config.thresholds = config.thresholds services = [ServiceObservation.from_config(config=c, parent_where=where) for c in config.services] applications = [ApplicationObservation.from_config(config=c, parent_where=where) for c in config.applications] @@ -281,7 +312,10 @@ class HostObservation(AbstractObservation, discriminator="host"): count = 1 while len(nics) < config.num_nics: nic_config = NICObservation.ConfigSchema( - nic_num=count, include_nmne=config.include_nmne, monitored_traffic=config.monitored_traffic + nic_num=count, + include_nmne=config.include_nmne, + monitored_traffic=config.monitored_traffic, + thresholds=config.thresholds, ) nics.append(NICObservation.from_config(config=nic_config, parent_where=where)) count += 1 @@ -301,5 +335,7 @@ class HostObservation(AbstractObservation, discriminator="host"): monitored_traffic=config.monitored_traffic, include_num_access=config.include_num_access, file_system_requires_scan=config.file_system_requires_scan, + services_requires_scan=config.services_requires_scan, + applications_requires_scan=config.applications_requires_scan, include_users=config.include_users, ) diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 1aa6470d..8faeb906 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -1,13 +1,14 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from typing import Dict, List, Optional +from typing import ClassVar, Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType from primaite.game.agent.observations.observations import AbstractObservation, WhereType from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.simulator.network.nmne import NMNEConfig from primaite.utils.validation.ip_protocol import IPProtocol from primaite.utils.validation.port import Port @@ -15,6 +16,9 @@ from primaite.utils.validation.port import Port class NICObservation(AbstractObservation, discriminator="network-interface"): """Status information about a network interface within the simulation environment.""" + capture_nmne: ClassVar[bool] = NMNEConfig().capture_nmne + "A Boolean specifying whether malicious network events should be captured." + class ConfigSchema(AbstractObservation.ConfigSchema): """Configuration schema for NICObservation.""" @@ -25,7 +29,13 @@ class NICObservation(AbstractObservation, discriminator="network-interface"): monitored_traffic: Optional[Dict[IPProtocol, List[Port]]] = None """A dict containing which traffic types are to be included in the observation.""" - def __init__(self, where: WhereType, include_nmne: bool, monitored_traffic: Optional[Dict] = None) -> None: + def __init__( + self, + where: WhereType, + include_nmne: bool, + monitored_traffic: Optional[Dict] = None, + thresholds: Dict = {}, + ) -> None: """ Initialise a network interface observation instance. @@ -45,10 +55,18 @@ class NICObservation(AbstractObservation, discriminator="network-interface"): self.nmne_inbound_last_step: int = 0 self.nmne_outbound_last_step: int = 0 - # TODO: allow these to be configured in yaml - self.high_nmne_threshold = 10 - self.med_nmne_threshold = 5 - self.low_nmne_threshold = 0 + if thresholds.get("nmne") is None: + self.low_nmne_threshold = 0 + self.med_nmne_threshold = 5 + self.high_nmne_threshold = 10 + else: + self._set_nmne_threshold( + thresholds=[ + thresholds.get("nmne")["low"], + thresholds.get("nmne")["medium"], + thresholds.get("nmne")["high"], + ] + ) self.monitored_traffic = monitored_traffic if self.monitored_traffic: @@ -105,6 +123,20 @@ class NICObservation(AbstractObservation, discriminator="network-interface"): bandwidth_utilisation = traffic_value / nic_max_bandwidth return int(bandwidth_utilisation * 9) + 1 + def _set_nmne_threshold(self, thresholds: List[int]): + """ + Method that validates and then sets the NMNE threshold. + + :param: thresholds: The NMNE threshold to validate and set. + """ + if self._validate_thresholds( + thresholds=thresholds, + threshold_identifier="nmne", + ): + self.low_nmne_threshold = thresholds[0] + self.med_nmne_threshold = thresholds[1] + self.high_nmne_threshold = thresholds[2] + def observe(self, state: Dict) -> ObsType: """ Generate observation based on the current state of the simulation. @@ -116,7 +148,7 @@ class NICObservation(AbstractObservation, discriminator="network-interface"): """ nic_state = access_from_nested_dict(state, self.where) - if nic_state is NOT_PRESENT_IN_STATE: + if nic_state is NOT_PRESENT_IN_STATE or self.where is None: return self.default_observation obs = {"nic_status": 1 if nic_state["enabled"] else 2} @@ -164,7 +196,7 @@ class NICObservation(AbstractObservation, discriminator="network-interface"): for port in self.monitored_traffic[protocol]: obs["TRAFFIC"][protocol][port] = {"inbound": 0, "outbound": 0} - if self.include_nmne: + if self.capture_nmne and self.include_nmne: obs.update({"NMNE": {}}) direction_dict = nic_state["nmne"].get("direction", {}) inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) @@ -224,6 +256,7 @@ class NICObservation(AbstractObservation, discriminator="network-interface"): where=parent_where + ["NICs", config.nic_num], include_nmne=config.include_nmne, monitored_traffic=config.monitored_traffic, + thresholds=config.thresholds, ) diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 3a3283a2..260fac68 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -48,7 +48,13 @@ class NodesObservation(AbstractObservation, discriminator="nodes"): include_num_access: Optional[bool] = None """Flag to include the number of accesses.""" file_system_requires_scan: bool = True - """If True, the folder must be scanned to update the health state. Tf False, the true state is always shown.""" + """If True, the folder must be scanned to update the health state. If False, the true state is always shown.""" + services_requires_scan: bool = True + """If True, the services must be scanned to update the health state. + If False, the true state is always shown.""" + applications_requires_scan: bool = True + """If True, the applications must be scanned to update the health state. + If False, the true state is always shown.""" include_users: Optional[bool] = True """If True, report user session information.""" num_ports: Optional[int] = None @@ -196,8 +202,14 @@ class NodesObservation(AbstractObservation, discriminator="nodes"): host_config.include_num_access = config.include_num_access if host_config.file_system_requires_scan is None: host_config.file_system_requires_scan = config.file_system_requires_scan + if host_config.services_requires_scan is None: + host_config.services_requires_scan = config.services_requires_scan + if host_config.applications_requires_scan is None: + host_config.applications_requires_scan = config.applications_requires_scan if host_config.include_users is None: host_config.include_users = config.include_users + if not host_config.thresholds: + host_config.thresholds = config.thresholds for router_config in config.routers: if router_config.num_ports is None: @@ -214,6 +226,8 @@ class NodesObservation(AbstractObservation, discriminator="nodes"): router_config.num_rules = config.num_rules if router_config.include_users is None: router_config.include_users = config.include_users + if not router_config.thresholds: + router_config.thresholds = config.thresholds for firewall_config in config.firewalls: if firewall_config.ip_list is None: @@ -228,6 +242,8 @@ class NodesObservation(AbstractObservation, discriminator="nodes"): firewall_config.num_rules = config.num_rules if firewall_config.include_users is None: firewall_config.include_users = config.include_users + if not firewall_config.thresholds: + firewall_config.thresholds = config.thresholds hosts = [HostObservation.from_config(config=c, parent_where=where) for c in config.hosts] routers = [RouterObservation.from_config(config=c, parent_where=where) for c in config.routers] diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py index 032435b8..e8cb18aa 100644 --- a/src/primaite/game/agent/observations/observation_manager.py +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -114,7 +114,9 @@ class NestedObservation(AbstractObservation, discriminator="custom"): instances = dict() for component in config.components: obs_class = AbstractObservation._registry[component.type] - obs_instance = obs_class.from_config(config=obs_class.ConfigSchema(**component.options)) + obs_instance = obs_class.from_config( + config=obs_class.ConfigSchema(**component.options, thresholds=config.thresholds) + ) instances[component.label] = obs_instance return cls(components=instances) @@ -242,8 +244,5 @@ class ObservationManager(BaseModel): """ if config is None: return cls(NullObservation()) - obs_type = config["type"] - obs_class = AbstractObservation._registry[obs_type] - observation = obs_class.from_config(config=obs_class.ConfigSchema(**config["options"])) - obs_manager = cls(observation) + obs_manager = cls(config=config) return obs_manager diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index da81d2ad..8558b75c 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK """Manages the observation space for the agent.""" from abc import ABC, abstractmethod -from typing import Any, Dict, Iterable, Optional, Type, Union +from typing import Any, Dict, Iterable, List, Optional, Type, Union from gymnasium import spaces from gymnasium.core import ObsType @@ -19,6 +19,9 @@ class AbstractObservation(ABC): class ConfigSchema(ABC, BaseModel): """Config schema for observations.""" + thresholds: Optional[Dict] = {} + """A dict containing the observation thresholds.""" + model_config = ConfigDict(extra="forbid") _registry: Dict[str, Type["AbstractObservation"]] = {} @@ -69,3 +72,34 @@ class AbstractObservation(ABC): def from_config(cls, config: ConfigSchema, parent_where: WhereType = []) -> "AbstractObservation": """Create this observation space component form a serialised format.""" return cls() + + def _validate_thresholds(self, thresholds: List[int] = None, threshold_identifier: Optional[str] = "") -> bool: + """ + Method that checks if the thresholds are non overlapping and in the correct (ascending) order. + + Pass in the thresholds from low to high e.g. + thresholds=[low_threshold, med_threshold, ..._threshold, high_threshold] + + Throws an error if the threshold is not valid + + :param: thresholds: List of thresholds in ascending order. + :type: List[int] + :param: threshold_identifier: The name of the threshold option. + :type: Optional[str] + + :returns: bool + """ + if thresholds is None or len(thresholds) < 2: + raise Exception(f"{threshold_identifier} thresholds are invalid {thresholds}") + for idx in range(1, len(thresholds)): + if not isinstance(thresholds[idx], int): + raise Exception(f"{threshold_identifier} threshold ({thresholds[idx]}) is not a valid int.") + if not isinstance(thresholds[idx - 1], int): + raise Exception(f"{threshold_identifier} threshold ({thresholds[idx]}) is not a valid int.") + + if thresholds[idx] <= thresholds[idx - 1]: + raise Exception( + f"{threshold_identifier} threshold ({thresholds[idx - 1]}) " + f"is greater than or equal to ({thresholds[idx]}.)" + ) + return True diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index 07ec1abf..dac6b362 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -1,7 +1,7 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK from __future__ import annotations -from typing import Dict +from typing import Dict, List, Optional from gymnasium import spaces from gymnasium.core import ObsType @@ -19,7 +19,10 @@ class ServiceObservation(AbstractObservation, discriminator="service"): service_name: str """Name of the service, used for querying simulation state dictionary""" - def __init__(self, where: WhereType) -> None: + services_requires_scan: Optional[bool] = None + """If True, services must be scanned to update the health state. If False, true state is always shown.""" + + def __init__(self, where: WhereType, services_requires_scan: bool) -> None: """ Initialise a service observation instance. @@ -28,6 +31,7 @@ class ServiceObservation(AbstractObservation, discriminator="service"): :type where: WhereType """ self.where = where + self.services_requires_scan = services_requires_scan self.default_observation = {"operating_status": 0, "health_status": 0} def observe(self, state: Dict) -> ObsType: @@ -44,7 +48,9 @@ class ServiceObservation(AbstractObservation, discriminator="service"): return self.default_observation return { "operating_status": service_state["operating_state"], - "health_status": service_state["health_state_visible"], + "health_status": service_state["health_state_visible"] + if self.services_requires_scan + else service_state["health_state_actual"], } @property @@ -70,7 +76,9 @@ class ServiceObservation(AbstractObservation, discriminator="service"): :return: Constructed service observation instance. :rtype: ServiceObservation """ - return cls(where=parent_where + ["services", config.service_name]) + return cls( + where=parent_where + ["services", config.service_name], services_requires_scan=config.services_requires_scan + ) class ApplicationObservation(AbstractObservation, discriminator="application"): @@ -82,7 +90,12 @@ class ApplicationObservation(AbstractObservation, discriminator="application"): application_name: str """Name of the application, used for querying simulation state dictionary""" - def __init__(self, where: WhereType) -> None: + applications_requires_scan: Optional[bool] = None + """ + If True, applications must be scanned to update the health state. If False, true state is always shown. + """ + + def __init__(self, where: WhereType, applications_requires_scan: bool, thresholds: Optional[Dict] = {}) -> None: """ Initialise an application observation instance. @@ -92,25 +105,52 @@ class ApplicationObservation(AbstractObservation, discriminator="application"): :type where: WhereType """ self.where = where + self.applications_requires_scan = applications_requires_scan self.default_observation = {"operating_status": 0, "health_status": 0, "num_executions": 0} - # TODO: allow these to be configured in yaml - self.high_threshold = 10 - self.med_threshold = 5 - self.low_threshold = 0 + if thresholds.get("app_executions") is None: + self.low_app_execution_threshold = 0 + self.med_app_execution_threshold = 5 + self.high_app_execution_threshold = 10 + else: + self._set_application_execution_thresholds( + thresholds=[ + thresholds.get("app_executions")["low"], + thresholds.get("app_executions")["medium"], + thresholds.get("app_executions")["high"], + ] + ) + + def _set_application_execution_thresholds(self, thresholds: List[int]): + """ + Method that validates and then sets the application execution threshold. + + :param: thresholds: The application execution threshold to validate and set. + """ + if self._validate_thresholds( + thresholds=[ + thresholds[0], + thresholds[1], + thresholds[2], + ], + threshold_identifier="app_executions", + ): + self.low_app_execution_threshold = thresholds[0] + self.med_app_execution_threshold = thresholds[1] + self.high_app_execution_threshold = thresholds[2] def _categorise_num_executions(self, num_executions: int) -> int: """ - Represent number of file accesses as a categorical variable. + Represent number of application executions as a categorical variable. - :param num_access: Number of file accesses. + :param num_access: Number of application executions. :return: Bin number corresponding to the number of accesses. """ - if num_executions > self.high_threshold: + if num_executions > self.high_app_execution_threshold: return 3 - elif num_executions > self.med_threshold: + elif num_executions > self.med_app_execution_threshold: return 2 - elif num_executions > self.low_threshold: + elif num_executions > self.low_app_execution_threshold: return 1 return 0 @@ -128,7 +168,9 @@ class ApplicationObservation(AbstractObservation, discriminator="application"): return self.default_observation return { "operating_status": application_state["operating_state"], - "health_status": application_state["health_state_visible"], + "health_status": application_state["health_state_visible"] + if self.applications_requires_scan + else application_state["health_state_actual"], "num_executions": self._categorise_num_executions(application_state["num_executions"]), } @@ -161,4 +203,8 @@ class ApplicationObservation(AbstractObservation, discriminator="application"): :return: Constructed application observation instance. :rtype: ApplicationObservation """ - return cls(where=parent_where + ["applications", config.application_name]) + return cls( + where=parent_where + ["applications", config.application_name], + applications_requires_scan=config.applications_requires_scan, + thresholds=config.thresholds, + ) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 1427776e..a3b77ec3 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict from primaite import DEFAULT_BANDWIDTH, getLogger from primaite.game.agent.interface import AbstractAgent, ProxyAgent +from primaite.game.agent.observations import NICObservation from primaite.game.agent.rewards import SharedReward from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator import SIM_OUTPUT @@ -44,15 +45,15 @@ from primaite.utils.validation.port import Port, PORT_LOOKUP _LOGGER = getLogger(__name__) SERVICE_TYPES_MAPPING = { - "DNSClient": DNSClient, - "DNSServer": DNSServer, - "DatabaseService": DatabaseService, - "WebServer": WebServer, - "FTPClient": FTPClient, - "FTPServer": FTPServer, - "NTPClient": NTPClient, - "NTPServer": NTPServer, - "Terminal": Terminal, + "dns-client": DNSClient, + "dns-server": DNSServer, + "database-service": DatabaseService, + "web-server": WebServer, + "ftp-client": FTPClient, + "ftp-server": FTPServer, + "ntp-client": NTPClient, + "ntp-server": NTPServer, + "terminal": Terminal, } """List of available services that can be installed on nodes in the PrimAITE Simulation.""" @@ -68,6 +69,8 @@ class PrimaiteGameOptions(BaseModel): seed: int = None """Random number seed for RNGs.""" + generate_seed_value: bool = False + """Internally generated seed value.""" max_episode_length: int = 256 """Maximum number of episodes for the PrimAITE game.""" ports: List[Port] @@ -175,6 +178,7 @@ class PrimaiteGame: parameters=parameters, request=request, response=response, + observation=obs, ) def pre_timestep(self) -> None: @@ -263,6 +267,7 @@ class PrimaiteGame: node_sets_cfg = network_config.get("node_sets", []) # Set the NMNE capture config NetworkInterface.nmne_config = NMNEConfig(**network_config.get("nmne_config", {})) + NICObservation.capture_nmne = NMNEConfig(**network_config.get("nmne_config", {})).capture_nmne for node_cfg in nodes_cfg: n_type = node_cfg["type"] @@ -293,6 +298,7 @@ class PrimaiteGame: if "users" in node_cfg and new_node.software_manager.software.get("user-manager"): user_manager: UserManager = new_node.software_manager.software["user-manager"] # noqa + for user_cfg in node_cfg["users"]: user_manager.add_user(**user_cfg, bypass_can_perform_action=True) @@ -407,6 +413,7 @@ class PrimaiteGame: agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: + agent_cfg = {**agent_cfg, "thresholds": game.options.thresholds} new_agent = AbstractAgent.from_config(agent_cfg) game.agents[agent_cfg["ref"]] = new_agent if isinstance(new_agent, ProxyAgent): diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index ef4e75dd..52499ea6 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -50,40 +50,22 @@ "custom_c2_agent = \"\"\"\n", " - ref: CustomC2Agent\n", " team: RED\n", - " type: ProxyAgent\n", + " type: proxy-a.gent\n", "\n", " action_space:\n", - " options:\n", - " nodes:\n", - " - node_name: web_server\n", - " applications:\n", - " - application_name: C2Beacon\n", - " - node_name: client_1\n", - " applications:\n", - " - application_name: C2Server\n", - " max_folders_per_node: 1\n", - " max_files_per_folder: 1\n", - " max_services_per_node: 2\n", - " max_nics_per_node: 8\n", - " max_acl_rules: 10\n", - " ip_list:\n", - " - 192.168.1.21\n", - " - 192.168.1.14\n", - " wildcard_list:\n", - " - 0.0.0.1\n", " action_map:\n", " 0:\n", " action: do_nothing\n", " options: {}\n", " 1:\n", - " action: node_application_install\n", + " action: node-application-install\n", " options:\n", - " node_id: 0\n", - " application_name: C2Beacon\n", + " node_name: web_server\n", + " application_name: c2-beacon\n", " 2:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " config:\n", " c2_server_ip_address: 192.168.10.21\n", " keep_alive_frequency:\n", @@ -92,10 +74,10 @@ " 3:\n", " action: node_application_execute\n", " options:\n", - " node_id: 0\n", - " application_id: 0\n", + " node_name: web_server\n", + " application_name: c2-beacon\n", " 4:\n", - " action: c2_server_terminal_command\n", + " action: c2-server-terminal-command\n", " options:\n", " node_id: 1\n", " ip_address:\n", @@ -111,14 +93,14 @@ " 5:\n", " action: c2-server-ransomware-configure\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " config:\n", " server_ip_address: 192.168.1.14\n", " payload: ENCRYPT\n", " 6:\n", - " action: c2_server_data_exfiltrate\n", + " action: c2-server-data-exfiltrate\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " target_file_name: \"database.db\"\n", " target_folder_name: \"database\"\n", " exfiltration_folder_name: \"spoils\"\n", @@ -128,31 +110,27 @@ " password: admin\n", "\n", " 7:\n", - " action: c2_server_ransomware_launch\n", + " action: c2-server-ransomware-launch\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " 8:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " config:\n", " c2_server_ip_address: 192.168.10.21\n", " keep_alive_frequency: 10\n", " masquerade_protocol: TCP\n", " masquerade_port: DNS\n", " 9:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " config:\n", " c2_server_ip_address: 192.168.10.22\n", " keep_alive_frequency:\n", " masquerade_protocol:\n", " masquerade_port:\n", - "\n", - " reward_function:\n", - " reward_components:\n", - " - type: DUMMY\n", "\"\"\"\n", "c2_agent_yaml = yaml.safe_load(custom_c2_agent)" ] @@ -225,7 +203,7 @@ " nodes: # Node List\n", " - node_name: web_server\n", " applications: \n", - " - application_name: C2Beacon\n", + " - application_name: c2-beacon\n", " ...\n", " ...\n", " action_map:\n", @@ -233,7 +211,7 @@ " action: node_application_install \n", " options:\n", " node_id: 0 # Index 0 at the node list.\n", - " application_name: C2Beacon\n", + " application_name: c2-beacon\n", "```" ] }, @@ -268,7 +246,7 @@ " action_map:\n", " ...\n", " 2:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", " node_id: 0 # Node Index\n", " config: # Further information about these config options can be found at the bottom of this notebook.\n", @@ -286,7 +264,7 @@ "outputs": [], "source": [ "env.step(2)\n", - "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", "web_server.software_manager.show()\n", "c2_beacon.show()" ] @@ -307,13 +285,13 @@ " nodes: # Node List\n", " - node_name: web_server\n", " applications: \n", - " - application_name: C2Beacon\n", + " - application_name: c2-beacon\n", " ...\n", " ...\n", " action_map:\n", " ...\n", " 3:\n", - " action: node_application_execute\n", + " action: node-application-execute\n", " options:\n", " node_id: 0\n", " application_id: 0\n", @@ -374,11 +352,11 @@ " ...\n", " - node_name: client_1\n", " applications: \n", - " - application_name: C2Server\n", + " - application_name: c2-server\n", " ...\n", " action_map:\n", " 4:\n", - " action: C2_SERVER_TERMINAL_COMMAND\n", + " action: c2-server-terminal-command\n", " options:\n", " node_id: 1\n", " ip_address:\n", @@ -431,7 +409,7 @@ " ...\n", " - node_name: client_1\n", " applications: \n", - " - application_name: C2Server\n", + " - application_name: c2-server\n", " ...\n", " action_map:\n", " 5:\n", @@ -459,7 +437,7 @@ "metadata": {}, "outputs": [], "source": [ - "ransomware_script: RansomwareScript = web_server.software_manager.software[\"RansomwareScript\"]\n", + "ransomware_script: RansomwareScript = web_server.software_manager.software[\"ransomware-script\"]\n", "web_server.software_manager.show()\n", "ransomware_script.show()" ] @@ -483,11 +461,11 @@ " ...\n", " - node_name: client_1\n", " applications: \n", - " - application_name: C2Server\n", + " - application_name: c2-server\n", " ...\n", " action_map:\n", " 6:\n", - " action: c2_server_data_exfiltrate\n", + " action: c2-server-data-exfiltrate\n", " options:\n", " node_id: 1\n", " target_file_name: \"database.db\"\n", @@ -549,11 +527,11 @@ " ...\n", " - node_name: client_1\n", " applications: \n", - " - application_name: C2Server\n", + " - application_name: c2-server\n", " ...\n", " action_map:\n", " 7:\n", - " action: c2_server_ransomware_launch\n", + " action: c2-server-ransomware-launch\n", " options:\n", " node_id: 1\n", "```\n" @@ -598,20 +576,20 @@ "custom_blue_agent_yaml = \"\"\"\n", " - ref: defender\n", " team: BLUE\n", - " type: ProxyAgent\n", + " type: proxy-agent\n", "\n", " observation_space:\n", - " type: CUSTOM\n", + " type: custom\n", " options:\n", " components:\n", - " - type: NODES\n", + " - type: nodes\n", " label: NODES\n", " options:\n", " hosts:\n", " - hostname: web_server\n", " applications:\n", - " - application_name: C2Beacon\n", - " - application_name: RansomwareScript\n", + " - application_name: c2-beacon\n", + " - application_name: ransomware-script\n", " folders:\n", " - folder_name: exfiltration_folder\n", " files:\n", @@ -661,7 +639,7 @@ " - UDP\n", " num_rules: 10\n", "\n", - " - type: LINKS\n", + " - type: links\n", " label: LINKS\n", " options:\n", " link_references:\n", @@ -675,7 +653,7 @@ " - switch_2:eth-1<->client_1:eth-1\n", " - switch_2:eth-2<->client_2:eth-1\n", " - switch_2:eth-7<->security_suite:eth-2\n", - " - type: \"NONE\"\n", + " - type: \"none\"\n", " label: ICS\n", " options: {}\n", "\n", @@ -685,16 +663,16 @@ " action: do_nothing\n", " options: {}\n", " 1:\n", - " action: node_application_remove\n", + " action: node-application-remove\n", " options:\n", - " node_id: 0\n", + " node_name: web-server\n", " application_name: C2Beacon\n", " 2:\n", - " action: node_shutdown\n", + " action: node-shutdown\n", " options:\n", - " node_id: 0\n", + " node_name: web-server\n", " 3:\n", - " action: router_acl_add_rule\n", + " action: router-acl-add-rule\n", " options:\n", " target_router: router_1\n", " position: 1\n", @@ -707,36 +685,6 @@ " source_wildcard_id: 0\n", " dest_wildcard_id: 0\n", "\n", - "\n", - " options:\n", - " nodes:\n", - " - node_name: web_server\n", - " applications:\n", - " - application_name: C2Beacon\n", - "\n", - " - node_name: database_server\n", - " folders:\n", - " - folder_name: database\n", - " files:\n", - " - file_name: database.db\n", - " services:\n", - " - service_name: DatabaseService\n", - " - node_name: router_1\n", - "\n", - " max_folders_per_node: 2\n", - " max_files_per_folder: 2\n", - " max_services_per_node: 2\n", - " max_nics_per_node: 8\n", - " max_acl_rules: 10\n", - " ip_list:\n", - " - 192.168.10.21\n", - " - 192.168.1.12\n", - " wildcard_list:\n", - " - 0.0.0.1\n", - " reward_function:\n", - " reward_components:\n", - " - type: DUMMY\n", - "\n", " agent_settings:\n", " flatten_obs: False\n", "\"\"\"\n", @@ -875,7 +823,7 @@ "outputs": [], "source": [ "# Installing RansomwareScript via C2 Terminal Commands\n", - "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"ransomware-script\"]],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)\n" @@ -1034,11 +982,11 @@ " web_server: Server = given_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "\n", " client_1.software_manager.install(C2Server)\n", - " c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + " c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", " c2_server.run()\n", "\n", " web_server.software_manager.install(C2Beacon)\n", - " c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + " c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", " c2_beacon.configure(c2_server_ip_address=\"192.168.10.21\")\n", " c2_beacon.establish()\n", "\n", @@ -1132,11 +1080,11 @@ "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", - "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"ransomware-script\"]],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" ] }, @@ -1220,11 +1168,11 @@ "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", - "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", + "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"ransomware-script\"],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", - "c2_server: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server.send_command(C2Command.TERMINAL, command_options=ransomware_install_command)" ] }, @@ -1345,7 +1293,7 @@ "metadata": {}, "outputs": [], "source": [ - "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", + "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database-server\")\n", "database_server.software_manager.file_system.show(full=True)" ] }, @@ -1391,7 +1339,7 @@ "\n", "``` YAML\n", "...\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", " node_id: 0\n", " config:\n", @@ -1446,16 +1394,16 @@ "source": [ "web_server: Server = c2_config_env.game.simulation.network.get_node_by_hostname(\"web_server\")\n", "web_server.software_manager.install(C2Beacon)\n", - "c2_beacon: C2Beacon = web_server.software_manager.software[\"C2Beacon\"]\n", + "c2_beacon: C2Beacon = web_server.software_manager.software[\"c2-beacon\"]\n", "\n", "client_1: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", "client_1.software_manager.install(C2Server)\n", - "c2_server_1: C2Server = client_1.software_manager.software[\"C2Server\"]\n", + "c2_server_1: C2Server = client_1.software_manager.software[\"c2-server\"]\n", "c2_server_1.run()\n", "\n", "client_2: Computer = c2_config_env.game.simulation.network.get_node_by_hostname(\"client_2\")\n", "client_2.software_manager.install(C2Server)\n", - "c2_server_2: C2Server = client_2.software_manager.software[\"C2Server\"]\n", + "c2_server_2: C2Server = client_2.software_manager.software[\"c2-server\"]\n", "c2_server_2.run()" ] }, @@ -1759,6 +1707,16 @@ "\n", "display_obs_diffs(tcp_c2_obs, udp_c2_obs, blue_config_env.game.step_counter)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "env.game.agents[\"CustomC2Agent\"].show_history()" + ] } ], "metadata": { diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 756fc44f..2dbf750e 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -47,7 +47,7 @@ "source": [ "def make_cfg_have_flat_obs(cfg):\n", " for agent in cfg['agents']:\n", - " if agent['type'] == \"ProxyAgent\":\n", + " if agent['type'] == \"proxy-agent\":\n", " agent['agent_settings']['flatten_obs'] = False" ] }, @@ -76,9 +76,9 @@ " # parse the info dict form step output and write out what the red agent is doing\n", " red_info : AgentHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", " red_action = red_info.action\n", - " if red_action == 'do_nothing':\n", + " if red_action == 'do-nothing':\n", " red_str = 'DO NOTHING'\n", - " elif red_action == 'node_application_execute':\n", + " elif red_action == 'node-application-execute':\n", " client = \"client 1\" if red_info.parameters['node_id'] == 0 else \"client 2\"\n", " red_str = f\"ATTACK from {client}\"\n", " return red_str" @@ -147,36 +147,14 @@ "```yaml\n", " - ref: data_manipulation_attacker # name of agent\n", " team: RED # not used, just for human reference\n", - " type: RedDatabaseCorruptingAgent # type of agent - this lets primaite know which agent class to use\n", + " type: red-database-corrupting-agent # type of agent - this lets primaite know which agent class to use\n", "\n", " # Since the agent does not need to react to what is happening in the environment, the observation space is empty.\n", " observation_space:\n", - " type: UC2RedObservation\n", + " type: uc2-red-observation # TODO: what\n", " options:\n", " nodes: {}\n", "\n", - " action_space:\n", - " \n", - " # The agent has access to the DataManipulationBoth on clients 1 and 2.\n", - " options:\n", - " nodes:\n", - " - node_name: client_1 # The network should have a node called client_1\n", - " applications:\n", - " - application_name: DataManipulationBot # The node client_1 should have DataManipulationBot configured on it\n", - " - node_name: client_2 # The network should have a node called client_2\n", - " applications:\n", - " - application_name: DataManipulationBot # The node client_2 should have DataManipulationBot configured on it\n", - "\n", - " # not important\n", - " max_folders_per_node: 1\n", - " max_files_per_folder: 1\n", - " max_services_per_node: 1\n", - "\n", - " # red agent does not need a reward function\n", - " reward_function:\n", - " reward_components:\n", - " - type: DUMMY\n", - "\n", " # These actions are passed to the RedDatabaseCorruptingAgent init method, they dictate the schedule of attacks\n", " agent_settings:\n", " start_settings:\n", @@ -211,15 +189,13 @@ " \n", " # \n", " applications:\n", - " - ref: data_manipulation_bot\n", - " type: DataManipulationBot\n", + " - type: data-manipulation-bot\n", " options:\n", " port_scan_p_of_success: 0.8 # Probability that port scan is successful\n", " data_manipulation_p_of_success: 0.8 # Probability that SQL attack is successful\n", " payload: \"DELETE\" # The SQL query which causes the attack (this has to be DELETE)\n", " server_ip: 192.168.1.14 # IP address of server hosting the database\n", - " - ref: client_1_database_client\n", - " type: DatabaseClient # Database client must be installed in order for DataManipulationBot to function\n", + " - type: database-client # Database client must be installed in order for DataManipulationBot to function\n", " options:\n", " db_server_ip: 192.168.1.14 # IP address of server hosting the database\n", "```" @@ -354,19 +330,16 @@ "# Make attack always succeed.\n", "change = yaml.safe_load(\"\"\"\n", " applications:\n", - " - ref: data_manipulation_bot\n", - " type: DataManipulationBot\n", + " - type: data-manipulation-bot\n", " options:\n", " port_scan_p_of_success: 1.0\n", " data_manipulation_p_of_success: 1.0\n", " payload: \"DELETE\"\n", " server_ip: 192.168.1.14\n", - " - ref: client_1_web_browser\n", - " type: WebBrowser\n", + " - type: web-browser\n", " options:\n", " target_url: http://arcd.com/users/\n", - " - ref: client_1_database_client\n", - " type: DatabaseClient\n", + " - type: database-client\n", " options:\n", " db_server_ip: 192.168.1.14\n", "\"\"\")\n", @@ -399,19 +372,16 @@ "# Make attack always fail.\n", "change = yaml.safe_load(\"\"\"\n", " applications:\n", - " - ref: data_manipulation_bot\n", - " type: DataManipulationBot\n", + " - type: data-manipulation-bot\n", " options:\n", " port_scan_p_of_success: 0.0\n", " data_manipulation_p_of_success: 0.0\n", " payload: \"DELETE\"\n", " server_ip: 192.168.1.14\n", - " - ref: client_1_web_browser\n", - " type: WebBrowser\n", + " - type: web-browser\n", " options:\n", " target_url: http://arcd.com/users/\n", - " - ref: client_1_database_client\n", - " type: DatabaseClient\n", + " - type: database-client\n", " options:\n", " db_server_ip: 192.168.1.14\n", "\"\"\")\n", diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index dbc6f0c1..2070e03c 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -684,6 +684,15 @@ " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.game.agents[\"data_manipulation_attacker\"].show_history()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -717,7 +726,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb index f8691d7d..58573ac6 100644 --- a/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb +++ b/src/primaite/notebooks/Getting-Information-Out-Of-PrimAITE.ipynb @@ -153,6 +153,49 @@ "PRIMAITE_CONFIG[\"developer_mode\"][\"enabled\"] = was_enabled\n", "PRIMAITE_CONFIG[\"developer_mode\"][\"output_sys_logs\"] = was_syslogs_enabled" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Viewing Agent history\n", + "\n", + "It's possible to view the actions carried out by an agent for a given training session using the `show_history()` method. By default, this will be all actions apart from DONOTHING actions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + "\n", + "env = PrimaiteGymEnv(env_config=cfg)\n", + "\n", + "# Run the training session to generate some resultant data.\n", + "for i in range(100):\n", + " env.step(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calling `.show_history()` should show us when the Data Manipulation used the `NODE_APPLICATION_EXECUTE` action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "attacker = env.game.agents[\"data_manipulation_attacker\"]\n", + "\n", + "attacker.show_history()" + ] } ], "metadata": { @@ -171,7 +214,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/notebooks/How-To-Use-Primaite-Dev-Mode.ipynb b/src/primaite/notebooks/How-To-Use-Primaite-Dev-Mode.ipynb new file mode 100644 index 00000000..8f8ec24b --- /dev/null +++ b/src/primaite/notebooks/How-To-Use-Primaite-Dev-Mode.ipynb @@ -0,0 +1,479 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PrimAITE Developer mode\n", + "\n", + "PrimAITE has built in developer tools.\n", + "\n", + "The dev-mode is designed to help make the development of PrimAITE easier.\n", + "\n", + "`NOTE: For the purposes of the notebook, the commands are preceeded by \"!\". When running the commands, run it without the \"!\".`\n", + "\n", + "To display the available dev-mode options, run the command below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode --help" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Save the current PRIMAITE_CONFIG to restore after the notebook runs\n", + "\n", + "from primaite import PRIMAITE_CONFIG\n", + "\n", + "temp_config = PRIMAITE_CONFIG.copy()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dev mode options" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### enable\n", + "\n", + "Enables the dev mode for PrimAITE.\n", + "\n", + "This will enable the developer mode for PrimAITE.\n", + "\n", + "By default, when developer mode is enabled, session logs will be generated in the PRIMAITE_ROOT/sessions folder unless configured to be generated in another location." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode enable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### disable\n", + "\n", + "Disables the dev mode for PrimAITE.\n", + "\n", + "This will disable the developer mode for PrimAITE." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode disable" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### show\n", + "\n", + "Shows if PrimAITE is running in dev mode or production mode.\n", + "\n", + "The command will also show the developer mode configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode show" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### config\n", + "\n", + "Configure the PrimAITE developer mode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### path\n", + "\n", + "Set the path where generated session files will be output.\n", + "\n", + "By default, this value will be in PRIMAITE_ROOT/sessions.\n", + "\n", + "To reset the path to default, run:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config path -root\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config path --default" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### --sys-log-level or -slevel\n", + "\n", + "Set the system log level.\n", + "\n", + "This will override the system log level in configurations and will make PrimAITE include the set log level and above.\n", + "\n", + "Available options are:\n", + "- `DEBUG`\n", + "- `INFO`\n", + "- `WARNING`\n", + "- `ERROR`\n", + "- `CRITICAL`\n", + "\n", + "Default value is `DEBUG`\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --sys-log-level DEBUG\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -slevel DEBUG" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### --agent-log-level or -alevel\n", + "\n", + "Set the agent log level.\n", + "\n", + "This will override the agent log level in configurations and will make PrimAITE include the set log level and above.\n", + "\n", + "Available options are:\n", + "- `DEBUG`\n", + "- `INFO`\n", + "- `WARNING`\n", + "- `ERROR`\n", + "- `CRITICAL`\n", + "\n", + "Default value is `DEBUG`\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --agent-log-level DEBUG\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -alevel DEBUG" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### --output-sys-logs or -sys\n", + "\n", + "If enabled, developer mode will output system logs.\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --output-sys-logs\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -sys" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To disable outputting sys logs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --no-sys-logs\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -nsys" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### --output-agent-logs or -agent\n", + "\n", + "If enabled, developer mode will output agent action logs.\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --output-agent-logs\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -agent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To disable outputting agent action logs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --no-agent-logs\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -nagent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### --output-pcap-logs or -pcap\n", + "\n", + "If enabled, developer mode will output PCAP logs.\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --output-pcap-logs\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -pcap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To disable outputting PCAP logs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --no-pcap-logs\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -npcap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### --output-to-terminal or -t\n", + "\n", + "If enabled, developer mode will output logs to the terminal.\n", + "\n", + "Example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --output-to-terminal\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -t" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To disable terminal outputs:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config --no-terminal\n", + "\n", + "# or\n", + "\n", + "!primaite dev-mode config -nt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Combining commands\n", + "\n", + "It is possible to combine commands to set the configuration.\n", + "\n", + "This saves having to enter multiple commands and allows for a much more efficient setting of PrimAITE developer mode configurations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Example of setting system log level and enabling the system logging:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config -slevel WARNING -sys" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another example where the system log and agent action log levels are set and enabled and should be printed to terminal:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!primaite dev-mode config -slevel ERROR -sys -alevel ERROR -agent -t" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Restore PRIMAITE_CONFIG\n", + "from primaite.utils.cli.primaite_config_utils import update_primaite_application_config\n", + "\n", + "\n", + "global PRIMAITE_CONFIG\n", + "PRIMAITE_CONFIG[\"developer_mode\"] = temp_config[\"developer_mode\"]\n", + "update_primaite_application_config(config=PRIMAITE_CONFIG)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/primaite/notebooks/Requests-and-Responses.ipynb b/src/primaite/notebooks/Requests-and-Responses.ipynb index 83aed07c..9260b29d 100644 --- a/src/primaite/notebooks/Requests-and-Responses.ipynb +++ b/src/primaite/notebooks/Requests-and-Responses.ipynb @@ -114,7 +114,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"DNS Client state: {client.software_manager.software.get('DNSClient').operating_state.name}\")" + "print(f\"DNS Client state: {client.software_manager.software.get('dns-client').operating_state.name}\")" ] }, { diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb index 9aa4e96a..7c94d432 100644 --- a/src/primaite/notebooks/Terminal-Processing.ipynb +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -9,6 +9,13 @@ "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simulation Layer Implementation." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -67,9 +74,9 @@ "source": [ "network: Network = basic_network()\n", "computer_a: Computer = network.get_node_by_hostname(\"node_a\")\n", - "terminal_a: Terminal = computer_a.software_manager.software.get(\"Terminal\")\n", + "terminal_a: Terminal = computer_a.software_manager.software.get(\"terminal\")\n", "computer_b: Computer = network.get_node_by_hostname(\"node_b\")\n", - "terminal_b: Terminal = computer_b.software_manager.software.get(\"Terminal\")" + "terminal_b: Terminal = computer_b.software_manager.software.get(\"terminal\")" ] }, { @@ -121,7 +128,7 @@ "metadata": {}, "outputs": [], "source": [ - "term_a_term_b_remote_connection.execute([\"software_manager\", \"application\", \"install\", \"RansomwareScript\"])" + "term_a_term_b_remote_connection.execute([\"software_manager\", \"application\", \"install\", \"ransomware-script\"])" ] }, { @@ -169,6 +176,22 @@ "computer_b.file_system.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Information about the latest response when executing a remote command can be seen by calling the `last_response` attribute within `Terminal`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(terminal_a.last_response)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -207,6 +230,263 @@ "source": [ "computer_b.user_session_manager.show(include_historic=True, include_session_id=True)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Game Layer Implementation\n", + "\n", + "This notebook section will detail the implementation of how the game layer utilises the terminal to support different agent actions.\n", + "\n", + "The ``Terminal`` is used in a variety of different ways in the game layer. Specifically, the terminal is leveraged to implement the following actions:\n", + "\n", + "\n", + "| Game Layer Action | Simulation Layer |\n", + "|-----------------------------------|--------------------------|\n", + "| ``node-send-local-command`` | Uses the given user credentials, creates a ``LocalTerminalSession`` and executes the given command and returns the ``RequestResponse``.\n", + "| ``node-session-remote-login`` | Uses the given user credentials and remote IP to create a ``RemoteTerminalSession``.\n", + "| ``node-send-remote-command`` | Uses the given remote IP to locate the correct ``RemoteTerminalSession``, executes the given command and returns the ``RequestsResponse``." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Game Layer Setup\n", + "\n", + "Similar to other notebooks, the next code cells create a custom proxy agent to demonstrate how these commands can be leveraged by agents in the ``UC2`` network environment.\n", + "\n", + "If you're unfamiliar with ``UC2`` then please refer to the [UC2-E2E-Demo notebook for further reference](./Data-Manipulation-E2E-Demonstration.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "from primaite.config.load import data_manipulation_config_path\n", + "from primaite.session.environment import PrimaiteGymEnv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "custom_terminal_agent = \"\"\"\n", + " - ref: CustomC2Agent\n", + " team: RED\n", + " type: proxy-agent\n", + " observation_space: null\n", + " action_space:\n", + " options:\n", + " nodes:\n", + " - node_name: client_1\n", + " max_folders_per_node: 1\n", + " max_files_per_folder: 1\n", + " max_services_per_node: 2\n", + " max_nics_per_node: 8\n", + " max_acl_rules: 10\n", + " ip_list:\n", + " - 192.168.1.21\n", + " - 192.168.1.14\n", + " wildcard_list:\n", + " - 0.0.0.1\n", + " action_map:\n", + " 0:\n", + " action: do-nothing\n", + " options: {}\n", + " 1:\n", + " action: node-send-local-command\n", + " options:\n", + " node_name: client_1\n", + " username: admin\n", + " password: admin\n", + " command:\n", + " - file_system\n", + " - create\n", + " - file\n", + " - downloads\n", + " - dog.png\n", + " - False\n", + " 2:\n", + " action: node-session-remote-login\n", + " options:\n", + " node_name: client_1\n", + " username: admin\n", + " password: admin\n", + " remote_ip: 192.168.10.22\n", + " 3:\n", + " action: node-send-remote-command\n", + " options:\n", + " node_name: client_1\n", + " remote_ip: 192.168.10.22\n", + " command:\n", + " - file_system\n", + " - create\n", + " - file\n", + " - downloads\n", + " - cat.png\n", + " - False\n", + "\"\"\"\n", + "custom_terminal_agent_yaml = yaml.safe_load(custom_terminal_agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path()) as f:\n", + " cfg = yaml.safe_load(f)\n", + " # removing all agents & adding the custom agent.\n", + " cfg['agents'] = {}\n", + " cfg['agents'] = custom_terminal_agent_yaml\n", + "\n", + "env = PrimaiteGymEnv(env_config=cfg)\n", + "\n", + "client_1: Computer = env.game.simulation.network.get_node_by_hostname(\"client_1\")\n", + "client_2: Computer = env.game.simulation.network.get_node_by_hostname(\"client_2\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Terminal Action | ``node-send-local-command`` \n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + "\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: node-send-local-command\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: client_1\n", + " ...\n", + " ...\n", + " action_map:\n", + " 1:\n", + " action: node-send-local-command\n", + " options:\n", + " node_id: 0 # Index 0 at the node list.\n", + " username: admin\n", + " password: admin\n", + " command:\n", + " - file_system\n", + " - create\n", + " - file\n", + " - downloads\n", + " - dog.png\n", + " - False\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(1)\n", + "client_1.file_system.show(full=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Terminal Action | ``node-session-remote-login`` \n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + "\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: node-session-remote-login\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: client_1\n", + " ...\n", + " ...\n", + " action_map:\n", + " 2:\n", + " action: node-session-remote-login\n", + " options:\n", + " node_id: 0 # Index 0 at the node list.\n", + " username: admin\n", + " password: admin\n", + " remote_ip: 192.168.10.22 # client_2's ip address.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(2)\n", + "client_2.session_manager.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Terminal Action | ``node-send-remote-command``\n", + "\n", + "The yaml snippet below shows all the relevant agent options for this action:\n", + "\n", + "```yaml\n", + "\n", + " action_space:\n", + " action_list:\n", + " ...\n", + " - type: node-send-remote-command\n", + " ...\n", + " options:\n", + " nodes: # Node List\n", + " - node_name: client_1\n", + " ...\n", + " ...\n", + " action_map:\n", + " 1:\n", + " action: node-send-remote-command\n", + " options:\n", + " node_id: 0 # Index 0 at the node list.\n", + " remote_ip: 192.168.10.22\n", + " commands:\n", + " - file_system\n", + " - create\n", + " - file\n", + " - downloads\n", + " - cat.png\n", + " - False\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.step(3)\n", + "client_2.file_system.show(full=True)" + ] } ], "metadata": { diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index b7a9a042..fa545dbc 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -26,14 +26,26 @@ except ModuleNotFoundError: _LOGGER.debug("Torch not available for importing") -def set_random_seed(seed: int) -> Union[None, int]: +def set_random_seed(seed: int, generate_seed_value: bool) -> Union[None, int]: """ Set random number generators. + If seed is None or -1 and generate_seed_value is True randomly generate a + seed value. + If seed is > -1 and generate_seed_value is True ignore the latter and use + the provide seed value. + :param seed: int + :param generate_seed_value: bool + :return: None or the int representing the seed used. """ if seed is None or seed == -1: - return None + if generate_seed_value: + rng = np.random.default_rng() + # 2**32-1 is highest value for python RNG seed. + seed = int(rng.integers(low=0, high=2**32 - 1)) + else: + return None elif seed < -1: raise ValueError("Invalid random number seed") # Seed python RNG @@ -50,6 +62,13 @@ def set_random_seed(seed: int) -> Union[None, int]: return seed +def log_seed_value(seed: int): + """Log the selected seed value to file.""" + path = SIM_OUTPUT.path / "seed.log" + with open(path, "w") as file: + file.write(f"Seed value = {seed}") + + class PrimaiteGymEnv(gymnasium.Env): """ Thin wrapper env to provide agents with a gymnasium API. @@ -65,7 +84,8 @@ class PrimaiteGymEnv(gymnasium.Env): """Object that returns a config corresponding to the current episode.""" self.seed = self.episode_scheduler(0).get("game", {}).get("seed") """Get RNG seed from config file. NB: Must be before game instantiation.""" - self.seed = set_random_seed(self.seed) + self.generate_seed_value = self.episode_scheduler(0).get("game", {}).get("generate_seed_value") + self.seed = set_random_seed(self.seed, self.generate_seed_value) self.io = PrimaiteIO.from_config(self.episode_scheduler(0).get("io_settings", {})) """Handles IO for the environment. This produces sys logs, agent logs, etc.""" self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(0)) @@ -79,6 +99,8 @@ class PrimaiteGymEnv(gymnasium.Env): _LOGGER.info(f"PrimaiteGymEnv RNG seed = {self.seed}") + log_seed_value(self.seed) + def action_masks(self) -> np.ndarray: """ Return the action mask for the agent. @@ -146,7 +168,7 @@ class PrimaiteGymEnv(gymnasium.Env): f"avg. reward: {self.agent.reward_function.total_reward}" ) if seed is not None: - set_random_seed(seed) + set_random_seed(seed, self.generate_seed_value) self.total_reward_per_episode[self.episode_counter] = self.agent.reward_function.total_reward if self.io.settings.save_agent_actions: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 8653359a..50a7d2a4 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -864,7 +864,21 @@ class UserManager(Service, discriminator="user-manager"): """ rm = super()._init_request_manager() - # todo add doc about requeest schemas + # todo add doc about request schemas + rm.add_request( + "add_user", + RequestType( + func=lambda request, context: RequestResponse.from_bool( + self.add_user(username=request[0], password=request[1], is_admin=request[2]) + ) + ), + ) + rm.add_request( + "disable_user", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.disable_user(username=request[0])) + ), + ) rm.add_request( "change_password", RequestType( @@ -1572,7 +1586,7 @@ class Node(SimComponent, ABC): operating_state: Any = None - users: Any = None # Temporary to appease "extra=forbid" + users: List[Dict] = [] # Temporary to appease "extra=forbid" config: ConfigSchema = Field(default_factory=lambda: Node.ConfigSchema()) """Configuration items within Node""" @@ -1638,6 +1652,8 @@ class Node(SimComponent, ABC): self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager + for user in self.config.users: + self.user_manager.add_user(**user, bypass_can_perform_action=True) @property def user_manager(self) -> Optional[UserManager]: @@ -1769,7 +1785,7 @@ class Node(SimComponent, ABC): """ application_name = request[0] if self.software_manager.software.get(application_name): - self.sys_log.warning(f"Can't install {application_name}. It's already installed.") + self.sys_log.info(f"Can't install {application_name}. It's already installed.") return RequestResponse(status="success", data={"reason": "already installed"}) application_class = Application._registry[application_name] self.software_manager.install(application_class) diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 76d9167c..86ac790c 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -2,11 +2,12 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, ClassVar, Dict, Literal, Optional +from typing import Any, ClassVar, Dict, List, Literal, Optional from pydantic import Field from primaite import getLogger +from primaite.simulator.file_system.file_type import FileType from primaite.simulator.network.hardware.base import ( IPWiredNetworkInterface, Link, @@ -313,7 +314,7 @@ class HostNode(Node, discriminator="host-node"): """ SYSTEM_SOFTWARE: ClassVar[Dict] = { - "HostARP": HostARP, + "host-arp": HostARP, "icmp": ICMP, "dns-client": DNSClient, "ntp-client": NTPClient, @@ -339,7 +340,7 @@ class HostNode(Node, discriminator="host-node"): ip_address: IPV4Address services: Any = None # temporarily unset to appease extra="forbid" applications: Any = None # temporarily unset to appease extra="forbid" - folders: Any = None # temporarily unset to appease extra="forbid" + folders: List[Dict] = {} # temporarily unset to appease extra="forbid" network_interfaces: Any = None # temporarily unset to appease extra="forbid" config: ConfigSchema = Field(default_factory=lambda: HostNode.ConfigSchema()) @@ -348,6 +349,18 @@ class HostNode(Node, discriminator="host-node"): super().__init__(**kwargs) self.connect_nic(NIC(ip_address=kwargs["config"].ip_address, subnet_mask=kwargs["config"].subnet_mask)) + for folder in self.config.folders: + # handle empty foler defined by just a string + self.file_system.create_folder(folder["folder_name"]) + + for file in folder.get("files", []): + self.file_system.create_file( + folder_name=folder["folder_name"], + file_name=file["file_name"], + size=file.get("size", 0), + file_type=FileType[file.get("type", "UNKNOWN").upper()], + ) + @property def nmap(self) -> Optional[NMAP]: """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 2cbc23d2..c872b8b3 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -49,7 +49,7 @@ class Firewall(Router, discriminator="firewall"): Example: >>> from primaite.simulator.network.transmission.network_layer import IPProtocol - >>> from primaite.simulator.network.transmission.transport_layer import Port + >>> from primaite.utils.validation.port import Port >>> firewall = Firewall(hostname="Firewall1") >>> firewall.configure_internal_port(ip_address="192.168.1.1", subnet_mask="255.255.255.0") >>> firewall.configure_external_port(ip_address="10.0.0.1", subnet_mask="255.255.255.0") diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 3b35600b..47ee3169 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -467,6 +467,7 @@ class AccessControlList(SimComponent): """Check if a packet with the given properties is permitted through the ACL.""" permitted = False rule: ACLRule = None + for _rule in self._acl: if not _rule: continue @@ -1215,9 +1216,9 @@ class Router(NetworkNode, discriminator="router"): config: ConfigSchema = Field(default_factory=lambda: Router.ConfigSchema()) SYSTEM_SOFTWARE: ClassVar[Dict] = { - "UserSessionManager": UserSessionManager, - "UserManager": UserManager, - "Terminal": Terminal, + "user-session-manager": UserSessionManager, + "user-manager": UserManager, + "terminal": Terminal, } network_interfaces: Dict[str, RouterInterface] = {} @@ -1385,6 +1386,12 @@ class Router(NetworkNode, discriminator="router"): return False + def subject_to_acl(self, frame: Frame) -> bool: + """Check that frame is subject to ACL rules.""" + if frame.ip.protocol == "udp" and frame.is_arp: + return False + return True + def receive_frame(self, frame: Frame, from_network_interface: RouterInterface): """ Processes an incoming frame received on one of the router's interfaces. @@ -1398,8 +1405,12 @@ class Router(NetworkNode, discriminator="router"): if self.operating_state != NodeOperatingState.ON: return - # Check if it's permitted - permitted, rule = self.acl.is_permitted(frame) + if self.subject_to_acl(frame=frame): + # Check if it's permitted + permitted, rule = self.acl.is_permitted(frame) + else: + permitted = True + rule = None if not permitted: at_port = self._get_port_of_nic(from_network_interface) diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index e7c2a124..a07194a4 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -163,7 +163,7 @@ class Frame(BaseModel): """ Checks if the Frame is an ARP (Address Resolution Protocol) packet. - This is determined by checking if the destination port of the TCP header is equal to the ARP port. + This is determined by checking if the destination and source port of the UDP header is equal to the ARP port. :return: True if the Frame is an ARP packet, otherwise False. """ diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index b0630d5d..c6b687ce 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -55,7 +55,7 @@ class ARP(Service, discriminator="arp"): :param markdown: If True, format the output as Markdown. Otherwise, use plain text. """ - table = PrettyTable(["IP Address", "MAC Address", "Via"]) + table = PrettyTable(["IP Address", "MAC Address", "Via", "Port"]) if markdown: table.set_style(MARKDOWN) table.align = "l" @@ -66,6 +66,7 @@ class ARP(Service, discriminator="arp"): str(ip), arp.mac_address, self.software_manager.node.network_interfaces[arp.network_interface_uuid].mac_address, + self.software_manager.node.network_interfaces[arp.network_interface_uuid].port_num, ] ) print(table) diff --git a/src/primaite/simulator/system/services/terminal/terminal.py b/src/primaite/simulator/system/services/terminal/terminal.py index 2ce7d176..112f6abc 100644 --- a/src/primaite/simulator/system/services/terminal/terminal.py +++ b/src/primaite/simulator/system/services/terminal/terminal.py @@ -142,12 +142,20 @@ class Terminal(Service, discriminator="terminal"): _client_connection_requests: Dict[str, Optional[Union[str, TerminalClientConnection]]] = {} """Dictionary of connect requests made to remote nodes.""" + _last_response: Optional[RequestResponse] = None + """Last response received from RequestManager, for returning remote RequestResponse.""" + def __init__(self, **kwargs): kwargs["name"] = "terminal" kwargs["port"] = PORT_LOOKUP["SSH"] kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"] super().__init__(**kwargs) + @property + def last_response(self) -> Optional[RequestResponse]: + """Public version of _last_response attribute.""" + return self._last_response + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -186,7 +194,7 @@ class Terminal(Service, discriminator="terminal"): return RequestResponse(status="failure", data={}) rm.add_request( - "node-session-remote-login", + "node_session_remote_login", request_type=RequestType(func=_remote_login), ) @@ -209,28 +217,45 @@ class Terminal(Service, discriminator="terminal"): command: str = request[1]["command"] remote_connection = self._get_connection_from_ip(ip_address=ip_address) if remote_connection: - outcome = remote_connection.execute(command) - if outcome: - return RequestResponse( - status="success", - data={}, - ) - else: - return RequestResponse( - status="failure", - data={}, - ) + remote_connection.execute(command) + return self.last_response if not None else RequestResponse(status="failure", data={}) + return RequestResponse( + status="failure", + data={"reason": "Failed to execute command."}, + ) rm.add_request( "send_remote_command", request_type=RequestType(func=remote_execute_request), ) + def local_execute_request(request: RequestFormat, context: Dict) -> RequestResponse: + """Executes a command using a local terminal session.""" + command: str = request[2]["command"] + local_connection = self._process_local_login(username=request[0], password=request[1]) + if local_connection: + outcome = local_connection.execute(command) + if outcome: + return RequestResponse( + status="success", + data={"reason": outcome}, + ) + return RequestResponse( + status="success", + data={"reason": "Local Terminal failed to resolve command. Potentially invalid credentials?"}, + ) + + rm.add_request( + "send_local_command", + request_type=RequestType(func=local_execute_request), + ) + return rm def execute(self, command: List[Any]) -> Optional[RequestResponse]: """Execute a passed ssh command via the request manager.""" - return self.parent.apply_request(command) + self._last_response = self.parent.apply_request(command) + return self._last_response def _get_connection_from_ip(self, ip_address: IPv4Address) -> Optional[RemoteTerminalConnection]: """Find Remote Terminal Connection from a given IP.""" @@ -409,6 +434,8 @@ class Terminal(Service, discriminator="terminal"): """ source_ip = kwargs["frame"].ip.src_ip_address self.sys_log.info(f"{self.name}: Received payload: {payload}. Source: {source_ip}") + self._last_response = None # Clear last response + if isinstance(payload, SSHPacket): if payload.transport_message == SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST: # validate & add connection @@ -457,6 +484,9 @@ class Terminal(Service, discriminator="terminal"): session_id=session_id, source_ip=source_ip, ) + self._last_response: RequestResponse = RequestResponse( + status="success", data={"reason": "Login Successful"} + ) elif payload.transport_message == SSHTransportMessage.SSH_MSG_SERVICE_REQUEST: # Requesting a command to be executed @@ -468,12 +498,32 @@ class Terminal(Service, discriminator="terminal"): payload.connection_uuid ) remote_session.last_active_step = self.software_manager.node.user_session_manager.current_timestep - self.execute(command) + self._last_response: RequestResponse = self.execute(command) + + if self._last_response.status == "success": + transport_message = SSHTransportMessage.SSH_MSG_SERVICE_SUCCESS + else: + transport_message = SSHTransportMessage.SSH_MSG_SERVICE_FAILED + + payload: SSHPacket = SSHPacket( + payload=self._last_response, + transport_message=transport_message, + connection_message=SSHConnectionMessage.SSH_MSG_CHANNEL_DATA, + ) + self.software_manager.send_payload_to_session_manager( + payload=payload, dest_port=self.port, session_id=session_id + ) return True else: self.sys_log.error( f"{self.name}: Connection UUID:{payload.connection_uuid} is not valid. Rejecting Command." ) + elif ( + payload.transport_message == SSHTransportMessage.SSH_MSG_SERVICE_SUCCESS + or SSHTransportMessage.SSH_MSG_SERVICE_FAILED + ): + # Likely receiving command ack from remote. + self._last_response = payload.payload if isinstance(payload, dict) and payload.get("type"): if payload["type"] == "disconnect": diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 2eddefc1..3f8760c4 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -117,37 +117,44 @@ class WebServer(Service, discriminator="web-server"): :type: payload: HttpRequestPacket """ response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND, payload=payload) - try: - parsed_url = urlparse(payload.request_url) - path = parsed_url.path.strip("/") - if len(path) < 1: + parsed_url = urlparse(payload.request_url) + path = parsed_url.path.strip("/") if parsed_url and parsed_url.path else "" + + if len(path) < 1: + # query succeeded + response.status_code = HttpStatusCode.OK + + if path.startswith("users"): + # get data from DatabaseServer + # get all users + if not self._establish_db_connection(): + # unable to create a db connection + response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR + return response + + if self.db_connection.query("SELECT"): # query succeeded + self.set_health_state(SoftwareHealthState.GOOD) response.status_code = HttpStatusCode.OK + else: + self.set_health_state(SoftwareHealthState.COMPROMISED) + return response - if path.startswith("users"): - # get data from DatabaseServer - # get all users - if not self.db_connection: - self._establish_db_connection() - - if self.db_connection.query("SELECT"): - # query succeeded - self.set_health_state(SoftwareHealthState.GOOD) - response.status_code = HttpStatusCode.OK - else: - self.set_health_state(SoftwareHealthState.COMPROMISED) - - return response - except Exception: # TODO: refactor this. Likely to cause silent bugs. (ADO ticket #2345 ) - # something went wrong on the server - response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR - return response - - def _establish_db_connection(self) -> None: + def _establish_db_connection(self) -> bool: """Establish a connection to db.""" + # if active db connection, return true + if self.db_connection: + return True + + # otherwise, try to create db connection db_client = self.software_manager.software.get("database-client") + + if db_client is None: + return False # database client not installed + self.db_connection: DatabaseClientConnection = db_client.get_new_connection() + return self.db_connection is not None def send( self, diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index c9ac5f8d..7ffe4c08 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -25,7 +25,19 @@ game: - ICMP - TCP - UDP - + thresholds: + nmne: + high: 100 + medium: 25 + low: 5 + file_access: + high: 10 + medium: 5 + low: 2 + app_executions: + high: 5 + medium: 3 + low: 2 agents: - ref: client_2_green_user team: GREEN @@ -64,10 +76,16 @@ agents: options: hosts: - hostname: client_1 + applications: + - application_name: WebBrowser + folders: + - folder_name: root + files: + - file_name: "test.txt" - hostname: client_2 - hostname: client_3 num_services: 1 - num_applications: 0 + num_applications: 1 num_folders: 1 num_files: 1 num_nics: 2 @@ -182,6 +200,10 @@ simulation: options: ntp_server_ip: 192.168.1.10 - type: ntp-server + folders: + - folder_name: root + files: + - file_name: test.txt - hostname: client_2 type: computer ip_address: 192.168.10.22 diff --git a/tests/assets/configs/nodes_with_initial_files.yaml b/tests/assets/configs/nodes_with_initial_files.yaml new file mode 100644 index 00000000..d4c6406b --- /dev/null +++ b/tests/assets/configs/nodes_with_initial_files.yaml @@ -0,0 +1,226 @@ +# Basic Switched network +# +# -------------- -------------- -------------- +# | client_1 |------| switch_1 |------| client_2 | +# -------------- -------------- -------------- +# +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 + - DNS + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + +agents: + - ref: client_2_green_user + team: GREEN + type: periodic-agent + action_space: + action_map: + 0: + action: do-nothing + options: {} + 1: + action: node-application-execute + options: + node_id: 0 + application_id: 0 + + agent_settings: + possible_start_nodes: [client_2,] + target_application: web-browser + start_step: 5 + frequency: 4 + variance: 3 + + + + - ref: defender + team: BLUE + type: proxy-agent + + observation_space: + type: custom + options: + components: + - type: nodes + label: NODES + options: + hosts: + - hostname: client_1 + - hostname: client_2 + - hostname: client_3 + num_services: 1 + num_applications: 0 + num_folders: 1 + num_files: 1 + num_nics: 2 + include_num_access: false + monitored_traffic: + icmp: + - NONE + tcp: + - DNS + include_nmne: false + routers: + - hostname: router_1 + num_ports: 0 + ip_list: + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.23 + wildcard_list: + - 0.0.0.1 + port_list: + - 80 + - 5432 + protocol_list: + - ICMP + - TCP + - UDP + num_rules: 10 + + - type: links + label: LINKS + options: + link_references: + - switch_1:eth-1<->client_1:eth-1 + - switch_1:eth-2<->client_2:eth-1 + - type: none + label: ICS + options: {} + + action_space: + action_map: + 0: + action: do-nothing + options: {} + + reward_function: + reward_components: + - type: database-file-integrity + weight: 0.5 + options: + node_hostname: database_server + folder_name: database + file_name: database.db + + - type: web-server-404-penalty + weight: 0.5 + options: + node_hostname: web_server + service_name: web_server_web_service + + + agent_settings: + flatten_obs: true + +simulation: + network: + nodes: + + - type: switch + hostname: switch_1 + num_ports: 8 + + - hostname: client_1 + type: computer + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - type: ransomware-script + - type: web-browser + options: + target_url: http://arcd.com/users/ + - type: database-client + options: + db_server_ip: 192.168.1.10 + server_password: arcd + - type: data-manipulation-bot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.21 + server_password: arcd + - type: dos-bot + options: + target_ip_address: 192.168.10.21 + payload: SPOOF DATA + port_scan_p_of_success: 0.8 + services: + - type: dns-client + options: + dns_server: 192.168.1.10 + - type: dns-server + options: + domain_mapping: + arcd.com: 192.168.1.10 + - type: database-service + options: + backup_server_ip: 192.168.1.10 + - type: web-server + - type: ftp-server + options: + server_password: arcd + - type: ntp-client + options: + ntp_server_ip: 192.168.1.10 + - type: ntp-server + - hostname: client_2 + type: computer + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + folders: + - folder_name: empty_folder + - folder_name: downloads + files: + - file_name: "test.txt" + - file_name: "another_file.pwtwoti" + - folder_name: root + files: + - file_name: passwords + size: 663 + type: TXT + # pre installed services and applications + - hostname: client_3 + type: computer + ip_address: 192.168.10.23 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + start_up_duration: 0 + shut_down_duration: 0 + operating_state: "OFF" + # pre installed services and applications + + links: + - endpoint_a_hostname: switch_1 + endpoint_a_port: 1 + endpoint_b_hostname: client_1 + endpoint_b_port: 1 + bandwidth: 200 + - endpoint_a_hostname: switch_1 + endpoint_a_port: 2 + endpoint_b_hostname: client_2 + endpoint_b_port: 1 + bandwidth: 200 diff --git a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py index 4153adc0..627fc53b 100644 --- a/tests/integration_tests/configuration_file_parsing/test_game_options_config.py +++ b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py @@ -8,7 +8,7 @@ from primaite.config.load import data_manipulation_config_path from primaite.game.game import PrimaiteGame from tests import TEST_ASSETS_ROOT -BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" +BASIC_SWITCHED_NETWORK_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" def load_config(config_path: Union[str, Path]) -> PrimaiteGame: @@ -24,3 +24,42 @@ def test_thresholds(): game = load_config(data_manipulation_config_path()) assert game.options.thresholds is not None + + +def test_nmne_threshold(): + """Test that the NMNE thresholds are properly loaded in by observation.""" + game = load_config(BASIC_SWITCHED_NETWORK_CONFIG) + + assert game.options.thresholds["nmne"] is not None + + # get NIC observation + nic_obs = game.agents["defender"].observation_manager.obs.components["NODES"].hosts[0].nics[0] + assert nic_obs.low_nmne_threshold == 5 + assert nic_obs.med_nmne_threshold == 25 + assert nic_obs.high_nmne_threshold == 100 + + +def test_file_access_threshold(): + """Test that the NMNE thresholds are properly loaded in by observation.""" + game = load_config(BASIC_SWITCHED_NETWORK_CONFIG) + + assert game.options.thresholds["file_access"] is not None + + # get file observation + file_obs = game.agents["defender"].observation_manager.obs.components["NODES"].hosts[0].folders[0].files[0] + assert file_obs.low_file_access_threshold == 2 + assert file_obs.med_file_access_threshold == 5 + assert file_obs.high_file_access_threshold == 10 + + +def test_app_executions_threshold(): + """Test that the NMNE thresholds are properly loaded in by observation.""" + game = load_config(BASIC_SWITCHED_NETWORK_CONFIG) + + assert game.options.thresholds["app_executions"] is not None + + # get application observation + app_obs = game.agents["defender"].observation_manager.obs.components["NODES"].hosts[0].applications[0] + assert app_obs.low_app_execution_threshold == 2 + assert app_obs.med_app_execution_threshold == 3 + assert app_obs.high_app_execution_threshold == 5 diff --git a/tests/integration_tests/configuration_file_parsing/test_node_file_system_config.py b/tests/integration_tests/configuration_file_parsing/test_node_file_system_config.py new file mode 100644 index 00000000..4c99a39f --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/test_node_file_system_config.py @@ -0,0 +1,64 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from pathlib import Path +from typing import Union + +import yaml + +from primaite.game.game import PrimaiteGame +from primaite.simulator.file_system.file_type import FileType +from tests import TEST_ASSETS_ROOT + +BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/nodes_with_initial_files.yaml" + + +def load_config(config_path: Union[str, Path]) -> PrimaiteGame: + """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" + with open(config_path, "r") as f: + cfg = yaml.safe_load(f) + + return PrimaiteGame.from_config(cfg) + + +def test_node_file_system_from_config(): + """Test that the appropriate files are instantiated in nodes when loaded from config.""" + game = load_config(BASIC_CONFIG) + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + assert client_1.software_manager.software.get("database-service") # database service should be installed + assert client_1.file_system.get_file(folder_name="database", file_name="database.db") # database files should exist + + assert client_1.software_manager.software.get("web-server") # web server should be installed + assert client_1.file_system.get_file(folder_name="primaite", file_name="index.html") # web files should exist + + client_2 = game.simulation.network.get_node_by_hostname("client_2") + + # database service should not be installed + assert client_2.software_manager.software.get("database-service") is None + # database files should not exist + assert client_2.file_system.get_file(folder_name="database", file_name="database.db") is None + + # web server should not be installed + assert client_2.software_manager.software.get("web-server") is None + # web files should not exist + assert client_2.file_system.get_file(folder_name="primaite", file_name="index.html") is None + + empty_folder = client_2.file_system.get_folder(folder_name="empty_folder") + assert empty_folder + assert len(empty_folder.files) == 0 # should have no files + + password_file = client_2.file_system.get_file(folder_name="root", file_name="passwords.txt") + assert password_file # should exist + assert password_file.file_type is FileType.TXT + assert password_file.size == 663 + + downloads_folder = client_2.file_system.get_folder(folder_name="downloads") + assert downloads_folder # downloads folder should exist + + test_txt = downloads_folder.get_file(file_name="test.txt") + assert test_txt # test.txt should exist + assert test_txt.file_type is FileType.TXT + + unknown_file_type = downloads_folder.get_file(file_name="another_file.pwtwoti") + assert unknown_file_type # unknown_file_type should exist + assert unknown_file_type.file_type is FileType.UNKNOWN diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index d9599618..5c202ed2 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -49,7 +49,7 @@ class GigaSwitch(NetworkNode, discriminator="gigaswitch"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Switch Ports" + table.title = f"{self.config.hostname} Switch Ports" for port_num, port in self.network_interface.items(): table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) print(table) diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index c39d8263..3ee97fb7 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -106,7 +106,6 @@ def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame "username": "user123", "current_password": "password", "new_password": "different_password", - "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) @@ -146,7 +145,6 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam "username": "user123", "current_password": "password", "new_password": "different_password", - "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) @@ -166,3 +164,55 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam assert server_1.file_system.get_folder("folder123") is None assert server_1.file_system.get_file("folder123", "doggo.pdf") is None + + +def test_local_terminal(game_and_agent_fixture: Tuple[PrimaiteGame, ProxyAgent]): + game, agent = game_and_agent_fixture + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + # create a new user account on server_1 that will be logged into remotely + client_1_usm: UserManager = client_1.software_manager.software["user-manager"] + client_1_usm.add_user("user123", "password", is_admin=True) + + action = ( + "node-send-local-command", + { + "node_name": "client_1", + "username": "user123", + "password": "password", + "command": ["file_system", "create", "file", "folder123", "doggo.pdf", False], + }, + ) + agent.store_action(action) + game.step() + + assert client_1.file_system.get_folder("folder123") + assert client_1.file_system.get_file("folder123", "doggo.pdf") + + # Change password + action = ( + "node-account-change-password", + { + "node_name": "client_1", + "username": "user123", + "current_password": "password", + "new_password": "different_password", + }, + ) + agent.store_action(action) + game.step() + + action = ( + "node-send-local-command", + { + "node_name": "client_1", + "username": "user123", + "password": "password", + "command": ["file_system", "create", "file", "folder123", "cat.pdf", False], + }, + ) + agent.store_action(action) + game.step() + + assert client_1.file_system.get_file("folder123", "cat.pdf") is None + client_1.session_manager.show() diff --git a/tests/integration_tests/game_layer/actions/test_user_account_actions.py b/tests/integration_tests/game_layer/actions/test_user_account_actions.py new file mode 100644 index 00000000..26b871db --- /dev/null +++ b/tests/integration_tests/game_layer/actions/test_user_account_actions.py @@ -0,0 +1,176 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import pytest + +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.utils.validation.port import Port, PORT_LOOKUP + + +@pytest.fixture +def game_and_agent_fixture(game_and_agent): + """Create a game with a simple agent that can be controlled by the tests.""" + game, agent = game_and_agent + + client_1: Computer = game.simulation.network.get_node_by_hostname("client_1") + client_1.start_up_duration = 3 + + return (game, agent) + + +def test_user_account_add_user_action(game_and_agent_fixture): + """Tests the add user account action.""" + game, agent = game_and_agent_fixture + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + assert len(client_1.user_manager.users) == 1 # admin is created by default + assert len(client_1.user_manager.admins) == 1 + + # add admin account + action = ( + "node-account-add-user", + {"node_name": "client_1", "username": "admin_2", "password": "e-tronic-boogaloo", "is_admin": True}, + ) + agent.store_action(action) + game.step() + + assert len(client_1.user_manager.users) == 2 # new user added + assert len(client_1.user_manager.admins) == 2 + + # add non admin account + action = ( + "node-account-add-user", + {"node_name": "client_1", "username": "leeroy.jenkins", "password": "no_plan_needed", "is_admin": False}, + ) + agent.store_action(action) + game.step() + + assert len(client_1.user_manager.users) == 3 # new user added + assert len(client_1.user_manager.admins) == 2 + + +def test_user_account_disable_user_action(game_and_agent_fixture): + """Tests the disable user account action.""" + game, agent = game_and_agent_fixture + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + client_1.user_manager.add_user(username="test", password="password", is_admin=True) + assert len(client_1.user_manager.users) == 2 # new user added + assert len(client_1.user_manager.admins) == 2 + + test_user = client_1.user_manager.users.get("test") + assert test_user + assert test_user.disabled is not True + + # disable test account + action = ( + "node-account-disable-user", + { + "node_name": "client_1", + "username": "test", + }, + ) + agent.store_action(action) + game.step() + assert test_user.disabled + + +def test_user_account_change_password_action(game_and_agent_fixture): + """Tests the change password user account action.""" + game, agent = game_and_agent_fixture + client_1 = game.simulation.network.get_node_by_hostname("client_1") + + client_1.user_manager.add_user(username="test", password="password", is_admin=True) + + test_user = client_1.user_manager.users.get("test") + assert test_user.password == "password" + + # change account password + action = ( + "node-account-change-password", + {"node_name": "client_1", "username": "test", "current_password": "password", "new_password": "2Hard_2_Hack"}, + ) + agent.store_action(action) + game.step() + + assert test_user.password == "2Hard_2_Hack" + + +def test_user_account_create_terminal_action(game_and_agent_fixture): + """Tests that agents can use the terminal to create new users.""" + game, agent = game_and_agent_fixture + + router = game.simulation.network.get_node_by_hostname("router") + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=4) + + server_1 = game.simulation.network.get_node_by_hostname("server_1") + server_1_usm = server_1.software_manager.software["user-manager"] + server_1_usm.add_user("user123", "password", is_admin=True) + + action = ( + "node-session-remote-login", + { + "node_name": "client_1", + "username": "user123", + "password": "password", + "remote_ip": str(server_1.network_interface[1].ip_address), + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + # Create a new user account via terminal. + action = ( + "node-send-remote-command", + { + "node_name": "client_1", + "remote_ip": str(server_1.network_interface[1].ip_address), + "command": ["service", "user-manager", "add_user", "new_user", "new_pass", True], + }, + ) + agent.store_action(action) + game.step() + new_user = server_1.user_manager.users.get("new_user") + assert new_user + assert new_user.password == "new_pass" + assert new_user.disabled is not True + + +def test_user_account_disable_terminal_action(game_and_agent_fixture): + """Tests that agents can use the terminal to disable users.""" + game, agent = game_and_agent_fixture + router = game.simulation.network.get_node_by_hostname("router") + router.acl.add_rule(action=ACLAction.PERMIT, src_port=PORT_LOOKUP["SSH"], dst_port=PORT_LOOKUP["SSH"], position=4) + + server_1 = game.simulation.network.get_node_by_hostname("server_1") + server_1_usm = server_1.software_manager.software["user-manager"] + server_1_usm.add_user("user123", "password", is_admin=True) + + action = ( + "node-session-remote-login", + { + "node_name": "client_1", + "username": "user123", + "password": "password", + "remote_ip": str(server_1.network_interface[1].ip_address), + }, + ) + agent.store_action(action) + game.step() + assert agent.history[-1].response.status == "success" + + # Disable a user via terminal + action = ( + "node-send-remote-command", + { + "node_name": "client_1", + "remote_ip": str(server_1.network_interface[1].ip_address), + "command": ["service", "user-manager", "disable_user", "user123"], + }, + ) + agent.store_action(action) + game.step() + + new_user = server_1.user_manager.users.get("user123") + assert new_user + assert new_user.disabled is True diff --git a/tests/integration_tests/game_layer/observations/test_file_system_observations.py b/tests/integration_tests/game_layer/observations/test_file_system_observations.py index 7323461c..722fd294 100644 --- a/tests/integration_tests/game_layer/observations/test_file_system_observations.py +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -44,6 +44,38 @@ def test_file_observation(simulation): assert observation_state.get("health_status") == 3 # corrupted +def test_config_file_access_categories(simulation): + pc: Computer = simulation.network.get_node_by_hostname("client_1") + file_obs = FileObservation( + where=["network", "nodes", pc.config.hostname, "file_system", "folders", "root", "files", "dog.png"], + include_num_access=False, + file_system_requires_scan=True, + thresholds={"file_access": {"low": 3, "medium": 6, "high": 9}}, + ) + + assert file_obs.high_file_access_threshold == 9 + assert file_obs.med_file_access_threshold == 6 + assert file_obs.low_file_access_threshold == 3 + + with pytest.raises(Exception): + # should throw an error + FileObservation( + where=["network", "nodes", pc.config.hostname, "file_system", "folders", "root", "files", "dog.png"], + include_num_access=False, + file_system_requires_scan=True, + thresholds={"file_access": {"low": 9, "medium": 6, "high": 9}}, + ) + + with pytest.raises(Exception): + # should throw an error + FileObservation( + where=["network", "nodes", pc.config.hostname, "file_system", "folders", "root", "files", "dog.png"], + include_num_access=False, + file_system_requires_scan=True, + thresholds={"file_access": {"low": 3, "medium": 9, "high": 9}}, + ) + + def test_folder_observation(simulation): """Test the folder observation.""" pc: Computer = simulation.network.get_node_by_hostname("client_1") diff --git a/tests/integration_tests/game_layer/observations/test_nic_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py index b5e5ca81..30eccb06 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -77,6 +77,14 @@ def test_nic(simulation): nic_obs = NICObservation(where=["network", "nodes", pc.config.hostname, "NICs", 1], include_nmne=True) + # The Simulation object created by the fixture also creates the + # NICObservation class with the NICObservation.capture_nmnme class variable + # set to False. Under normal (non-test) circumstances this class variable + # is set from a config file such as data_manipulation.yaml. So although + # capture_nmne is set to True in the NetworkInterface class it's still False + # in the NICObservation class so we set it now. + nic_obs.capture_nmne = True + # Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs nmne_config = { "capture_nmne": True, # Enable the capture of MNEs @@ -115,14 +123,11 @@ def test_nic_categories(simulation): assert nic_obs.low_nmne_threshold == 0 # default -@pytest.mark.skip(reason="Feature not implemented yet") def test_config_nic_categories(simulation): pc: Computer = simulation.network.get_node_by_hostname("client_1") nic_obs = NICObservation( - where=["network", "nodes", pc.hostname, "NICs", 1], - low_nmne_threshold=3, - med_nmne_threshold=6, - high_nmne_threshold=9, + where=["network", "nodes", pc.config.hostname, "NICs", 1], + thresholds={"nmne": {"low": 3, "medium": 6, "high": 9}}, include_nmne=True, ) @@ -133,20 +138,16 @@ def test_config_nic_categories(simulation): with pytest.raises(Exception): # should throw an error NICObservation( - where=["network", "nodes", pc.hostname, "NICs", 1], - low_nmne_threshold=9, - med_nmne_threshold=6, - high_nmne_threshold=9, + where=["network", "nodes", pc.config.hostname, "NICs", 1], + thresholds={"nmne": {"low": 9, "medium": 6, "high": 9}}, include_nmne=True, ) with pytest.raises(Exception): # should throw an error NICObservation( - where=["network", "nodes", pc.hostname, "NICs", 1], - low_nmne_threshold=3, - med_nmne_threshold=9, - high_nmne_threshold=9, + where=["network", "nodes", pc.config.hostname, "NICs", 1], + thresholds={"nmne": {"low": 3, "medium": 9, "high": 9}}, include_nmne=True, ) diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py index 09eb3fe4..aef60bc2 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -39,6 +39,8 @@ def test_host_observation(simulation): folders=[], network_interfaces=[], file_system_requires_scan=True, + services_requires_scan=True, + applications_requires_scan=True, include_users=False, ) diff --git a/tests/integration_tests/game_layer/observations/test_obs_data_capture.py b/tests/integration_tests/game_layer/observations/test_obs_data_capture.py new file mode 100644 index 00000000..e8bdea22 --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_obs_data_capture.py @@ -0,0 +1,28 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import json + +from primaite.session.environment import PrimaiteGymEnv +from primaite.session.io import PrimaiteIO +from tests import TEST_ASSETS_ROOT + +DATA_MANIPULATION_CONFIG = TEST_ASSETS_ROOT / "configs" / "data_manipulation.yaml" + + +def test_obs_data_in_log_file(): + """Create a log file of AgentHistoryItems and check observation data is + included. Assumes that data_manipulation.yaml has an agent labelled + 'defender' with a non-null observation space. + The log file will be in: + primaite/VERSION/sessions/YYYY-MM-DD/HH-MM-SS/agent_actions + """ + env = PrimaiteGymEnv(DATA_MANIPULATION_CONFIG) + env.reset() + for _ in range(10): + env.step(0) + env.reset() + io = PrimaiteIO() + path = io.generate_agent_actions_save_path(episode=1) + with open(path, "r") as f: + j = json.load(f) + + assert type(j["0"]["defender"]["observation"]) == dict diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py index 28cdaf01..1ebff10c 100644 --- a/tests/integration_tests/game_layer/observations/test_software_observations.py +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -29,7 +29,9 @@ def test_service_observation(simulation): ntp_server = pc.software_manager.software.get("ntp-server") assert ntp_server - service_obs = ServiceObservation(where=["network", "nodes", pc.config.hostname, "services", "ntp-server"]) + service_obs = ServiceObservation( + where=["network", "nodes", pc.config.hostname, "services", "ntp-server"], services_requires_scan=True + ) assert service_obs.space["operating_status"] == spaces.Discrete(7) assert service_obs.space["health_status"] == spaces.Discrete(5) @@ -54,7 +56,9 @@ def test_application_observation(simulation): web_browser: WebBrowser = pc.software_manager.software.get("web-browser") assert web_browser - app_obs = ApplicationObservation(where=["network", "nodes", pc.config.hostname, "applications", "web-browser"]) + app_obs = ApplicationObservation( + where=["network", "nodes", pc.config.hostname, "applications", "web-browser"], applications_requires_scan=True + ) web_browser.close() observation_state = app_obs.observe(simulation.describe_state()) @@ -69,3 +73,33 @@ def test_application_observation(simulation): assert observation_state.get("health_status") == 1 assert observation_state.get("operating_status") == 1 # running assert observation_state.get("num_executions") == 1 + + +def test_application_executions_categories(simulation): + pc: Computer = simulation.network.get_node_by_hostname("client_1") + + app_obs = ApplicationObservation( + where=["network", "nodes", pc.config.hostname, "applications", "WebBrowser"], + applications_requires_scan=False, + thresholds={"app_executions": {"low": 3, "medium": 6, "high": 9}}, + ) + + assert app_obs.high_app_execution_threshold == 9 + assert app_obs.med_app_execution_threshold == 6 + assert app_obs.low_app_execution_threshold == 3 + + with pytest.raises(Exception): + # should throw an error + ApplicationObservation( + where=["network", "nodes", pc.config.hostname, "applications", "WebBrowser"], + applications_requires_scan=False, + thresholds={"app_executions": {"low": 9, "medium": 6, "high": 9}}, + ) + + with pytest.raises(Exception): + # should throw an error + ApplicationObservation( + where=["network", "nodes", pc.config.hostname, "applications", "WebBrowser"], + applications_requires_scan=False, + thresholds={"app_executions": {"low": 3, "medium": 9, "high": 9}}, + ) diff --git a/tests/integration_tests/game_layer/test_RNG_seed.py b/tests/integration_tests/game_layer/test_RNG_seed.py index 45fa445d..2b80e153 100644 --- a/tests/integration_tests/game_layer/test_RNG_seed.py +++ b/tests/integration_tests/game_layer/test_RNG_seed.py @@ -7,6 +7,7 @@ import yaml from primaite.config.load import data_manipulation_config_path from primaite.game.agent.interface import AgentHistoryItem from primaite.session.environment import PrimaiteGymEnv +from primaite.simulator import SIM_OUTPUT @pytest.fixture() @@ -33,6 +34,11 @@ def test_rng_seed_set(create_env): assert a == b + # Check that seed log file was created. + path = SIM_OUTPUT.path / "seed.log" + with open(path, "r") as file: + assert file + def test_rng_seed_unset(create_env): """Test with no RNG seed.""" @@ -48,3 +54,19 @@ def test_rng_seed_unset(create_env): b = [item.timestep for item in env.game.agents["client_2_green_user"].history if item.action != "do-nothing"] assert a != b + + +def test_for_generated_seed(): + """ + Show that setting generate_seed_value to true producess a valid seed. + """ + with open(data_manipulation_config_path(), "r") as f: + cfg = yaml.safe_load(f) + + cfg["game"]["generate_seed_value"] = True + PrimaiteGymEnv(env_config=cfg) + path = SIM_OUTPUT.path / "seed.log" + with open(path, "r") as file: + data = file.read() + + assert data.split(" ")[3] != None diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 03b94ab7..59bee385 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -22,6 +22,7 @@ from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus from primaite.simulator.network.hardware.nodes.network.firewall import Firewall +from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.software import SoftwareHealthState @@ -107,7 +108,7 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox """ Test that the RouterACLAddRuleAction can form a request and that it is accepted by the simulation. - The acl starts off with 4 rules, and we add a rule, and check that the acl now has 5 rules. + The ACL starts off with 4 rules, and we add a rule, and check that the ACL now has 5 rules. """ game, agent = game_and_agent @@ -164,11 +165,9 @@ def test_router_acl_addrule_integration(game_and_agent: Tuple[PrimaiteGame, Prox }, ) agent.store_action(action) - print(agent.most_recent_action) game.step() - print(agent.most_recent_action) + # 5: Check that the ACL now has 6 rules, but that server_1 can still ping server_2 - print(router.acl.show()) assert router.acl.num_rules == 6 assert server_1.ping("10.0.2.3") # Can ping server_2 @@ -180,7 +179,8 @@ def test_router_acl_removerule_integration(game_and_agent: Tuple[PrimaiteGame, P # 1: Check that http traffic is going across the network nicely. client_1 = game.simulation.network.get_node_by_hostname("client_1") server_1 = game.simulation.network.get_node_by_hostname("server_1") - router = game.simulation.network.get_node_by_hostname("router") + router: Router = game.simulation.network.get_node_by_hostname("router") + assert router.acl.num_rules == 4 browser: WebBrowser = client_1.software_manager.software.get("web-browser") browser.run() diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index ea7fbc99..80e7c3b3 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,5 +1,11 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +from itertools import product + +import yaml + +from primaite.config.load import data_manipulation_config_path from primaite.game.agent.observations.nic_observations import NICObservation +from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server @@ -277,3 +283,19 @@ def test_capture_nmne_observations(uc2_network: Network): assert web_nic_obs["outbound"] == expected_nmne assert db_nic_obs["inbound"] == expected_nmne uc2_network.apply_timestep(timestep=0) + + +def test_nmne_parameter_settings(): + """ + Check that the four permutations of the values of capture_nmne and + include_nmne work as expected. + """ + + with open(data_manipulation_config_path(), "r") as f: + cfg = yaml.safe_load(f) + + DEFENDER = 3 + for capture, include in product([True, False], [True, False]): + cfg["simulation"]["network"]["nmne_config"]["capture_nmne"] = capture + cfg["agents"][DEFENDER]["observation_space"]["options"]["components"][0]["options"]["include_nmne"] = include + PrimaiteGymEnv(env_config=cfg) diff --git a/tests/integration_tests/system/test_arp.py b/tests/integration_tests/system/test_arp.py index 055d58c6..b9a92255 100644 --- a/tests/integration_tests/system/test_arp.py +++ b/tests/integration_tests/system/test_arp.py @@ -1,6 +1,7 @@ -# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK -from primaite.simulator.network.hardware.nodes.network.router import RouterARP +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router, RouterARP from primaite.simulator.system.services.arp.arp import ARP +from primaite.utils.validation.port import PORT_LOOKUP from tests.integration_tests.network.test_routing import multi_hop_network @@ -48,3 +49,19 @@ def test_arp_fails_for_network_address_between_routers(multi_hop_network): actual_result = router_1_arp.get_arp_cache_mac_address(router_1.network_interface[1].ip_network.network_address) assert actual_result == expected_result + + +def test_arp_not_affected_by_acl(multi_hop_network): + pc_a = multi_hop_network.get_node_by_hostname("pc_a") + router_1: Router = multi_hop_network.get_node_by_hostname("router_1") + + # Add explicit rule to block ARP traffic. This shouldn't actually stop ARP traffic + # as it operates a different layer within the network. + router_1.acl.add_rule(action=ACLAction.DENY, src_port=PORT_LOOKUP["ARP"], dst_port=PORT_LOOKUP["ARP"], position=23) + + pc_a_arp: ARP = pc_a.software_manager.arp + + expected_result = router_1.network_interface[2].mac_address + actual_result = pc_a_arp.get_arp_cache_mac_address(router_1.network_interface[2].ip_address) + + assert actual_result == expected_result diff --git a/tests/unit_tests/_primaite/_game/_agent/test_observations.py b/tests/unit_tests/_primaite/_game/_agent/test_observations.py index 5d5921a9..3df6ca0a 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_observations.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_observations.py @@ -1,10 +1,11 @@ # © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK +import json from typing import List import pytest import yaml -from primaite.game.agent.observations import ObservationManager +from primaite.game.agent.observations import ApplicationObservation, ObservationManager, ServiceObservation from primaite.game.agent.observations.file_system_observations import FileObservation, FolderObservation from primaite.game.agent.observations.host_observations import HostObservation @@ -136,3 +137,227 @@ class TestFileSystemRequiresScan: [], files=[], num_files=0, include_num_access=False, file_system_requires_scan=True ) assert obs_requiring_scan.observe(folder_state)["health_status"] == 1 + + +class TestServicesRequiresScan: + @pytest.mark.parametrize( + ("yaml_option_string", "expected_val"), + ( + ("services_requires_scan: true", True), + ("services_requires_scan: false", False), + (" ", True), + ), + ) + def test_obs_config(self, yaml_option_string, expected_val): + """Check that the default behaviour is to set service_requires_scan to True.""" + obs_cfg_yaml = f""" + type: custom + options: + components: + - type: nodes + label: NODES + options: + hosts: + - hostname: domain_controller + - hostname: web_server + services: + - service_name: web-server + - service_name: dns-client + - hostname: database_server + folders: + - folder_name: database + files: + - file_name: database.db + - hostname: backup_server + services: + - service_name: ftp-server + - hostname: security_suite + - hostname: client_1 + - hostname: client_2 + num_services: 3 + num_applications: 0 + num_folders: 1 + num_files: 1 + num_nics: 2 + include_num_access: false + {yaml_option_string} + include_nmne: true + monitored_traffic: + icmp: + - NONE + tcp: + - DNS + routers: + - hostname: router_1 + num_ports: 0 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + wildcard_list: + - 0.0.0.1 + port_list: + - 80 + - 5432 + protocol_list: + - ICMP + - TCP + - UDP + num_rules: 10 + + - type: links + label: LINKS + options: + link_references: + - router_1:eth-1<->switch_1:eth-8 + - router_1:eth-2<->switch_2:eth-8 + - switch_1:eth-1<->domain_controller:eth-1 + - switch_1:eth-2<->web_server:eth-1 + - switch_1:eth-3<->database_server:eth-1 + - switch_1:eth-4<->backup_server:eth-1 + - switch_1:eth-7<->security_suite:eth-1 + - switch_2:eth-1<->client_1:eth-1 + - switch_2:eth-2<->client_2:eth-1 + - switch_2:eth-7<->security_suite:eth-2 + - type: none + label: ICS + options: {{}} + + """ + + cfg = yaml.safe_load(obs_cfg_yaml) + manager = ObservationManager.from_config(cfg) + + hosts: List[HostObservation] = manager.obs.components["NODES"].hosts + for i, host in enumerate(hosts): + services: List[ServiceObservation] = host.services + for j, service in enumerate(services): + val = service.services_requires_scan + print(f"host {i} service {j} {val}") + assert val == expected_val # Make sure services require scan by default + + def test_services_requires_scan(self): + state = {"health_state_actual": 3, "health_state_visible": 1, "operating_state": 1} + + obs_requiring_scan = ServiceObservation([], services_requires_scan=True) + assert obs_requiring_scan.observe(state)["health_status"] == 1 # should be visible value + + obs_not_requiring_scan = ServiceObservation([], services_requires_scan=False) + assert obs_not_requiring_scan.observe(state)["health_status"] == 3 # should be actual value + + +class TestApplicationsRequiresScan: + @pytest.mark.parametrize( + ("yaml_option_string", "expected_val"), + ( + ("applications_requires_scan: true", True), + ("applications_requires_scan: false", False), + (" ", True), + ), + ) + def test_obs_config(self, yaml_option_string, expected_val): + """Check that the default behaviour is to set applications_requires_scan to True.""" + obs_cfg_yaml = f""" + type: custom + options: + components: + - type: nodes + label: NODES + options: + hosts: + - hostname: domain_controller + - hostname: web_server + - hostname: database_server + folders: + - folder_name: database + files: + - file_name: database.db + - hostname: backup_server + - hostname: security_suite + - hostname: client_1 + applications: + - application_name: web-browser + - hostname: client_2 + applications: + - application_name: web-browser + - application_name: database-client + num_services: 0 + num_applications: 3 + num_folders: 1 + num_files: 1 + num_nics: 2 + include_num_access: false + {yaml_option_string} + include_nmne: true + monitored_traffic: + icmp: + - NONE + tcp: + - DNS + routers: + - hostname: router_1 + num_ports: 0 + ip_list: + - 192.168.1.10 + - 192.168.1.12 + - 192.168.1.14 + - 192.168.1.16 + - 192.168.1.110 + - 192.168.10.21 + - 192.168.10.22 + - 192.168.10.110 + wildcard_list: + - 0.0.0.1 + port_list: + - 80 + - 5432 + protocol_list: + - ICMP + - TCP + - UDP + num_rules: 10 + + - type: links + label: LINKS + options: + link_references: + - router_1:eth-1<->switch_1:eth-8 + - router_1:eth-2<->switch_2:eth-8 + - switch_1:eth-1<->domain_controller:eth-1 + - switch_1:eth-2<->web_server:eth-1 + - switch_1:eth-3<->database_server:eth-1 + - switch_1:eth-4<->backup_server:eth-1 + - switch_1:eth-7<->security_suite:eth-1 + - switch_2:eth-1<->client_1:eth-1 + - switch_2:eth-2<->client_2:eth-1 + - switch_2:eth-7<->security_suite:eth-2 + - type: none + label: ICS + options: {{}} + + """ + + cfg = yaml.safe_load(obs_cfg_yaml) + manager = ObservationManager.from_config(cfg) + + hosts: List[HostObservation] = manager.obs.components["NODES"].hosts + for i, host in enumerate(hosts): + services: List[ServiceObservation] = host.services + for j, service in enumerate(services): + val = service.services_requires_scan + print(f"host {i} service {j} {val}") + assert val == expected_val # Make sure applications require scan by default + + def test_applications_requires_scan(self): + state = {"health_state_actual": 3, "health_state_visible": 1, "operating_state": 1, "num_executions": 1} + + obs_requiring_scan = ApplicationObservation([], applications_requires_scan=True) + assert obs_requiring_scan.observe(state)["health_status"] == 1 # should be visible value + + obs_not_requiring_scan = ApplicationObservation([], applications_requires_scan=False) + assert obs_not_requiring_scan.observe(state)["health_status"] == 3 # should be actual value diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 91369f6c..81e05467 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -73,7 +73,7 @@ def test_ftp_should_not_process_commands_if_service_not_running(ftp_client): assert ftp_client_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR -def test_ftp_tries_to_senf_file__that_does_not_exist(ftp_client): +def test_ftp_tries_to_send_file__that_does_not_exist(ftp_client): """Method send_file should return false if no file to send.""" assert ftp_client.file_system.get_file(folder_name="root", file_name="test.txt") is None diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py index 32fcae9a..3b2377e9 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_terminal.py @@ -6,6 +6,7 @@ import pytest from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame +from primaite.interface.request import RequestResponse from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server @@ -442,3 +443,59 @@ def test_terminal_connection_timeout(basic_network): assert len(computer_b.user_session_manager.remote_sessions) == 0 assert not remote_connection.is_active + + +def test_terminal_last_response_updates(basic_network): + """Test that the _last_response within Terminal correctly updates.""" + network: Network = basic_network + computer_a: Computer = network.get_node_by_hostname("node_a") + terminal_a: Terminal = computer_a.software_manager.software.get("terminal") + computer_b: Computer = network.get_node_by_hostname("node_b") + + assert terminal_a.last_response is None + + remote_connection = terminal_a.login(username="admin", password="admin", ip_address="192.168.0.11") + + # Last response should be a successful logon + assert terminal_a.last_response == RequestResponse(status="success", data={"reason": "Login Successful"}) + + remote_connection.execute(command=["software_manager", "application", "install", "ransomware-script"]) + + # Last response should now update following successful install + assert terminal_a.last_response == RequestResponse(status="success", data={}) + + remote_connection.execute(command=["software_manager", "application", "install", "ransomware-script"]) + + # Last response should now update to success, but with supplied reason. + assert terminal_a.last_response == RequestResponse(status="success", data={"reason": "already installed"}) + + remote_connection.execute(command=["file_system", "create", "file", "folder123", "doggo.pdf", False]) + + # Check file was created. + assert computer_b.file_system.access_file(folder_name="folder123", file_name="doggo.pdf") + + # Last response should be confirmation of file creation. + assert terminal_a.last_response == RequestResponse( + status="success", + data={"file_name": "doggo.pdf", "folder_name": "folder123", "file_type": "PDF", "file_size": 102400}, + ) + + remote_connection.execute( + command=[ + "service", + "ftp-client", + "send", + { + "dest_ip_address": "192.168.0.2", + "src_folder": "folder123", + "src_file_name": "cat.pdf", + "dest_folder": "root", + "dest_file_name": "cat.pdf", + }, + ] + ) + + assert terminal_a.last_response == RequestResponse( + status="failure", + data={"reason": "Unable to locate given file on local file system. Perhaps given options are invalid?"}, + ) From 56e81a020ca31c8a8770d7a737f8008166474b50 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 11 Feb 2025 10:53:41 +0000 Subject: [PATCH 196/224] #3060 - Adding version to YAML files within PrimAITE --- .../config/_package_data/basic_lan_network_example.yaml | 2 ++ .../_package_data/client_server_p2p_network_example.yaml | 2 ++ src/primaite/config/_package_data/data_manipulation.yaml | 2 ++ .../config/_package_data/data_manipulation_marl.yaml | 2 ++ .../base_scenario.yaml | 2 ++ .../_package_data/multi_lan_internet_network_example.yaml | 2 ++ .../_package_data/scenario_with_placeholders/scenario.yaml | 2 ++ src/primaite/game/game.py | 5 +++++ tests/assets/configs/action_penalty.yaml | 2 ++ tests/assets/configs/bad_primaite_session.yaml | 2 ++ tests/assets/configs/basic_c2_setup.yaml | 2 ++ tests/assets/configs/basic_firewall.yaml | 1 + .../configs/basic_node_with_software_listening_ports.yaml | 2 ++ tests/assets/configs/basic_node_with_users.yaml | 2 ++ tests/assets/configs/basic_switched_network.yaml | 2 ++ tests/assets/configs/data_manipulation.yaml | 2 ++ tests/assets/configs/dmz_network.yaml | 2 ++ tests/assets/configs/eval_only_primaite_session.yaml | 2 ++ tests/assets/configs/extended_config.yaml | 2 ++ tests/assets/configs/firewall_actions_network.yaml | 2 ++ tests/assets/configs/fixing_duration_one_item.yaml | 2 ++ tests/assets/configs/install_and_configure_apps.yaml | 2 ++ tests/assets/configs/multi_agent_session.yaml | 2 ++ .../configs/nmap_network_service_recon_red_agent_config.yaml | 2 ++ tests/assets/configs/nmap_ping_scan_red_agent_config.yaml | 2 ++ tests/assets/configs/nmap_port_scan_red_agent_config.yaml | 2 ++ tests/assets/configs/no_nodes_links_agents_network.yaml | 2 ++ .../assets/configs/scenario_with_placeholders/scenario.yaml | 2 ++ tests/assets/configs/shared_rewards.yaml | 2 ++ tests/assets/configs/software_fixing_duration.yaml | 2 ++ tests/assets/configs/test_application_install.yaml | 2 ++ tests/assets/configs/test_primaite_session.yaml | 2 ++ tests/assets/configs/wireless_wan_network_config.yaml | 2 ++ .../wireless_wan_network_config_freq_max_override.yaml | 2 ++ ...ireless_wan_network_config_freq_max_override_blocked.yaml | 2 ++ 35 files changed, 72 insertions(+) diff --git a/src/primaite/config/_package_data/basic_lan_network_example.yaml b/src/primaite/config/_package_data/basic_lan_network_example.yaml index 9490ff00..854dfcf3 100644 --- a/src/primaite/config/_package_data/basic_lan_network_example.yaml +++ b/src/primaite/config/_package_data/basic_lan_network_example.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: ports: - ARP diff --git a/src/primaite/config/_package_data/client_server_p2p_network_example.yaml b/src/primaite/config/_package_data/client_server_p2p_network_example.yaml index 798dd318..702c864f 100644 --- a/src/primaite/config/_package_data/client_server_p2p_network_example.yaml +++ b/src/primaite/config/_package_data/client_server_p2p_network_example.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: ports: - ARP diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 4705c135..72b3f671 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index 0263edb0..178b3322 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml index 4c7734d8..9530305a 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: max_episode_length: 128 ports: [] diff --git a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml index cbd1c01e..4e8f085e 100644 --- a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml +++ b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: ports: - ARP diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index 1a96e3a0..d34fb310 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 1427776e..639b37cf 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -247,6 +247,11 @@ class PrimaiteGame: game.options = PrimaiteGameOptions(**cfg["game"]) game.save_step_metadata = cfg.get("io_settings", {}).get("save_step_metadata") or False + # TODO: Future YAML config should specify the PrimAITE version they are written for. + # For now, we warn that if it is missing, pending a mechanism to handle variations. + if not cfg["version"]: + _LOGGER.warning("Version definition is missing from provided configuration. ") + # 1. create simulation sim = game.simulation net = sim.network diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 9c117dfe..9561db4f 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: false save_step_metadata: false diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 18b37466..1f59225b 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: ports: - ARP diff --git a/tests/assets/configs/basic_c2_setup.yaml b/tests/assets/configs/basic_c2_setup.yaml index ac2b026e..3011457f 100644 --- a/tests/assets/configs/basic_c2_setup.yaml +++ b/tests/assets/configs/basic_c2_setup.yaml @@ -4,6 +4,8 @@ # | node_a |------| switch_1 |------| node_b | # -------------- -------------- -------------- # +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 8108ecbe..1aa1361a 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -4,6 +4,7 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # +version: 3.0 io_settings: save_step_metadata: 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 index 7b04196b..b503d66d 100644 --- a/tests/assets/configs/basic_node_with_software_listening_ports.yaml +++ b/tests/assets/configs/basic_node_with_software_listening_ports.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/basic_node_with_users.yaml b/tests/assets/configs/basic_node_with_users.yaml index 064519dd..3009b79e 100644 --- a/tests/assets/configs/basic_node_with_users.yaml +++ b/tests/assets/configs/basic_node_with_users.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index c9ac5f8d..7e8a5715 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -4,6 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index e9464f0f..baf9df7d 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 2aa472d8..456920f1 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -30,6 +30,8 @@ # | external_computer |------| switch_3 |------| external_server | # ----------------------- -------------- --------------------- # +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 3403a7ff..c0a61b9d 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: ports: - ARP diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index 8bf7d506..1b0772c1 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index eb39aa1a..b624be52 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -30,6 +30,8 @@ # | external_computer |------| switch_3 |------| external_server | # ----------------------- -------------- --------------------- # +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/fixing_duration_one_item.yaml b/tests/assets/configs/fixing_duration_one_item.yaml index ff3a6504..ad94a9da 100644 --- a/tests/assets/configs/fixing_duration_one_item.yaml +++ b/tests/assets/configs/fixing_duration_one_item.yaml @@ -4,6 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index 6b80519c..4f96496e 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: false diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index e99ea49b..263d5f66 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: false save_step_metadata: false diff --git a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml index 9b49d466..6abb11ce 100644 --- a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml +++ b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index e3cb33e1..869f4fb8 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 2692a3bc..1d5ab323 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/no_nodes_links_agents_network.yaml b/tests/assets/configs/no_nodes_links_agents_network.yaml index b20835bc..b79e6bc6 100644 --- a/tests/assets/configs/no_nodes_links_agents_network.yaml +++ b/tests/assets/configs/no_nodes_links_agents_network.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index ab8c968c..76b51626 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 07256f01..68b3b310 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: false save_step_metadata: false diff --git a/tests/assets/configs/software_fixing_duration.yaml b/tests/assets/configs/software_fixing_duration.yaml index 63409f2b..2f5613e1 100644 --- a/tests/assets/configs/software_fixing_duration.yaml +++ b/tests/assets/configs/software_fixing_duration.yaml @@ -4,6 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # +version: 3.0 + io_settings: save_step_metadata: false save_pcap_logs: true diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index 0266dd00..d49d9bf2 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: false diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 65d16a47..c1206634 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -1,3 +1,5 @@ +version: 3.0 + io_settings: save_agent_actions: true save_step_metadata: true diff --git a/tests/assets/configs/wireless_wan_network_config.yaml b/tests/assets/configs/wireless_wan_network_config.yaml index 40a9126c..b722e50f 100644 --- a/tests/assets/configs/wireless_wan_network_config.yaml +++ b/tests/assets/configs/wireless_wan_network_config.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: max_episode_length: 256 ports: diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml index 13ceee16..4c2f4f86 100644 --- a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: max_episode_length: 256 ports: diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml index 6c52fd95..a98b6ee2 100644 --- a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml @@ -1,3 +1,5 @@ +version: 3.0 + game: max_episode_length: 256 ports: From dadf93bc7fd70e459f525d27c429b6514932614b Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 11 Feb 2025 14:59:25 +0000 Subject: [PATCH 197/224] #3060 - Address some missed pre-commit errors --- docs/source/how_to_guides/extensible_nodes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 043d0f06..4819cbb2 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -52,4 +52,4 @@ class Router(NetworkNode, identifier="router"): Changes to YAML file. ===================== -While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. +While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. From 70fc11f4f110e138c16c4a80030b408b3ecb7f15 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 11 Feb 2025 16:20:11 +0000 Subject: [PATCH 198/224] #3060 - Correct change in game.py that caused test failures --- src/primaite/game/game.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 639b37cf..4741199c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -247,9 +247,11 @@ class PrimaiteGame: game.options = PrimaiteGameOptions(**cfg["game"]) game.save_step_metadata = cfg.get("io_settings", {}).get("save_step_metadata") or False + config_version = cfg.get("version", {}) + # TODO: Future YAML config should specify the PrimAITE version they are written for. # For now, we warn that if it is missing, pending a mechanism to handle variations. - if not cfg["version"]: + if not isinstance(config_version, int): _LOGGER.warning("Version definition is missing from provided configuration. ") # 1. create simulation From 56699d2377a706c54648b54c5fb72456fbc1961d Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 12 Feb 2025 16:18:50 +0000 Subject: [PATCH 199/224] Resolve hardcoding of Agent Logger having agent_name as abstract_agent --- src/primaite/game/agent/interface.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index a55cd3ff..82edab88 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -70,7 +70,7 @@ class AbstractAgent(BaseModel, ABC): config: ConfigSchema = Field(default_factory=lambda: AbstractAgent.ConfigSchema()) - logger: AgentLog = AgentLog(agent_name="Abstract_Agent") + logger: AgentLog = None history: List[AgentHistoryItem] = [] action_manager: ActionManager = Field(default_factory=lambda: ActionManager()) @@ -79,6 +79,12 @@ class AbstractAgent(BaseModel, ABC): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} + def __init__(self, **kwargs): + """init""" + + super().__init__(**kwargs) + self.logger: AgentLog = AgentLog(agent_name=kwargs["config"]["ref"]) + def __init_subclass__(cls, discriminator: Optional[str] = None, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) if discriminator is None: From 7e138d1d61b9e1e2fad0bf5fd9e00ba4e4ea7701 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 14 Feb 2025 11:38:15 +0000 Subject: [PATCH 200/224] #Bug and test fixes --- src/primaite/game/agent/interface.py | 2 +- src/primaite/simulator/network/container.py | 65 ++++++++----------- .../simulator/network/hardware/base.py | 4 +- src/primaite/simulator/system/software.py | 2 + .../extensions/nodes/giga_switch.py | 2 +- .../actions/test_terminal_actions.py | 2 - .../_primaite/_game/_agent/test_agent.py | 2 +- 7 files changed, 34 insertions(+), 45 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 82edab88..571c850d 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -80,7 +80,7 @@ class AbstractAgent(BaseModel, ABC): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} def __init__(self, **kwargs): - """init""" + """Initialise and setup agent logger""" super().__init__(**kwargs) self.logger: AgentLog = AgentLog(agent_name=kwargs["config"]["ref"]) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 2e494910..1fa8c680 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -163,15 +163,6 @@ class Network(SimComponent): :param links: Include link details in the output. Defaults to True. :param markdown: Use Markdown style in table output. Defaults to False. """ - nodes_type_map = { - "Router": self.router_nodes, - "Firewall": self.firewall_nodes, - "Switch": self.switch_nodes, - "Server": self.server_nodes, - "Computer": self.computer_nodes, - "Printer": self.printer_nodes, - "Wireless Router": self.wireless_router_nodes, - } if nodes: table = PrettyTable(["Node", "Type", "Operating State"]) @@ -189,19 +180,18 @@ class Network(SimComponent): table.set_style(MARKDOWN) table.align = "l" table.title = "IP Addresses" - for nodes in nodes_type_map.values(): - for node in nodes: - for i, port in node.network_interface.items(): - if hasattr(port, "ip_address"): - if port.ip_address != IPv4Address("127.0.0.1"): - port_str = port.port_name if port.port_name else port.port_num - table.add_row( - [ - node.config.hostname, - port_str, - port.ip_address, - port.subnet_mask, - node.config.default_gateway, + for node in self.nodes.values(): + for i, port in node.network_interface.items(): + if hasattr(port, "ip_address"): + if port.ip_address != IPv4Address("127.0.0.1"): + port_str = port.port_name if port.port_name else port.port_num + table.add_row( + [ + node.config.hostname, + port_str, + port.ip_address, + port.subnet_mask, + node.config.default_gateway, ] ) print(table) @@ -215,22 +205,21 @@ class Network(SimComponent): table.align = "l" table.title = "Links" links = list(self.links.values()) - for nodes in nodes_type_map.values(): - for node in nodes: - for link in links[::-1]: - if node in [link.endpoint_a.parent, link.endpoint_b.parent]: - table.add_row( - [ - link.endpoint_a.parent.config.hostname, - str(link.endpoint_a), - link.endpoint_b.parent.config.hostname, - str(link.endpoint_b), - link.is_up, - link.bandwidth, - link.current_load_percent, - ] - ) - links.remove(link) + for node in self.nodes.values(): + for link in links[::-1]: + if node in [link.endpoint_a.parent, link.endpoint_b.parent]: + table.add_row( + [ + link.endpoint_a.parent.config.hostname, + str(link.endpoint_a), + link.endpoint_b.parent.config.hostname, + str(link.endpoint_b), + link.is_up, + link.bandwidth, + link.current_load_percent, + ] + ) + links.remove(link) print(table) def clear_links(self): diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 8653359a..4c252050 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1197,7 +1197,7 @@ class UserSessionManager(Service, discriminator="user-session-manager"): """Request should take the form [username, password, remote_ip_address].""" username, password, remote_ip_address = request response = RequestResponse.from_bool(self.remote_login(username, password, remote_ip_address)) - response.data = {"remote_hostname": self.parent.hostname, "username": username} + response.data = {"remote_hostname": self.parent.config.hostname, "username": username} return response rm.add_request("remote_login", RequestType(func=_remote_login)) @@ -1230,7 +1230,7 @@ class UserSessionManager(Service, discriminator="user-session-manager"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.parent.hostname} User Sessions" + table.title = f"{self.parent.config.hostname} User Sessions" def _add_session_to_table(user_session: UserSession): """ diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 950f77c6..42468057 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -20,6 +20,7 @@ from primaite.utils.validation.port import Port if TYPE_CHECKING: from primaite.simulator.system.core.software_manager import SoftwareManager + from primaite.simulator.network.hardware.base import Node class SoftwareType(Enum): @@ -110,6 +111,7 @@ class Software(SimComponent, ABC): "The folder on the file system the Software uses." _fixing_countdown: Optional[int] = None "Current number of ticks left to patch the software." + # parent: Optional[Node] = None def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/tests/integration_tests/extensions/nodes/giga_switch.py b/tests/integration_tests/extensions/nodes/giga_switch.py index d9599618..5c202ed2 100644 --- a/tests/integration_tests/extensions/nodes/giga_switch.py +++ b/tests/integration_tests/extensions/nodes/giga_switch.py @@ -49,7 +49,7 @@ class GigaSwitch(NetworkNode, discriminator="gigaswitch"): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.hostname} Switch Ports" + table.title = f"{self.config.hostname} Switch Ports" for port_num, port in self.network_interface.items(): table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) print(table) diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index c39d8263..6d30644c 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -106,7 +106,6 @@ def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame "username": "user123", "current_password": "password", "new_password": "different_password", - "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) @@ -146,7 +145,6 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam "username": "user123", "current_password": "password", "new_password": "different_password", - "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_agent.py index a2693591..064537c2 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent.py @@ -4,7 +4,7 @@ from primaite.game.agent.scripted_agents.random_agent import RandomAgent def test_creating_empty_agent(): - agent = RandomAgent() + agent = RandomAgent(config={"ref" :"Empty Agent"}) assert len(agent.action_manager.action_map) == 0 assert isinstance(agent.observation_manager.obs, NullObservation) assert len(agent.reward_function.reward_components) == 0 From 7b80d15d8131bc5203c0a34b676746d572700486 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 14 Feb 2025 11:39:25 +0000 Subject: [PATCH 201/224] # Pre-commit actions --- docs/source/how_to_guides/extensible_nodes.rst | 2 +- src/primaite/game/agent/interface.py | 3 +-- src/primaite/simulator/network/container.py | 5 ++--- src/primaite/simulator/system/software.py | 1 - tests/unit_tests/_primaite/_game/_agent/test_agent.py | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/source/how_to_guides/extensible_nodes.rst b/docs/source/how_to_guides/extensible_nodes.rst index 043d0f06..4819cbb2 100644 --- a/docs/source/how_to_guides/extensible_nodes.rst +++ b/docs/source/how_to_guides/extensible_nodes.rst @@ -52,4 +52,4 @@ class Router(NetworkNode, identifier="router"): Changes to YAML file. ===================== -While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. +While effort has been made to ensure that nodes defined within configuration YAML files for use with PrimAITE 3.X remain compatible with PrimAITE v4+, it is encouraged to review for minor changes needed. diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 571c850d..07363012 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -80,8 +80,7 @@ class AbstractAgent(BaseModel, ABC): _registry: ClassVar[Dict[str, Type[AbstractAgent]]] = {} def __init__(self, **kwargs): - """Initialise and setup agent logger""" - + """Initialise and setup agent logger.""" super().__init__(**kwargs) self.logger: AgentLog = AgentLog(agent_name=kwargs["config"]["ref"]) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 1fa8c680..b0426537 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -163,7 +163,6 @@ class Network(SimComponent): :param links: Include link details in the output. Defaults to True. :param markdown: Use Markdown style in table output. Defaults to False. """ - if nodes: table = PrettyTable(["Node", "Type", "Operating State"]) if markdown: @@ -192,8 +191,8 @@ class Network(SimComponent): port.ip_address, port.subnet_mask, node.config.default_gateway, - ] - ) + ] + ) print(table) if links: diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 42468057..8fb64d73 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -20,7 +20,6 @@ from primaite.utils.validation.port import Port if TYPE_CHECKING: from primaite.simulator.system.core.software_manager import SoftwareManager - from primaite.simulator.network.hardware.base import Node class SoftwareType(Enum): diff --git a/tests/unit_tests/_primaite/_game/_agent/test_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_agent.py index 064537c2..b555f1b2 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_agent.py @@ -4,7 +4,7 @@ from primaite.game.agent.scripted_agents.random_agent import RandomAgent def test_creating_empty_agent(): - agent = RandomAgent(config={"ref" :"Empty Agent"}) + agent = RandomAgent(config={"ref": "Empty Agent"}) assert len(agent.action_manager.action_map) == 0 assert isinstance(agent.observation_manager.obs, NullObservation) assert len(agent.reward_function.reward_components) == 0 From 2830a2aef5b3002028a580a3aa9aebe469a459f4 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Fri, 14 Feb 2025 12:36:23 +0000 Subject: [PATCH 202/224] #3060 - Amendment to new version tag in configuration yaml. Now under the metadata tag --- .../config/_package_data/basic_lan_network_example.yaml | 3 ++- .../_package_data/client_server_p2p_network_example.yaml | 3 ++- src/primaite/config/_package_data/data_manipulation.yaml | 3 ++- src/primaite/config/_package_data/data_manipulation_marl.yaml | 3 ++- .../mini_scenario_with_simulation_variation/base_scenario.yaml | 3 ++- .../_package_data/multi_lan_internet_network_example.yaml | 3 ++- .../_package_data/scenario_with_placeholders/scenario.yaml | 3 ++- tests/assets/configs/action_penalty.yaml | 3 ++- tests/assets/configs/bad_primaite_session.yaml | 3 ++- tests/assets/configs/basic_c2_setup.yaml | 3 ++- tests/assets/configs/basic_firewall.yaml | 3 ++- .../configs/basic_node_with_software_listening_ports.yaml | 3 ++- tests/assets/configs/basic_node_with_users.yaml | 3 ++- tests/assets/configs/basic_switched_network.yaml | 3 ++- tests/assets/configs/data_manipulation.yaml | 3 ++- tests/assets/configs/dmz_network.yaml | 3 ++- tests/assets/configs/eval_only_primaite_session.yaml | 3 ++- tests/assets/configs/extended_config.yaml | 3 ++- tests/assets/configs/firewall_actions_network.yaml | 3 ++- tests/assets/configs/fixing_duration_one_item.yaml | 3 ++- tests/assets/configs/install_and_configure_apps.yaml | 3 ++- tests/assets/configs/multi_agent_session.yaml | 3 ++- .../configs/nmap_network_service_recon_red_agent_config.yaml | 3 ++- tests/assets/configs/nmap_ping_scan_red_agent_config.yaml | 3 ++- tests/assets/configs/nmap_port_scan_red_agent_config.yaml | 3 ++- tests/assets/configs/no_nodes_links_agents_network.yaml | 3 ++- tests/assets/configs/scenario_with_placeholders/scenario.yaml | 3 ++- tests/assets/configs/shared_rewards.yaml | 3 ++- tests/assets/configs/software_fixing_duration.yaml | 3 ++- tests/assets/configs/test_application_install.yaml | 3 ++- tests/assets/configs/test_primaite_session.yaml | 3 ++- tests/assets/configs/wireless_wan_network_config.yaml | 3 ++- .../configs/wireless_wan_network_config_freq_max_override.yaml | 3 ++- .../wireless_wan_network_config_freq_max_override_blocked.yaml | 3 ++- 34 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/primaite/config/_package_data/basic_lan_network_example.yaml b/src/primaite/config/_package_data/basic_lan_network_example.yaml index 854dfcf3..9996be84 100644 --- a/src/primaite/config/_package_data/basic_lan_network_example.yaml +++ b/src/primaite/config/_package_data/basic_lan_network_example.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: ports: diff --git a/src/primaite/config/_package_data/client_server_p2p_network_example.yaml b/src/primaite/config/_package_data/client_server_p2p_network_example.yaml index 702c864f..1a9fca98 100644 --- a/src/primaite/config/_package_data/client_server_p2p_network_example.yaml +++ b/src/primaite/config/_package_data/client_server_p2p_network_example.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: ports: diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 72b3f671..81f9aabe 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index 178b3322..71507acb 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml index 9530305a..2ea18867 100644 --- a/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml +++ b/src/primaite/config/_package_data/mini_scenario_with_simulation_variation/base_scenario.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: max_episode_length: 128 diff --git a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml index 4e8f085e..deaef3bd 100644 --- a/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml +++ b/src/primaite/config/_package_data/multi_lan_internet_network_example.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: ports: diff --git a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml index d34fb310..4ec3d257 100644 --- a/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml +++ b/src/primaite/config/_package_data/scenario_with_placeholders/scenario.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/tests/assets/configs/action_penalty.yaml b/tests/assets/configs/action_penalty.yaml index 9561db4f..6700172e 100644 --- a/tests/assets/configs/action_penalty.yaml +++ b/tests/assets/configs/action_penalty.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: false diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 1f59225b..b8551caf 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: ports: diff --git a/tests/assets/configs/basic_c2_setup.yaml b/tests/assets/configs/basic_c2_setup.yaml index 3011457f..9b569b44 100644 --- a/tests/assets/configs/basic_c2_setup.yaml +++ b/tests/assets/configs/basic_c2_setup.yaml @@ -4,7 +4,8 @@ # | node_a |------| switch_1 |------| node_b | # -------------- -------------- -------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 1aa1361a..26038270 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -4,7 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: 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 index b503d66d..6372de54 100644 --- a/tests/assets/configs/basic_node_with_software_listening_ports.yaml +++ b/tests/assets/configs/basic_node_with_software_listening_ports.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/basic_node_with_users.yaml b/tests/assets/configs/basic_node_with_users.yaml index 3009b79e..20331ff2 100644 --- a/tests/assets/configs/basic_node_with_users.yaml +++ b/tests/assets/configs/basic_node_with_users.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 7e8a5715..b57ed3e0 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -4,7 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/data_manipulation.yaml b/tests/assets/configs/data_manipulation.yaml index baf9df7d..59f97644 100644 --- a/tests/assets/configs/data_manipulation.yaml +++ b/tests/assets/configs/data_manipulation.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 456920f1..0accb3b2 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -30,7 +30,8 @@ # | external_computer |------| switch_3 |------| external_server | # ----------------------- -------------- --------------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index c0a61b9d..6085b1e7 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: ports: diff --git a/tests/assets/configs/extended_config.yaml b/tests/assets/configs/extended_config.yaml index 1b0772c1..a58a9d4a 100644 --- a/tests/assets/configs/extended_config.yaml +++ b/tests/assets/configs/extended_config.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/tests/assets/configs/firewall_actions_network.yaml b/tests/assets/configs/firewall_actions_network.yaml index b624be52..66470f5a 100644 --- a/tests/assets/configs/firewall_actions_network.yaml +++ b/tests/assets/configs/firewall_actions_network.yaml @@ -30,7 +30,8 @@ # | external_computer |------| switch_3 |------| external_server | # ----------------------- -------------- --------------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/fixing_duration_one_item.yaml b/tests/assets/configs/fixing_duration_one_item.yaml index ad94a9da..02b69e5c 100644 --- a/tests/assets/configs/fixing_duration_one_item.yaml +++ b/tests/assets/configs/fixing_duration_one_item.yaml @@ -4,7 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/install_and_configure_apps.yaml b/tests/assets/configs/install_and_configure_apps.yaml index 4f96496e..35546902 100644 --- a/tests/assets/configs/install_and_configure_apps.yaml +++ b/tests/assets/configs/install_and_configure_apps.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 263d5f66..de0cdad9 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: false diff --git a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml index 6abb11ce..f7b8431e 100644 --- a/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml +++ b/tests/assets/configs/nmap_network_service_recon_red_agent_config.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml index 869f4fb8..112d7266 100644 --- a/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_ping_scan_red_agent_config.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml index 1d5ab323..acd5319a 100644 --- a/tests/assets/configs/nmap_port_scan_red_agent_config.yaml +++ b/tests/assets/configs/nmap_port_scan_red_agent_config.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/no_nodes_links_agents_network.yaml b/tests/assets/configs/no_nodes_links_agents_network.yaml index b79e6bc6..ed279b51 100644 --- a/tests/assets/configs/no_nodes_links_agents_network.yaml +++ b/tests/assets/configs/no_nodes_links_agents_network.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/scenario_with_placeholders/scenario.yaml b/tests/assets/configs/scenario_with_placeholders/scenario.yaml index 76b51626..57fe59ab 100644 --- a/tests/assets/configs/scenario_with_placeholders/scenario.yaml +++ b/tests/assets/configs/scenario_with_placeholders/scenario.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 68b3b310..5aeb99fa 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: false diff --git a/tests/assets/configs/software_fixing_duration.yaml b/tests/assets/configs/software_fixing_duration.yaml index 2f5613e1..66ba6f18 100644 --- a/tests/assets/configs/software_fixing_duration.yaml +++ b/tests/assets/configs/software_fixing_duration.yaml @@ -4,7 +4,8 @@ # | client_1 |------| switch_1 |------| client_2 | # -------------- -------------- -------------- # -version: 3.0 +metadata: + version: 3.0 io_settings: save_step_metadata: false diff --git a/tests/assets/configs/test_application_install.yaml b/tests/assets/configs/test_application_install.yaml index d49d9bf2..8a292f83 100644 --- a/tests/assets/configs/test_application_install.yaml +++ b/tests/assets/configs/test_application_install.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index c1206634..ad43732f 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 io_settings: save_agent_actions: true diff --git a/tests/assets/configs/wireless_wan_network_config.yaml b/tests/assets/configs/wireless_wan_network_config.yaml index b722e50f..45721d8a 100644 --- a/tests/assets/configs/wireless_wan_network_config.yaml +++ b/tests/assets/configs/wireless_wan_network_config.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: max_episode_length: 256 diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml index 4c2f4f86..20e48a89 100644 --- a/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: max_episode_length: 256 diff --git a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml index a98b6ee2..6342d1b1 100644 --- a/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml +++ b/tests/assets/configs/wireless_wan_network_config_freq_max_override_blocked.yaml @@ -1,4 +1,5 @@ -version: 3.0 +metadata: + version: 3.0 game: max_episode_length: 256 From bf30b70bd2055500dfaf88d78682734cdf124af2 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 17 Feb 2025 10:09:39 +0000 Subject: [PATCH 203/224] #3060 - Removal of un-necessary check in game.py --- src/primaite/game/game.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 4741199c..1427776e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -247,13 +247,6 @@ class PrimaiteGame: game.options = PrimaiteGameOptions(**cfg["game"]) game.save_step_metadata = cfg.get("io_settings", {}).get("save_step_metadata") or False - config_version = cfg.get("version", {}) - - # TODO: Future YAML config should specify the PrimAITE version they are written for. - # For now, we warn that if it is missing, pending a mechanism to handle variations. - if not isinstance(config_version, int): - _LOGGER.warning("Version definition is missing from provided configuration. ") - # 1. create simulation sim = game.simulation net = sim.network From 486c797d60422c6f0acfea7a2e9cf9094bf4b42c Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 17 Feb 2025 11:54:13 +0000 Subject: [PATCH 204/224] #3075: Change duplicate application_name in config file. --- src/primaite/config/_package_data/data_manipulation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 4705c135..ceee607d 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -81,7 +81,7 @@ agents: action: node-application-execute options: node_name: client_1 - application_name: web-browser + application_name: database-client reward_function: reward_components: From 7e82de919c4287eb64d4e47b84808be2d2525f61 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 17 Feb 2025 14:19:56 +0000 Subject: [PATCH 205/224] #3075: Markdown changes. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 85 +- ...a-Manipulation-Customising-Red-Agent.ipynb | 48 +- .../Data-Manipulation-E2E-Demonstration.ipynb | 4531 ++++++++++++++++- 3 files changed, 4548 insertions(+), 116 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 474dadff..6dc8e077 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -6,7 +6,7 @@ "source": [ "# Command and Control Application Suite E2E Demonstration\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This notebook demonstrates the current implementation of the command and control (C2) server and beacon applications in primAITE." ] @@ -201,15 +201,15 @@ " nodes: # Node List\n", " - node_name: web_server\n", " applications: \n", - " - application_name: C2Beacon\n", + " - application_name: c2-beacon\n", " ...\n", " ...\n", " action_map:\n", " 1:\n", - " action: node_application_install \n", + " action: node-application-install \n", " options:\n", " node_id: 0 # Index 0 at the node list.\n", - " application_name: C2Beacon\n", + " application_name: c2-beacon\n", "```" ] }, @@ -244,7 +244,7 @@ " action_map:\n", " ...\n", " 2:\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", " node_id: 0 # Node Index\n", " config: # Further information about these config options can be found at the bottom of this notebook.\n", @@ -273,7 +273,7 @@ "source": [ "### **Command and Control** | C2 Beacon Actions | node_application_execute\n", "\n", - "The final action is ``node_application_execute`` which is used to establish a connection for the C2 application. This action can be called by the Red Agent via action ``3`` in it's action map. \n", + "The final action is ``node-application-execute`` which is used to establish a connection for the C2 application. This action can be called by the Red Agent via action ``3`` in it's action map. \n", "\n", "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", @@ -283,13 +283,13 @@ " nodes: # Node List\n", " - node_name: web_server\n", " applications: \n", - " - application_name: C2Beacon\n", + " - application_name: c2-beacon\n", " ...\n", " ...\n", " action_map:\n", " ...\n", " 3:\n", - " action: node_application_execute\n", + " action: node-application-execute\n", " options:\n", " node_id: 0\n", " application_id: 0\n", @@ -331,34 +331,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | C2_SERVER_TERMINAL_COMMAND\n", + "### **Command and Control** | C2 Server Actions | c2-server-terminal-command\n", "\n", - "The C2 Server's terminal action: ``C2_SERVER_TERMINAL_COMMAND`` is indexed at ``4`` in it's action map. \n", + "The C2 Server's terminal action: ``c2-server-terminal-command`` is indexed at ``4`` in it's action map. \n", "\n", "This action leverages the terminal service that is installed by default on all nodes to grant red agents a lot more configurability. If you're unfamiliar with terminals then it's recommended that you refer to the ``Terminal Processing`` notebook.\n", "\n", "It's worth noting that an additional benefit a red agent has when using the terminal service via the C2 Server is that you can execute multiple commands in one action. \n", "\n", - "In this notebook, the ``C2_SERVER_TERMINAL_COMMAND`` is used to install a RansomwareScript application on the ``web_server`` node.\n", + "In this notebook, the ``c2-server-terminal-command`` is used to install a RansomwareScript application on the ``web_server`` node.\n", "\n", "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", + " ...\n", " 4:\n", - " action: C2_SERVER_TERMINAL_COMMAND\n", + " action: c2-server-terminal-command\n", " options:\n", - " node_id: 1\n", + " node_name: client_1\n", " ip_address:\n", - " account:\n", " username: admin\n", " password: admin\n", " commands:\n", @@ -366,7 +359,7 @@ " - software_manager\n", " - application\n", " - install\n", - " - RansomwareScript\n", + " - ransomware-script\n", "```" ] }, @@ -402,14 +395,8 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", + " ...\n", " 5:\n", " action: c2-server-ransomware-configure\n", " options:\n", @@ -444,9 +431,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | c2_server_data_exfiltrate\n", + "### **Command and Control** | C2 Server Actions | c2-server-data-exfiltrate\n", "\n", - "The second to last action available is the ``c2_server_data_exfiltrate`` which is indexed as action ``6`` in the action map.\n", + "The second to last action available is the ``c2-server-data-exfiltrate`` which is indexed as action ``6`` in the action map.\n", "\n", "This action can be used to exfiltrate a target file on a remote node to the C2 Beacon and the C2 Server's host file system via the ``FTP`` services.\n", "\n", @@ -454,25 +441,18 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", + " ...\n", " 6:\n", - " action: c2_server_data_exfiltrate\n", + " action: c2-server-data-exfiltrate\n", " options:\n", " node_id: 1\n", " target_file_name: \"database.db\"\n", " target_folder_name: \"database\"\n", " exfiltration_folder_name: \"spoils\"\n", " target_ip_address: \"192.168.1.14\"\n", - " account:\n", - " username: \"admin\",\n", - " password: \"admin\"\n", + " username: \"admin\",\n", + " password: \"admin\"\n", "\n", "```" ] @@ -510,9 +490,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### **Command and Control** | C2 Server Actions | c2_server_ransomware_launch\n", + "### **Command and Control** | C2 Server Actions | c2-server-ransomware-launch\n", "\n", - "Finally, the last available action is for the c2_server_ransomware_launch to start the ransomware script installed on the same node as the C2 beacon.\n", + "Finally, the last available action is for the c2-server-ransomware-launch to start the ransomware script installed on the same node as the C2 beacon.\n", "\n", "This action is indexed as action ``7``.\n", "\n", @@ -520,16 +500,10 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", " ...\n", " action_map:\n", " 7:\n", - " action: c2_server_ransomware_launch\n", + " action: c2-server-ransomware-launch\n", " options:\n", " node_id: 1\n", "```\n" @@ -1337,18 +1311,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As demonstrated earlier, red agents can use the ``configure_c2_beacon`` action to configure these settings mid episode through the configuration options:\n", + "As demonstrated earlier, red agents can use the ``configure-c2-beacon`` action to configure these settings mid episode through the configuration options:\n", "\n", "``` YAML\n", "...\n", - " action: configure_c2_beacon\n", + " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " config:\n", " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency: 10\n", - " masquerade_protocol: TCP\n", - " masquerade_port: DNS\n", "```" ] }, diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 2e0a0d40..1eb814e3 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -146,42 +146,15 @@ "```yaml\n", " - ref: data_manipulation_attacker # name of agent\n", " team: RED # not used, just for human reference\n", - " type: RedDatabaseCorruptingAgent # type of agent - this lets primaite know which agent class to use\n", - "\n", - " # Since the agent does not need to react to what is happening in the environment, the observation space is empty.\n", - " observation_space:\n", - " type: UC2RedObservation\n", - " options:\n", - " nodes: {}\n", - "\n", - " action_space:\n", - " \n", - " # The agent has access to the DataManipulationBoth on clients 1 and 2.\n", - " options:\n", - " nodes:\n", - " - node_name: client_1 # The network should have a node called client_1\n", - " applications:\n", - " - application_name: DataManipulationBot # The node client_1 should have DataManipulationBot configured on it\n", - " - node_name: client_2 # The network should have a node called client_2\n", - " applications:\n", - " - application_name: DataManipulationBot # The node client_2 should have DataManipulationBot configured on it\n", - "\n", - " # not important\n", - " max_folders_per_node: 1\n", - " max_files_per_folder: 1\n", - " max_services_per_node: 1\n", - "\n", - " # red agent does not need a reward function\n", - " reward_function:\n", - " reward_components:\n", - " - type: DUMMY\n", + " type: red-database-corrupting-agent # type of agent - this lets primaite know which agent class to use\n", "\n", " # These actions are passed to the RedDatabaseCorruptingAgent init method, they dictate the schedule of attacks\n", " agent_settings:\n", - " start_settings:\n", - " start_step: 25 # first attack at step 25\n", - " frequency: 20 # attacks will happen every 20 steps (on average)\n", - " variance: 5 # the timing of attacks will vary by up to 5 steps earlier or later\n", + " possible_start_nodes: [client_1, client_2] # List of clients the attack can start from\n", + " target_application: data-manipulation-bot\n", + " start_step: 25 # first attack at step 25\n", + " frequency: 20 # attacks will happen every 20 steps (on average)\n", + " variance: 5 # the timing of attacks will vary by up to 5 steps earlier or later\n", "```" ] }, @@ -201,8 +174,7 @@ "simulation:\n", " network:\n", " nodes:\n", - " - ref: client_1\n", - " hostname: client_1\n", + " - hostname: client_1\n", " type: computer\n", " ip_address: 192.168.10.21\n", " subnet_mask: 255.255.255.0\n", @@ -210,15 +182,13 @@ " \n", " # \n", " applications:\n", - " - ref: data_manipulation_bot\n", - " type: DataManipulationBot\n", + " - type: data-manipulation-bot\n", " options:\n", " port_scan_p_of_success: 0.8 # Probability that port scan is successful\n", " data_manipulation_p_of_success: 0.8 # Probability that SQL attack is successful\n", " payload: \"DELETE\" # The SQL query which causes the attack (this has to be DELETE)\n", " server_ip: 192.168.1.14 # IP address of server hosting the database\n", - " - ref: client_1_database_client\n", - " type: DatabaseClient # Database client must be installed in order for DataManipulationBot to function\n", + " - type: database-client # Database client must be installed in order for DataManipulationBot to function\n", " options:\n", " db_server_ip: 192.168.1.14 # IP address of server hosting the database\n", "```" diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 2d3f99b2..f3e3fb98 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -6,7 +6,7 @@ "source": [ "# Data Manipulation Scenario\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK" + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK" ] }, { @@ -373,16 +373,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2025-02-17 11:15:30,959: Performing the PrimAITE first-time setup...\n", + "2025-02-17 11:15:30,960: Building the PrimAITE app directories...\n", + "2025-02-17 11:15:30,960: Building primaite_config.yaml...\n", + "2025-02-17 11:15:30,960: Rebuilding the demo notebooks...\n", + "2025-02-17 11:15:30,967: Rebuilding the example notebooks...\n", + "2025-02-17 11:15:30,970: PrimAITE setup complete!\n" + ] + } + ], "source": [ "!primaite setup" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "tags": [] }, @@ -394,7 +407,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "tags": [] }, @@ -420,9 +433,254 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-02-17 11:15:35,108: PrimaiteGymEnv RNG seed = None\n", + "2025-02-17 11:15:35,109: Resetting environment, episode 0, avg. reward: 0.0\n", + "2025-02-17 11:15:35,111: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-17/11-15-31/agent_actions/episode_0.json\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "env created successfully\n", + "{'ICS': 0,\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 0}},\n", + " 2: {'PROTOCOLS': {'ALL': 0}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 0}},\n", + " 5: {'PROTOCOLS': {'ALL': 0}},\n", + " 6: {'PROTOCOLS': {'ALL': 0}},\n", + " 7: {'PROTOCOLS': {'ALL': 0}},\n", + " 8: {'PROTOCOLS': {'ALL': 0}},\n", + " 9: {'PROTOCOLS': {'ALL': 0}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'NODES': {'HOST0': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST1': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 1}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST2': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST3': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST4': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST5': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST6': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0,\n", + " 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0,\n", + " 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'ROUTER0': {'ACL': {1: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 2: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 3: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 4: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 5: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 6: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 7: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 8: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 9: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 10: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}}}\n" + ] + } + ], "source": [ "# create the env\n", "with open(data_manipulation_config_path(), 'r') as f:\n", @@ -450,7 +708,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -468,9 +726,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 1, Red action: DO NOTHING, Blue reward:0.65\n", + "step: 2, Red action: DO NOTHING, Blue reward:0.65\n", + "step: 3, Red action: DO NOTHING, Blue reward:0.65\n", + "step: 4, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 5, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 6, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 7, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 8, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 9, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 10, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 11, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 12, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 13, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 14, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 15, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 16, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 17, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 18, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 19, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 20, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 21, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 22, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 23, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 24, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 25, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 26, Red action: ATTACK from client 2, Blue reward:0.15\n", + "step: 27, Red action: DO NOTHING, Blue reward:-0.35\n", + "step: 28, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 29, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 30, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 31, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 32, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 33, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 34, Red action: DO NOTHING, Blue reward:-0.85\n", + "step: 35, Red action: DO NOTHING, Blue reward:-0.85\n" + ] + } + ], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -486,9 +786,198 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'HOST0': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST1': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST2': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST3': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST4': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST5': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST6': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'ROUTER0': {'ACL': {1: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 2: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 3: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 4: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 5: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 6: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 7: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 8: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 9: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 10: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}}\n" + ] + } + ], "source": [ "pprint(obs['NODES'])" ] @@ -502,9 +991,198 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'HOST0': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST1': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST2': {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST3': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST4': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST5': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'HOST6': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", + " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1,\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", + " 'ROUTER0': {'ACL': {1: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 2: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 3: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 4: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 5: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 6: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 7: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 8: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 9: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0},\n", + " 10: {'dest_ip_id': 0,\n", + " 'dest_port_id': 0,\n", + " 'dest_wildcard_id': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol_id': 0,\n", + " 'source_ip_id': 0,\n", + " 'source_port_id': 0,\n", + " 'source_wildcard_id': 0}},\n", + " 'users': {'local_login': 0, 'remote_sessions': 0}}}\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -536,9 +1214,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 38\n", + "Red action: do-nothing\n", + "Green action: do-nothing\n", + "Green action: node-application-execute\n", + "Blue reward:-0.8500000000000001\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -561,9 +1251,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 39\n", + "Red action: do-nothing\n", + "Green action: timestep=38 action='node-application-execute' parameters={'node_name': 'client_2', 'application_name': 'web-browser'} request=['network', 'node', 'client_2', 'application', 'web-browser', 'execute'] response=RequestResponse(status='failure', data={}) reward=-0.2 reward_info={'connection_attempt_status': 'n/a'}\n", + "Green action: timestep=38 action='node-application-execute' parameters={'node_name': 'client_1', 'application_name': 'web-browser'} request=['network', 'node', 'client_1', 'application', 'web-browser', 'execute'] response=RequestResponse(status='failure', data={}) reward=-0.25 reward_info={'connection_attempt_status': 'n/a'}\n", + "Blue reward:-0.05\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -586,7 +1288,3796 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 40, Red action: do-nothing, Blue reward:-0.05\n", + "step: 41, Red action: do-nothing, Blue reward:-0.05\n", + "step: 42, Red action: do-nothing, Blue reward:-0.05\n", + "step: 43, Red action: do-nothing, Blue reward:0.95\n", + "step: 44, Red action: do-nothing, Blue reward:0.95\n", + "step: 45, Red action: do-nothing, Blue reward:0.95\n", + "step: 46, Red action: do-nothing, Blue reward:0.95\n", + "step: 47, Red action: do-nothing, Blue reward:0.95\n", + "step: 48, Red action: do-nothing, Blue reward:0.95\n", + "step: 49, Red action: do-nothing, Blue reward:0.95\n", + "step: 50, Red action: do-nothing, Blue reward:0.95\n", + "step: 51, Red action: node-application-execute, Blue reward:0.95\n", + "step: 52, Red action: do-nothing, Blue reward:0.95\n", + "step: 53, Red action: do-nothing, Blue reward:0.85\n", + "step: 54, Red action: do-nothing, Blue reward:0.85\n", + "step: 55, Red action: do-nothing, Blue reward:0.85\n", + "step: 56, Red action: do-nothing, Blue reward:0.85\n", + "step: 57, Red action: do-nothing, Blue reward:0.85\n", + "step: 58, Red action: do-nothing, Blue reward:0.85\n", + "step: 59, Red action: do-nothing, Blue reward:0.85\n", + "step: 60, Red action: do-nothing, Blue reward:0.85\n", + "step: 61, Red action: do-nothing, Blue reward:0.85\n", + "step: 62, Red action: do-nothing, Blue reward:0.85\n", + "step: 63, Red action: do-nothing, Blue reward:0.85\n", + "step: 64, Red action: do-nothing, Blue reward:0.85\n", + "step: 65, Red action: do-nothing, Blue reward:0.85\n", + "step: 66, Red action: do-nothing, Blue reward:0.85\n", + "step: 67, Red action: do-nothing, Blue reward:0.85\n", + "step: 68, Red action: do-nothing, Blue reward:0.85\n", + "step: 69, Red action: do-nothing, Blue reward:0.85\n", + "step: 70, Red action: do-nothing, Blue reward:0.85\n", + "step: 71, Red action: do-nothing, Blue reward:0.85\n", + "step: 72, Red action: do-nothing, Blue reward:0.85\n", + "step: 73, Red action: do-nothing, Blue reward:0.85\n", + "step: 74, Red action: do-nothing, Blue reward:0.85\n", + "step: 75, Red action: node-application-execute, Blue reward:0.85\n", + "step: 76, Red action: do-nothing, Blue reward:0.85\n", + "step: 77, Red action: do-nothing, Blue reward:0.85\n", + "step: 78, Red action: do-nothing, Blue reward:0.85\n", + "step: 79, Red action: do-nothing, Blue reward:0.85\n", + "step: 80, Red action: do-nothing, Blue reward:0.85\n", + "step: 81, Red action: do-nothing, Blue reward:0.85\n", + "step: 82, Red action: do-nothing, Blue reward:0.85\n", + "step: 83, Red action: do-nothing, Blue reward:0.85\n", + "step: 84, Red action: do-nothing, Blue reward:0.85\n", + "step: 85, Red action: do-nothing, Blue reward:0.85\n", + "step: 86, Red action: do-nothing, Blue reward:0.85\n", + "step: 87, Red action: do-nothing, Blue reward:0.85\n", + "step: 88, Red action: do-nothing, Blue reward:0.85\n", + "step: 89, Red action: do-nothing, Blue reward:0.85\n", + "step: 90, Red action: do-nothing, Blue reward:0.85\n", + "step: 91, Red action: do-nothing, Blue reward:0.85\n", + "step: 92, Red action: do-nothing, Blue reward:0.85\n", + "step: 93, Red action: do-nothing, Blue reward:0.85\n", + "step: 94, Red action: do-nothing, Blue reward:0.85\n", + "step: 95, Red action: do-nothing, Blue reward:0.85\n", + "step: 96, Red action: do-nothing, Blue reward:0.85\n", + "step: 97, Red action: node-application-execute, Blue reward:0.85\n", + "step: 98, Red action: do-nothing, Blue reward:0.85\n", + "step: 99, Red action: do-nothing, Blue reward:0.85\n", + "step: 100, Red action: do-nothing, Blue reward:0.85\n", + "step: 101, Red action: do-nothing, Blue reward:0.85\n", + "step: 102, Red action: do-nothing, Blue reward:0.85\n", + "step: 103, Red action: do-nothing, Blue reward:0.85\n", + "step: 104, Red action: do-nothing, Blue reward:0.85\n", + "step: 105, Red action: do-nothing, Blue reward:0.85\n", + "step: 106, Red action: do-nothing, Blue reward:0.85\n", + "step: 107, Red action: do-nothing, Blue reward:0.85\n", + "step: 108, Red action: do-nothing, Blue reward:0.85\n", + "step: 109, Red action: do-nothing, Blue reward:0.85\n", + "step: 110, Red action: do-nothing, Blue reward:0.85\n", + "step: 111, Red action: do-nothing, Blue reward:0.85\n", + "step: 112, Red action: do-nothing, Blue reward:0.85\n", + "step: 113, Red action: node-application-execute, Blue reward:0.85\n", + "step: 114, Red action: do-nothing, Blue reward:0.85\n", + "step: 115, Red action: do-nothing, Blue reward:0.85\n", + "step: 116, Red action: do-nothing, Blue reward:0.85\n", + "step: 117, Red action: do-nothing, Blue reward:0.85\n", + "step: 118, Red action: do-nothing, Blue reward:0.85\n", + "step: 119, Red action: do-nothing, Blue reward:0.85\n", + "step: 120, Red action: do-nothing, Blue reward:0.85\n", + "step: 121, Red action: do-nothing, Blue reward:0.85\n", + "step: 122, Red action: do-nothing, Blue reward:0.85\n", + "step: 123, Red action: do-nothing, Blue reward:0.85\n", + "step: 124, Red action: do-nothing, Blue reward:0.85\n", + "step: 125, Red action: do-nothing, Blue reward:0.85\n", + "step: 126, Red action: do-nothing, Blue reward:0.85\n", + "step: 127, Red action: do-nothing, Blue reward:0.85\n", + "step: 128, Red action: do-nothing, Blue reward:0.85\n", + "step: 129, Red action: do-nothing, Blue reward:0.85\n", + "step: 130, Red action: do-nothing, Blue reward:0.85\n", + "step: 131, Red action: do-nothing, Blue reward:0.85\n", + "step: 132, Red action: node-application-execute, Blue reward:0.85\n", + "step: 133, Red action: do-nothing, Blue reward:0.85\n", + "step: 134, Red action: do-nothing, Blue reward:0.85\n", + "step: 135, Red action: do-nothing, Blue reward:0.85\n", + "step: 136, Red action: do-nothing, Blue reward:0.85\n", + "step: 137, Red action: do-nothing, Blue reward:0.85\n", + "step: 138, Red action: do-nothing, Blue reward:0.85\n", + "step: 139, Red action: do-nothing, Blue reward:0.85\n", + "step: 140, Red action: do-nothing, Blue reward:0.85\n", + "step: 141, Red action: do-nothing, Blue reward:0.85\n", + "step: 142, Red action: do-nothing, Blue reward:0.85\n", + "step: 143, Red action: do-nothing, Blue reward:0.85\n", + "step: 144, Red action: do-nothing, Blue reward:0.85\n", + "step: 145, Red action: do-nothing, Blue reward:0.85\n", + "step: 146, Red action: do-nothing, Blue reward:0.85\n", + "step: 147, Red action: do-nothing, Blue reward:0.85\n", + "step: 148, Red action: do-nothing, Blue reward:0.85\n", + "step: 149, Red action: do-nothing, Blue reward:0.85\n", + "step: 150, Red action: do-nothing, Blue reward:0.85\n", + "step: 151, Red action: do-nothing, Blue reward:0.85\n", + "step: 152, Red action: do-nothing, Blue reward:0.85\n", + "step: 153, Red action: do-nothing, Blue reward:0.85\n", + "step: 154, Red action: do-nothing, Blue reward:0.85\n", + "step: 155, Red action: do-nothing, Blue reward:0.85\n", + "step: 156, Red action: do-nothing, Blue reward:0.85\n", + "step: 157, Red action: node-application-execute, Blue reward:0.85\n", + "step: 158, Red action: do-nothing, Blue reward:0.85\n", + "step: 159, Red action: do-nothing, Blue reward:0.85\n", + "step: 160, Red action: do-nothing, Blue reward:0.85\n", + "step: 161, Red action: do-nothing, Blue reward:0.85\n", + "step: 162, Red action: do-nothing, Blue reward:0.85\n", + "step: 163, Red action: do-nothing, Blue reward:0.85\n", + "step: 164, Red action: do-nothing, Blue reward:0.85\n", + "step: 165, Red action: do-nothing, Blue reward:0.85\n", + "step: 166, Red action: do-nothing, Blue reward:0.85\n", + "step: 167, Red action: do-nothing, Blue reward:0.85\n", + "step: 168, Red action: do-nothing, Blue reward:0.85\n", + "step: 169, Red action: do-nothing, Blue reward:0.85\n", + "step: 170, Red action: do-nothing, Blue reward:0.85\n", + "step: 171, Red action: do-nothing, Blue reward:0.85\n", + "step: 172, Red action: do-nothing, Blue reward:0.85\n", + "step: 173, Red action: do-nothing, Blue reward:0.85\n", + "step: 174, Red action: do-nothing, Blue reward:0.85\n", + "step: 175, Red action: do-nothing, Blue reward:0.85\n", + "step: 176, Red action: do-nothing, Blue reward:0.85\n", + "step: 177, Red action: do-nothing, Blue reward:0.85\n", + "step: 178, Red action: node-application-execute, Blue reward:0.85\n", + "step: 179, Red action: do-nothing, Blue reward:0.85\n", + "step: 180, Red action: do-nothing, Blue reward:0.85\n", + "step: 181, Red action: do-nothing, Blue reward:0.85\n", + "step: 182, Red action: do-nothing, Blue reward:0.85\n", + "step: 183, Red action: do-nothing, Blue reward:0.85\n", + "step: 184, Red action: do-nothing, Blue reward:0.85\n", + "step: 185, Red action: do-nothing, Blue reward:0.85\n", + "step: 186, Red action: do-nothing, Blue reward:0.85\n", + "step: 187, Red action: do-nothing, Blue reward:0.85\n", + "step: 188, Red action: do-nothing, Blue reward:0.85\n", + "step: 189, Red action: do-nothing, Blue reward:0.85\n", + "step: 190, Red action: do-nothing, Blue reward:0.85\n", + "step: 191, Red action: do-nothing, Blue reward:0.85\n", + "step: 192, Red action: do-nothing, Blue reward:0.85\n", + "step: 193, Red action: do-nothing, Blue reward:0.85\n", + "step: 194, Red action: do-nothing, Blue reward:0.85\n", + "step: 195, Red action: do-nothing, Blue reward:0.85\n", + "step: 196, Red action: do-nothing, Blue reward:0.85\n", + "step: 197, Red action: do-nothing, Blue reward:0.85\n", + "step: 198, Red action: node-application-execute, Blue reward:0.85\n", + "step: 199, Red action: do-nothing, Blue reward:0.85\n", + "step: 200, Red action: do-nothing, Blue reward:0.85\n", + "step: 201, Red action: do-nothing, Blue reward:0.85\n", + "step: 202, Red action: do-nothing, Blue reward:0.85\n", + "step: 203, Red action: do-nothing, Blue reward:0.85\n", + "step: 204, Red action: do-nothing, Blue reward:0.85\n", + "step: 205, Red action: do-nothing, Blue reward:0.85\n", + "step: 206, Red action: do-nothing, Blue reward:0.85\n", + "step: 207, Red action: do-nothing, Blue reward:0.85\n", + "step: 208, Red action: do-nothing, Blue reward:0.85\n", + "step: 209, Red action: do-nothing, Blue reward:0.85\n", + "step: 210, Red action: do-nothing, Blue reward:0.85\n", + "step: 211, Red action: do-nothing, Blue reward:0.85\n", + "step: 212, Red action: do-nothing, Blue reward:0.85\n", + "step: 213, Red action: do-nothing, Blue reward:0.85\n", + "step: 214, Red action: do-nothing, Blue reward:0.85\n", + "step: 215, Red action: do-nothing, Blue reward:0.85\n", + "step: 216, Red action: do-nothing, Blue reward:0.85\n", + "step: 217, Red action: do-nothing, Blue reward:0.85\n", + "step: 218, Red action: node-application-execute, Blue reward:0.85\n", + "step: 219, Red action: do-nothing, Blue reward:0.85\n", + "step: 220, Red action: do-nothing, Blue reward:0.85\n", + "step: 221, Red action: do-nothing, Blue reward:0.85\n", + "step: 222, Red action: do-nothing, Blue reward:0.85\n", + "step: 223, Red action: do-nothing, Blue reward:0.85\n", + "step: 224, Red action: do-nothing, Blue reward:0.85\n", + "step: 225, Red action: do-nothing, Blue reward:0.85\n", + "step: 226, Red action: do-nothing, Blue reward:0.85\n", + "step: 227, Red action: do-nothing, Blue reward:0.85\n", + "step: 228, Red action: do-nothing, Blue reward:0.85\n", + "step: 229, Red action: do-nothing, Blue reward:0.85\n", + "step: 230, Red action: do-nothing, Blue reward:0.85\n", + "step: 231, Red action: do-nothing, Blue reward:0.85\n", + "step: 232, Red action: do-nothing, Blue reward:0.85\n", + "step: 233, Red action: do-nothing, Blue reward:0.85\n", + "step: 234, Red action: do-nothing, Blue reward:0.85\n", + "step: 235, Red action: do-nothing, Blue reward:0.85\n", + "step: 236, Red action: do-nothing, Blue reward:0.85\n", + "step: 237, Red action: do-nothing, Blue reward:0.85\n", + "step: 238, Red action: do-nothing, Blue reward:0.85\n", + "step: 239, Red action: do-nothing, Blue reward:0.85\n", + "step: 240, Red action: do-nothing, Blue reward:0.85\n", + "step: 241, Red action: node-application-execute, Blue reward:0.85\n", + "step: 242, Red action: do-nothing, Blue reward:0.85\n", + "step: 243, Red action: do-nothing, Blue reward:0.85\n", + "step: 244, Red action: do-nothing, Blue reward:0.85\n", + "step: 245, Red action: do-nothing, Blue reward:0.85\n", + "step: 246, Red action: do-nothing, Blue reward:0.85\n", + "step: 247, Red action: do-nothing, Blue reward:0.85\n", + "step: 248, Red action: do-nothing, Blue reward:0.85\n", + "step: 249, Red action: do-nothing, Blue reward:0.85\n", + "step: 250, Red action: do-nothing, Blue reward:0.85\n", + "step: 251, Red action: do-nothing, Blue reward:0.85\n", + "step: 252, Red action: do-nothing, Blue reward:0.85\n", + "step: 253, Red action: do-nothing, Blue reward:0.85\n", + "step: 254, Red action: do-nothing, Blue reward:0.85\n", + "step: 255, Red action: do-nothing, Blue reward:0.85\n", + "step: 256, Red action: do-nothing, Blue reward:0.85\n", + "step: 257, Red action: do-nothing, Blue reward:0.85\n", + "step: 258, Red action: do-nothing, Blue reward:0.85\n", + "step: 259, Red action: do-nothing, Blue reward:0.85\n", + "step: 260, Red action: do-nothing, Blue reward:0.85\n", + "step: 261, Red action: node-application-execute, Blue reward:0.85\n", + "step: 262, Red action: do-nothing, Blue reward:0.85\n", + "step: 263, Red action: do-nothing, Blue reward:0.85\n", + "step: 264, Red action: do-nothing, Blue reward:0.85\n", + "step: 265, Red action: do-nothing, Blue reward:0.85\n", + "step: 266, Red action: do-nothing, Blue reward:0.85\n", + "step: 267, Red action: do-nothing, Blue reward:0.85\n", + "step: 268, Red action: do-nothing, Blue reward:0.85\n", + "step: 269, Red action: do-nothing, Blue reward:0.85\n", + "step: 270, Red action: do-nothing, Blue reward:0.85\n", + "step: 271, Red action: do-nothing, Blue reward:0.85\n", + "step: 272, Red action: do-nothing, Blue reward:0.85\n", + "step: 273, Red action: do-nothing, Blue reward:0.85\n", + "step: 274, Red action: do-nothing, Blue reward:0.85\n", + "step: 275, Red action: do-nothing, Blue reward:0.85\n", + "step: 276, Red action: do-nothing, Blue reward:0.85\n", + "step: 277, Red action: do-nothing, Blue reward:0.85\n", + "step: 278, Red action: do-nothing, Blue reward:0.85\n", + "step: 279, Red action: do-nothing, Blue reward:0.85\n", + "step: 280, Red action: do-nothing, Blue reward:0.85\n", + "step: 281, Red action: node-application-execute, Blue reward:0.85\n", + "step: 282, Red action: do-nothing, Blue reward:0.85\n", + "step: 283, Red action: do-nothing, Blue reward:0.85\n", + "step: 284, Red action: do-nothing, Blue reward:0.85\n", + "step: 285, Red action: do-nothing, Blue reward:0.85\n", + "step: 286, Red action: do-nothing, Blue reward:0.85\n", + "step: 287, Red action: do-nothing, Blue reward:0.85\n", + "step: 288, Red action: do-nothing, Blue reward:0.85\n", + "step: 289, Red action: do-nothing, Blue reward:0.85\n", + "step: 290, Red action: do-nothing, Blue reward:0.85\n", + "step: 291, Red action: do-nothing, Blue reward:0.85\n", + "step: 292, Red action: do-nothing, Blue reward:0.85\n", + "step: 293, Red action: do-nothing, Blue reward:0.85\n", + "step: 294, Red action: do-nothing, Blue reward:0.85\n", + "step: 295, Red action: do-nothing, Blue reward:0.85\n", + "step: 296, Red action: do-nothing, Blue reward:0.85\n", + "step: 297, Red action: do-nothing, Blue reward:0.85\n", + "step: 298, Red action: do-nothing, Blue reward:0.85\n", + "step: 299, Red action: do-nothing, Blue reward:0.85\n", + "step: 300, Red action: do-nothing, Blue reward:0.85\n", + "step: 301, Red action: do-nothing, Blue reward:0.85\n", + "step: 302, Red action: do-nothing, Blue reward:0.85\n", + "step: 303, Red action: do-nothing, Blue reward:0.85\n", + "step: 304, Red action: node-application-execute, Blue reward:0.85\n", + "step: 305, Red action: do-nothing, Blue reward:0.85\n", + "step: 306, Red action: do-nothing, Blue reward:0.85\n", + "step: 307, Red action: do-nothing, Blue reward:0.85\n", + "step: 308, Red action: do-nothing, Blue reward:0.85\n", + "step: 309, Red action: do-nothing, Blue reward:0.85\n", + "step: 310, Red action: do-nothing, Blue reward:0.85\n", + "step: 311, Red action: do-nothing, Blue reward:0.85\n", + "step: 312, Red action: do-nothing, Blue reward:0.85\n", + "step: 313, Red action: do-nothing, Blue reward:0.85\n", + "step: 314, Red action: do-nothing, Blue reward:0.85\n", + "step: 315, Red action: do-nothing, Blue reward:0.85\n", + "step: 316, Red action: do-nothing, Blue reward:0.85\n", + "step: 317, Red action: do-nothing, Blue reward:0.85\n", + "step: 318, Red action: do-nothing, Blue reward:0.85\n", + "step: 319, Red action: do-nothing, Blue reward:0.85\n", + "step: 320, Red action: do-nothing, Blue reward:0.85\n", + "step: 321, Red action: do-nothing, Blue reward:0.85\n", + "step: 322, Red action: do-nothing, Blue reward:0.85\n", + "step: 323, Red action: do-nothing, Blue reward:0.85\n", + "step: 324, Red action: do-nothing, Blue reward:0.85\n", + "step: 325, Red action: node-application-execute, Blue reward:0.85\n", + "step: 326, Red action: do-nothing, Blue reward:0.85\n", + "step: 327, Red action: do-nothing, Blue reward:0.85\n", + "step: 328, Red action: do-nothing, Blue reward:0.85\n", + "step: 329, Red action: do-nothing, Blue reward:0.85\n", + "step: 330, Red action: do-nothing, Blue reward:0.85\n", + "step: 331, Red action: do-nothing, Blue reward:0.85\n", + "step: 332, Red action: do-nothing, Blue reward:0.85\n", + "step: 333, Red action: do-nothing, Blue reward:0.85\n", + "step: 334, Red action: do-nothing, Blue reward:0.85\n", + "step: 335, Red action: do-nothing, Blue reward:0.85\n", + "step: 336, Red action: do-nothing, Blue reward:0.85\n", + "step: 337, Red action: do-nothing, Blue reward:0.85\n", + "step: 338, Red action: do-nothing, Blue reward:0.85\n", + "step: 339, Red action: do-nothing, Blue reward:0.85\n", + "step: 340, Red action: do-nothing, Blue reward:0.85\n", + "step: 341, Red action: do-nothing, Blue reward:0.85\n", + "step: 342, Red action: do-nothing, Blue reward:0.85\n", + "step: 343, Red action: do-nothing, Blue reward:0.85\n", + "step: 344, Red action: do-nothing, Blue reward:0.85\n", + "step: 345, Red action: do-nothing, Blue reward:0.85\n", + "step: 346, Red action: do-nothing, Blue reward:0.85\n", + "step: 347, Red action: do-nothing, Blue reward:0.85\n", + "step: 348, Red action: do-nothing, Blue reward:0.85\n", + "step: 349, Red action: node-application-execute, Blue reward:0.85\n", + "step: 350, Red action: do-nothing, Blue reward:0.85\n", + "step: 351, Red action: do-nothing, Blue reward:0.85\n", + "step: 352, Red action: do-nothing, Blue reward:0.85\n", + "step: 353, Red action: do-nothing, Blue reward:0.85\n", + "step: 354, Red action: do-nothing, Blue reward:0.85\n", + "step: 355, Red action: do-nothing, Blue reward:0.85\n", + "step: 356, Red action: do-nothing, Blue reward:0.85\n", + "step: 357, Red action: do-nothing, Blue reward:0.85\n", + "step: 358, Red action: do-nothing, Blue reward:0.85\n", + "step: 359, Red action: do-nothing, Blue reward:0.85\n", + "step: 360, Red action: do-nothing, Blue reward:0.85\n", + "step: 361, Red action: do-nothing, Blue reward:0.85\n", + "step: 362, Red action: do-nothing, Blue reward:0.85\n", + "step: 363, Red action: do-nothing, Blue reward:0.85\n", + "step: 364, Red action: do-nothing, Blue reward:0.85\n", + "step: 365, Red action: do-nothing, Blue reward:0.85\n", + "step: 366, Red action: do-nothing, Blue reward:0.85\n", + "step: 367, Red action: do-nothing, Blue reward:0.85\n", + "step: 368, Red action: do-nothing, Blue reward:0.85\n", + "step: 369, Red action: node-application-execute, Blue reward:0.85\n", + "step: 370, Red action: do-nothing, Blue reward:0.85\n", + "step: 371, Red action: do-nothing, Blue reward:0.85\n", + "step: 372, Red action: do-nothing, Blue reward:0.85\n", + "step: 373, Red action: do-nothing, Blue reward:0.85\n", + "step: 374, Red action: do-nothing, Blue reward:0.85\n", + "step: 375, Red action: do-nothing, Blue reward:0.85\n", + "step: 376, Red action: do-nothing, Blue reward:0.85\n", + "step: 377, Red action: do-nothing, Blue reward:0.85\n", + "step: 378, Red action: do-nothing, Blue reward:0.85\n", + "step: 379, Red action: do-nothing, Blue reward:0.85\n", + "step: 380, Red action: do-nothing, Blue reward:0.85\n", + "step: 381, Red action: do-nothing, Blue reward:0.85\n", + "step: 382, Red action: do-nothing, Blue reward:0.85\n", + "step: 383, Red action: do-nothing, Blue reward:0.85\n", + "step: 384, Red action: do-nothing, Blue reward:0.85\n", + "step: 385, Red action: do-nothing, Blue reward:0.85\n", + "step: 386, Red action: do-nothing, Blue reward:0.85\n", + "step: 387, Red action: do-nothing, Blue reward:0.85\n", + "step: 388, Red action: do-nothing, Blue reward:0.85\n", + "step: 389, Red action: do-nothing, Blue reward:0.85\n", + "step: 390, Red action: do-nothing, Blue reward:0.85\n", + "step: 391, Red action: do-nothing, Blue reward:0.85\n", + "step: 392, Red action: do-nothing, Blue reward:0.85\n", + "step: 393, Red action: do-nothing, Blue reward:0.85\n", + "step: 394, Red action: node-application-execute, Blue reward:0.85\n", + "step: 395, Red action: do-nothing, Blue reward:0.85\n", + "step: 396, Red action: do-nothing, Blue reward:0.85\n", + "step: 397, Red action: do-nothing, Blue reward:0.85\n", + "step: 398, Red action: do-nothing, Blue reward:0.85\n", + "step: 399, Red action: do-nothing, Blue reward:0.85\n", + "step: 400, Red action: do-nothing, Blue reward:0.85\n", + "step: 401, Red action: do-nothing, Blue reward:0.85\n", + "step: 402, Red action: do-nothing, Blue reward:0.85\n", + "step: 403, Red action: do-nothing, Blue reward:0.85\n", + "step: 404, Red action: do-nothing, Blue reward:0.85\n", + "step: 405, Red action: do-nothing, Blue reward:0.85\n", + "step: 406, Red action: do-nothing, Blue reward:0.85\n", + "step: 407, Red action: do-nothing, Blue reward:0.85\n", + "step: 408, Red action: do-nothing, Blue reward:0.85\n", + "step: 409, Red action: do-nothing, Blue reward:0.85\n", + "step: 410, Red action: do-nothing, Blue reward:0.85\n", + "step: 411, Red action: do-nothing, Blue reward:0.85\n", + "step: 412, Red action: do-nothing, Blue reward:0.85\n", + "step: 413, Red action: do-nothing, Blue reward:0.85\n", + "step: 414, Red action: do-nothing, Blue reward:0.85\n", + "step: 415, Red action: do-nothing, Blue reward:0.85\n", + "step: 416, Red action: do-nothing, Blue reward:0.85\n", + "step: 417, Red action: do-nothing, Blue reward:0.85\n", + "step: 418, Red action: node-application-execute, Blue reward:0.85\n", + "step: 419, Red action: do-nothing, Blue reward:0.85\n", + "step: 420, Red action: do-nothing, Blue reward:0.85\n", + "step: 421, Red action: do-nothing, Blue reward:0.85\n", + "step: 422, Red action: do-nothing, Blue reward:0.85\n", + "step: 423, Red action: do-nothing, Blue reward:0.85\n", + "step: 424, Red action: do-nothing, Blue reward:0.85\n", + "step: 425, Red action: do-nothing, Blue reward:0.85\n", + "step: 426, Red action: do-nothing, Blue reward:0.85\n", + "step: 427, Red action: do-nothing, Blue reward:0.85\n", + "step: 428, Red action: do-nothing, Blue reward:0.85\n", + "step: 429, Red action: do-nothing, Blue reward:0.85\n", + "step: 430, Red action: do-nothing, Blue reward:0.85\n", + "step: 431, Red action: do-nothing, Blue reward:0.85\n", + "step: 432, Red action: do-nothing, Blue reward:0.85\n", + "step: 433, Red action: do-nothing, Blue reward:0.85\n", + "step: 434, Red action: do-nothing, Blue reward:0.85\n", + "step: 435, Red action: do-nothing, Blue reward:0.85\n", + "step: 436, Red action: do-nothing, Blue reward:0.85\n", + "step: 437, Red action: do-nothing, Blue reward:0.85\n", + "step: 438, Red action: do-nothing, Blue reward:0.85\n", + "step: 439, Red action: do-nothing, Blue reward:0.85\n", + "step: 440, Red action: do-nothing, Blue reward:0.85\n", + "step: 441, Red action: do-nothing, Blue reward:0.85\n", + "step: 442, Red action: do-nothing, Blue reward:0.85\n", + "step: 443, Red action: node-application-execute, Blue reward:0.85\n", + "step: 444, Red action: do-nothing, Blue reward:0.85\n", + "step: 445, Red action: do-nothing, Blue reward:0.85\n", + "step: 446, Red action: do-nothing, Blue reward:0.85\n", + "step: 447, Red action: do-nothing, Blue reward:0.85\n", + "step: 448, Red action: do-nothing, Blue reward:0.85\n", + "step: 449, Red action: do-nothing, Blue reward:0.85\n", + "step: 450, Red action: do-nothing, Blue reward:0.85\n", + "step: 451, Red action: do-nothing, Blue reward:0.85\n", + "step: 452, Red action: do-nothing, Blue reward:0.85\n", + "step: 453, Red action: do-nothing, Blue reward:0.85\n", + "step: 454, Red action: do-nothing, Blue reward:0.85\n", + "step: 455, Red action: do-nothing, Blue reward:0.85\n", + "step: 456, Red action: do-nothing, Blue reward:0.85\n", + "step: 457, Red action: do-nothing, Blue reward:0.85\n", + "step: 458, Red action: do-nothing, Blue reward:0.85\n", + "step: 459, Red action: do-nothing, Blue reward:0.85\n", + "step: 460, Red action: do-nothing, Blue reward:0.85\n", + "step: 461, Red action: node-application-execute, Blue reward:0.85\n", + "step: 462, Red action: do-nothing, Blue reward:0.85\n", + "step: 463, Red action: do-nothing, Blue reward:0.85\n", + "step: 464, Red action: do-nothing, Blue reward:0.85\n", + "step: 465, Red action: do-nothing, Blue reward:0.85\n", + "step: 466, Red action: do-nothing, Blue reward:0.85\n", + "step: 467, Red action: do-nothing, Blue reward:0.85\n", + "step: 468, Red action: do-nothing, Blue reward:0.85\n", + "step: 469, Red action: do-nothing, Blue reward:0.85\n", + "step: 470, Red action: do-nothing, Blue reward:0.85\n", + "step: 471, Red action: do-nothing, Blue reward:0.85\n", + "step: 472, Red action: do-nothing, Blue reward:0.85\n", + "step: 473, Red action: do-nothing, Blue reward:0.85\n", + "step: 474, Red action: do-nothing, Blue reward:0.85\n", + "step: 475, Red action: do-nothing, Blue reward:0.85\n", + "step: 476, Red action: do-nothing, Blue reward:0.85\n", + "step: 477, Red action: do-nothing, Blue reward:0.85\n", + "step: 478, Red action: do-nothing, Blue reward:0.85\n", + "step: 479, Red action: do-nothing, Blue reward:0.85\n", + "step: 480, Red action: node-application-execute, Blue reward:0.85\n", + "step: 481, Red action: do-nothing, Blue reward:0.85\n", + "step: 482, Red action: do-nothing, Blue reward:0.85\n", + "step: 483, Red action: do-nothing, Blue reward:0.85\n", + "step: 484, Red action: do-nothing, Blue reward:0.85\n", + "step: 485, Red action: do-nothing, Blue reward:0.85\n", + "step: 486, Red action: do-nothing, Blue reward:0.85\n", + "step: 487, Red action: do-nothing, Blue reward:0.85\n", + "step: 488, Red action: do-nothing, Blue reward:0.85\n", + "step: 489, Red action: do-nothing, Blue reward:0.85\n", + "step: 490, Red action: do-nothing, Blue reward:0.85\n", + "step: 491, Red action: do-nothing, Blue reward:0.85\n", + "step: 492, Red action: do-nothing, Blue reward:0.85\n", + "step: 493, Red action: do-nothing, Blue reward:0.85\n", + "step: 494, Red action: do-nothing, Blue reward:0.85\n", + "step: 495, Red action: do-nothing, Blue reward:0.85\n", + "step: 496, Red action: do-nothing, Blue reward:0.85\n", + "step: 497, Red action: do-nothing, Blue reward:0.85\n", + "step: 498, Red action: do-nothing, Blue reward:0.85\n", + "step: 499, Red action: do-nothing, Blue reward:0.85\n", + "step: 500, Red action: do-nothing, Blue reward:0.85\n", + "step: 501, Red action: do-nothing, Blue reward:0.85\n", + "step: 502, Red action: do-nothing, Blue reward:0.85\n", + "step: 503, Red action: do-nothing, Blue reward:0.85\n", + "step: 504, Red action: node-application-execute, Blue reward:0.85\n", + "step: 505, Red action: do-nothing, Blue reward:0.85\n", + "step: 506, Red action: do-nothing, Blue reward:0.85\n", + "step: 507, Red action: do-nothing, Blue reward:0.85\n", + "step: 508, Red action: do-nothing, Blue reward:0.85\n", + "step: 509, Red action: do-nothing, Blue reward:0.85\n", + "step: 510, Red action: do-nothing, Blue reward:0.85\n", + "step: 511, Red action: do-nothing, Blue reward:0.85\n", + "step: 512, Red action: do-nothing, Blue reward:0.85\n", + "step: 513, Red action: do-nothing, Blue reward:0.85\n", + "step: 514, Red action: do-nothing, Blue reward:0.85\n", + "step: 515, Red action: do-nothing, Blue reward:0.85\n", + "step: 516, Red action: do-nothing, Blue reward:0.85\n", + "step: 517, Red action: do-nothing, Blue reward:0.85\n", + "step: 518, Red action: do-nothing, Blue reward:0.85\n", + "step: 519, Red action: do-nothing, Blue reward:0.85\n", + "step: 520, Red action: do-nothing, Blue reward:0.85\n", + "step: 521, Red action: do-nothing, Blue reward:0.85\n", + "step: 522, Red action: do-nothing, Blue reward:0.85\n", + "step: 523, Red action: do-nothing, Blue reward:0.85\n", + "step: 524, Red action: do-nothing, Blue reward:0.85\n", + "step: 525, Red action: do-nothing, Blue reward:0.85\n", + "step: 526, Red action: do-nothing, Blue reward:0.85\n", + "step: 527, Red action: node-application-execute, Blue reward:0.85\n", + "step: 528, Red action: do-nothing, Blue reward:0.85\n", + "step: 529, Red action: do-nothing, Blue reward:0.85\n", + "step: 530, Red action: do-nothing, Blue reward:0.85\n", + "step: 531, Red action: do-nothing, Blue reward:0.85\n", + "step: 532, Red action: do-nothing, Blue reward:0.85\n", + "step: 533, Red action: do-nothing, Blue reward:0.85\n", + "step: 534, Red action: do-nothing, Blue reward:0.85\n", + "step: 535, Red action: do-nothing, Blue reward:0.85\n", + "step: 536, Red action: do-nothing, Blue reward:0.85\n", + "step: 537, Red action: do-nothing, Blue reward:0.85\n", + "step: 538, Red action: do-nothing, Blue reward:0.85\n", + "step: 539, Red action: do-nothing, Blue reward:0.85\n", + "step: 540, Red action: do-nothing, Blue reward:0.85\n", + "step: 541, Red action: do-nothing, Blue reward:0.85\n", + "step: 542, Red action: do-nothing, Blue reward:0.85\n", + "step: 543, Red action: do-nothing, Blue reward:0.85\n", + "step: 544, Red action: do-nothing, Blue reward:0.85\n", + "step: 545, Red action: do-nothing, Blue reward:0.85\n", + "step: 546, Red action: do-nothing, Blue reward:0.85\n", + "step: 547, Red action: do-nothing, Blue reward:0.85\n", + "step: 548, Red action: do-nothing, Blue reward:0.85\n", + "step: 549, Red action: node-application-execute, Blue reward:0.85\n", + "step: 550, Red action: do-nothing, Blue reward:0.85\n", + "step: 551, Red action: do-nothing, Blue reward:0.85\n", + "step: 552, Red action: do-nothing, Blue reward:0.85\n", + "step: 553, Red action: do-nothing, Blue reward:0.85\n", + "step: 554, Red action: do-nothing, Blue reward:0.85\n", + "step: 555, Red action: do-nothing, Blue reward:0.85\n", + "step: 556, Red action: do-nothing, Blue reward:0.85\n", + "step: 557, Red action: do-nothing, Blue reward:0.85\n", + "step: 558, Red action: do-nothing, Blue reward:0.85\n", + "step: 559, Red action: do-nothing, Blue reward:0.85\n", + "step: 560, Red action: do-nothing, Blue reward:0.85\n", + "step: 561, Red action: do-nothing, Blue reward:0.85\n", + "step: 562, Red action: do-nothing, Blue reward:0.85\n", + "step: 563, Red action: do-nothing, Blue reward:0.85\n", + "step: 564, Red action: do-nothing, Blue reward:0.85\n", + "step: 565, Red action: node-application-execute, Blue reward:0.85\n", + "step: 566, Red action: do-nothing, Blue reward:0.85\n", + "step: 567, Red action: do-nothing, Blue reward:0.85\n", + "step: 568, Red action: do-nothing, Blue reward:0.85\n", + "step: 569, Red action: do-nothing, Blue reward:0.85\n", + "step: 570, Red action: do-nothing, Blue reward:0.85\n", + "step: 571, Red action: do-nothing, Blue reward:0.85\n", + "step: 572, Red action: do-nothing, Blue reward:0.85\n", + "step: 573, Red action: do-nothing, Blue reward:0.85\n", + "step: 574, Red action: do-nothing, Blue reward:0.85\n", + "step: 575, Red action: do-nothing, Blue reward:0.85\n", + "step: 576, Red action: do-nothing, Blue reward:0.85\n", + "step: 577, Red action: do-nothing, Blue reward:0.85\n", + "step: 578, Red action: do-nothing, Blue reward:0.85\n", + "step: 579, Red action: do-nothing, Blue reward:0.85\n", + "step: 580, Red action: do-nothing, Blue reward:0.85\n", + "step: 581, Red action: do-nothing, Blue reward:0.85\n", + "step: 582, Red action: do-nothing, Blue reward:0.85\n", + "step: 583, Red action: do-nothing, Blue reward:0.85\n", + "step: 584, Red action: do-nothing, Blue reward:0.85\n", + "step: 585, Red action: do-nothing, Blue reward:0.85\n", + "step: 586, Red action: do-nothing, Blue reward:0.85\n", + "step: 587, Red action: do-nothing, Blue reward:0.85\n", + "step: 588, Red action: do-nothing, Blue reward:0.85\n", + "step: 589, Red action: node-application-execute, Blue reward:0.85\n", + "step: 590, Red action: do-nothing, Blue reward:0.85\n", + "step: 591, Red action: do-nothing, Blue reward:0.85\n", + "step: 592, Red action: do-nothing, Blue reward:0.85\n", + "step: 593, Red action: do-nothing, Blue reward:0.85\n", + "step: 594, Red action: do-nothing, Blue reward:0.85\n", + "step: 595, Red action: do-nothing, Blue reward:0.85\n", + "step: 596, Red action: do-nothing, Blue reward:0.85\n", + "step: 597, Red action: do-nothing, Blue reward:0.85\n", + "step: 598, Red action: do-nothing, Blue reward:0.85\n", + "step: 599, Red action: do-nothing, Blue reward:0.85\n", + "step: 600, Red action: do-nothing, Blue reward:0.85\n", + "step: 601, Red action: do-nothing, Blue reward:0.85\n", + "step: 602, Red action: do-nothing, Blue reward:0.85\n", + "step: 603, Red action: do-nothing, Blue reward:0.85\n", + "step: 604, Red action: do-nothing, Blue reward:0.85\n", + "step: 605, Red action: do-nothing, Blue reward:0.85\n", + "step: 606, Red action: do-nothing, Blue reward:0.85\n", + "step: 607, Red action: do-nothing, Blue reward:0.85\n", + "step: 608, Red action: do-nothing, Blue reward:0.85\n", + "step: 609, Red action: do-nothing, Blue reward:0.85\n", + "step: 610, Red action: node-application-execute, Blue reward:0.85\n", + "step: 611, Red action: do-nothing, Blue reward:0.85\n", + "step: 612, Red action: do-nothing, Blue reward:0.85\n", + "step: 613, Red action: do-nothing, Blue reward:0.85\n", + "step: 614, Red action: do-nothing, Blue reward:0.85\n", + "step: 615, Red action: do-nothing, Blue reward:0.85\n", + "step: 616, Red action: do-nothing, Blue reward:0.85\n", + "step: 617, Red action: do-nothing, Blue reward:0.85\n", + "step: 618, Red action: do-nothing, Blue reward:0.85\n", + "step: 619, Red action: do-nothing, Blue reward:0.85\n", + "step: 620, Red action: do-nothing, Blue reward:0.85\n", + "step: 621, Red action: do-nothing, Blue reward:0.85\n", + "step: 622, Red action: do-nothing, Blue reward:0.85\n", + "step: 623, Red action: do-nothing, Blue reward:0.85\n", + "step: 624, Red action: do-nothing, Blue reward:0.85\n", + "step: 625, Red action: do-nothing, Blue reward:0.85\n", + "step: 626, Red action: node-application-execute, Blue reward:0.85\n", + "step: 627, Red action: do-nothing, Blue reward:0.85\n", + "step: 628, Red action: do-nothing, Blue reward:0.85\n", + "step: 629, Red action: do-nothing, Blue reward:0.85\n", + "step: 630, Red action: do-nothing, Blue reward:0.85\n", + "step: 631, Red action: do-nothing, Blue reward:0.85\n", + "step: 632, Red action: do-nothing, Blue reward:0.85\n", + "step: 633, Red action: do-nothing, Blue reward:0.85\n", + "step: 634, Red action: do-nothing, Blue reward:0.85\n", + "step: 635, Red action: do-nothing, Blue reward:0.85\n", + "step: 636, Red action: do-nothing, Blue reward:0.85\n", + "step: 637, Red action: do-nothing, Blue reward:0.85\n", + "step: 638, Red action: do-nothing, Blue reward:0.85\n", + "step: 639, Red action: do-nothing, Blue reward:0.85\n", + "step: 640, Red action: do-nothing, Blue reward:0.85\n", + "step: 641, Red action: do-nothing, Blue reward:0.85\n", + "step: 642, Red action: do-nothing, Blue reward:0.85\n", + "step: 643, Red action: do-nothing, Blue reward:0.85\n", + "step: 644, Red action: node-application-execute, Blue reward:0.85\n", + "step: 645, Red action: do-nothing, Blue reward:0.85\n", + "step: 646, Red action: do-nothing, Blue reward:0.85\n", + "step: 647, Red action: do-nothing, Blue reward:0.85\n", + "step: 648, Red action: do-nothing, Blue reward:0.85\n", + "step: 649, Red action: do-nothing, Blue reward:0.85\n", + "step: 650, Red action: do-nothing, Blue reward:0.85\n", + "step: 651, Red action: do-nothing, Blue reward:0.85\n", + "step: 652, Red action: do-nothing, Blue reward:0.85\n", + "step: 653, Red action: do-nothing, Blue reward:0.85\n", + "step: 654, Red action: do-nothing, Blue reward:0.85\n", + "step: 655, Red action: do-nothing, Blue reward:0.85\n", + "step: 656, Red action: do-nothing, Blue reward:0.85\n", + "step: 657, Red action: do-nothing, Blue reward:0.85\n", + "step: 658, Red action: do-nothing, Blue reward:0.85\n", + "step: 659, Red action: do-nothing, Blue reward:0.85\n", + "step: 660, Red action: do-nothing, Blue reward:0.85\n", + "step: 661, Red action: do-nothing, Blue reward:0.85\n", + "step: 662, Red action: do-nothing, Blue reward:0.85\n", + "step: 663, Red action: do-nothing, Blue reward:0.85\n", + "step: 664, Red action: do-nothing, Blue reward:0.85\n", + "step: 665, Red action: do-nothing, Blue reward:0.85\n", + "step: 666, Red action: do-nothing, Blue reward:0.85\n", + "step: 667, Red action: node-application-execute, Blue reward:0.85\n", + "step: 668, Red action: do-nothing, Blue reward:0.85\n", + "step: 669, Red action: do-nothing, Blue reward:0.85\n", + "step: 670, Red action: do-nothing, Blue reward:0.85\n", + "step: 671, Red action: do-nothing, Blue reward:0.85\n", + "step: 672, Red action: do-nothing, Blue reward:0.85\n", + "step: 673, Red action: do-nothing, Blue reward:0.85\n", + "step: 674, Red action: do-nothing, Blue reward:0.85\n", + "step: 675, Red action: do-nothing, Blue reward:0.85\n", + "step: 676, Red action: do-nothing, Blue reward:0.85\n", + "step: 677, Red action: do-nothing, Blue reward:0.85\n", + "step: 678, Red action: do-nothing, Blue reward:0.85\n", + "step: 679, Red action: do-nothing, Blue reward:0.85\n", + "step: 680, Red action: do-nothing, Blue reward:0.85\n", + "step: 681, Red action: do-nothing, Blue reward:0.85\n", + "step: 682, Red action: do-nothing, Blue reward:0.85\n", + "step: 683, Red action: do-nothing, Blue reward:0.85\n", + "step: 684, Red action: do-nothing, Blue reward:0.85\n", + "step: 685, Red action: node-application-execute, Blue reward:0.85\n", + "step: 686, Red action: do-nothing, Blue reward:0.85\n", + "step: 687, Red action: do-nothing, Blue reward:0.85\n", + "step: 688, Red action: do-nothing, Blue reward:0.85\n", + "step: 689, Red action: do-nothing, Blue reward:0.85\n", + "step: 690, Red action: do-nothing, Blue reward:0.85\n", + "step: 691, Red action: do-nothing, Blue reward:0.85\n", + "step: 692, Red action: do-nothing, Blue reward:0.85\n", + "step: 693, Red action: do-nothing, Blue reward:0.85\n", + "step: 694, Red action: do-nothing, Blue reward:0.85\n", + "step: 695, Red action: do-nothing, Blue reward:0.85\n", + "step: 696, Red action: do-nothing, Blue reward:0.85\n", + "step: 697, Red action: do-nothing, Blue reward:0.85\n", + "step: 698, Red action: do-nothing, Blue reward:0.85\n", + "step: 699, Red action: do-nothing, Blue reward:0.85\n", + "step: 700, Red action: do-nothing, Blue reward:0.85\n", + "step: 701, Red action: node-application-execute, Blue reward:0.85\n", + "step: 702, Red action: do-nothing, Blue reward:0.85\n", + "step: 703, Red action: do-nothing, Blue reward:0.85\n", + "step: 704, Red action: do-nothing, Blue reward:0.85\n", + "step: 705, Red action: do-nothing, Blue reward:0.85\n", + "step: 706, Red action: do-nothing, Blue reward:0.85\n", + "step: 707, Red action: do-nothing, Blue reward:0.85\n", + "step: 708, Red action: do-nothing, Blue reward:0.85\n", + "step: 709, Red action: do-nothing, Blue reward:0.85\n", + "step: 710, Red action: do-nothing, Blue reward:0.85\n", + "step: 711, Red action: do-nothing, Blue reward:0.85\n", + "step: 712, Red action: do-nothing, Blue reward:0.85\n", + "step: 713, Red action: do-nothing, Blue reward:0.85\n", + "step: 714, Red action: do-nothing, Blue reward:0.85\n", + "step: 715, Red action: do-nothing, Blue reward:0.85\n", + "step: 716, Red action: do-nothing, Blue reward:0.85\n", + "step: 717, Red action: do-nothing, Blue reward:0.85\n", + "step: 718, Red action: node-application-execute, Blue reward:0.85\n", + "step: 719, Red action: do-nothing, Blue reward:0.85\n", + "step: 720, Red action: do-nothing, Blue reward:0.85\n", + "step: 721, Red action: do-nothing, Blue reward:0.85\n", + "step: 722, Red action: do-nothing, Blue reward:0.85\n", + "step: 723, Red action: do-nothing, Blue reward:0.85\n", + "step: 724, Red action: do-nothing, Blue reward:0.85\n", + "step: 725, Red action: do-nothing, Blue reward:0.85\n", + "step: 726, Red action: do-nothing, Blue reward:0.85\n", + "step: 727, Red action: do-nothing, Blue reward:0.85\n", + "step: 728, Red action: do-nothing, Blue reward:0.85\n", + "step: 729, Red action: do-nothing, Blue reward:0.85\n", + "step: 730, Red action: do-nothing, Blue reward:0.85\n", + "step: 731, Red action: do-nothing, Blue reward:0.85\n", + "step: 732, Red action: do-nothing, Blue reward:0.85\n", + "step: 733, Red action: do-nothing, Blue reward:0.85\n", + "step: 734, Red action: do-nothing, Blue reward:0.85\n", + "step: 735, Red action: do-nothing, Blue reward:0.85\n", + "step: 736, Red action: do-nothing, Blue reward:0.85\n", + "step: 737, Red action: do-nothing, Blue reward:0.85\n", + "step: 738, Red action: do-nothing, Blue reward:0.85\n", + "step: 739, Red action: do-nothing, Blue reward:0.85\n", + "step: 740, Red action: do-nothing, Blue reward:0.85\n", + "step: 741, Red action: do-nothing, Blue reward:0.85\n", + "step: 742, Red action: do-nothing, Blue reward:0.85\n", + "step: 743, Red action: node-application-execute, Blue reward:0.85\n", + "step: 744, Red action: do-nothing, Blue reward:0.85\n", + "step: 745, Red action: do-nothing, Blue reward:0.85\n", + "step: 746, Red action: do-nothing, Blue reward:0.85\n", + "step: 747, Red action: do-nothing, Blue reward:0.85\n", + "step: 748, Red action: do-nothing, Blue reward:0.85\n", + "step: 749, Red action: do-nothing, Blue reward:0.85\n", + "step: 750, Red action: do-nothing, Blue reward:0.85\n", + "step: 751, Red action: do-nothing, Blue reward:0.85\n", + "step: 752, Red action: do-nothing, Blue reward:0.85\n", + "step: 753, Red action: do-nothing, Blue reward:0.85\n", + "step: 754, Red action: do-nothing, Blue reward:0.85\n", + "step: 755, Red action: do-nothing, Blue reward:0.85\n", + "step: 756, Red action: do-nothing, Blue reward:0.85\n", + "step: 757, Red action: do-nothing, Blue reward:0.85\n", + "step: 758, Red action: do-nothing, Blue reward:0.85\n", + "step: 759, Red action: do-nothing, Blue reward:0.85\n", + "step: 760, Red action: node-application-execute, Blue reward:0.85\n", + "step: 761, Red action: do-nothing, Blue reward:0.85\n", + "step: 762, Red action: do-nothing, Blue reward:0.85\n", + "step: 763, Red action: do-nothing, Blue reward:0.85\n", + "step: 764, Red action: do-nothing, Blue reward:0.85\n", + "step: 765, Red action: do-nothing, Blue reward:0.85\n", + "step: 766, Red action: do-nothing, Blue reward:0.85\n", + "step: 767, Red action: do-nothing, Blue reward:0.85\n", + "step: 768, Red action: do-nothing, Blue reward:0.85\n", + "step: 769, Red action: do-nothing, Blue reward:0.85\n", + "step: 770, Red action: do-nothing, Blue reward:0.85\n", + "step: 771, Red action: do-nothing, Blue reward:0.85\n", + "step: 772, Red action: do-nothing, Blue reward:0.85\n", + "step: 773, Red action: do-nothing, Blue reward:0.85\n", + "step: 774, Red action: do-nothing, Blue reward:0.85\n", + "step: 775, Red action: do-nothing, Blue reward:0.85\n", + "step: 776, Red action: do-nothing, Blue reward:0.85\n", + "step: 777, Red action: do-nothing, Blue reward:0.85\n", + "step: 778, Red action: do-nothing, Blue reward:0.85\n", + "step: 779, Red action: do-nothing, Blue reward:0.85\n", + "step: 780, Red action: do-nothing, Blue reward:0.85\n", + "step: 781, Red action: do-nothing, Blue reward:0.85\n", + "step: 782, Red action: do-nothing, Blue reward:0.85\n", + "step: 783, Red action: node-application-execute, Blue reward:0.85\n", + "step: 784, Red action: do-nothing, Blue reward:0.85\n", + "step: 785, Red action: do-nothing, Blue reward:0.85\n", + "step: 786, Red action: do-nothing, Blue reward:0.85\n", + "step: 787, Red action: do-nothing, Blue reward:0.85\n", + "step: 788, Red action: do-nothing, Blue reward:0.85\n", + "step: 789, Red action: do-nothing, Blue reward:0.85\n", + "step: 790, Red action: do-nothing, Blue reward:0.85\n", + "step: 791, Red action: do-nothing, Blue reward:0.85\n", + "step: 792, Red action: do-nothing, Blue reward:0.85\n", + "step: 793, Red action: do-nothing, Blue reward:0.85\n", + "step: 794, Red action: do-nothing, Blue reward:0.85\n", + "step: 795, Red action: do-nothing, Blue reward:0.85\n", + "step: 796, Red action: do-nothing, Blue reward:0.85\n", + "step: 797, Red action: do-nothing, Blue reward:0.85\n", + "step: 798, Red action: do-nothing, Blue reward:0.85\n", + "step: 799, Red action: do-nothing, Blue reward:0.85\n", + "step: 800, Red action: do-nothing, Blue reward:0.85\n", + "step: 801, Red action: do-nothing, Blue reward:0.85\n", + "step: 802, Red action: do-nothing, Blue reward:0.85\n", + "step: 803, Red action: node-application-execute, Blue reward:0.85\n", + "step: 804, Red action: do-nothing, Blue reward:0.85\n", + "step: 805, Red action: do-nothing, Blue reward:0.85\n", + "step: 806, Red action: do-nothing, Blue reward:0.85\n", + "step: 807, Red action: do-nothing, Blue reward:0.85\n", + "step: 808, Red action: do-nothing, Blue reward:0.85\n", + "step: 809, Red action: do-nothing, Blue reward:0.85\n", + "step: 810, Red action: do-nothing, Blue reward:0.85\n", + "step: 811, Red action: do-nothing, Blue reward:0.85\n", + "step: 812, Red action: do-nothing, Blue reward:0.85\n", + "step: 813, Red action: do-nothing, Blue reward:0.85\n", + "step: 814, Red action: do-nothing, Blue reward:0.85\n", + "step: 815, Red action: do-nothing, Blue reward:0.85\n", + "step: 816, Red action: do-nothing, Blue reward:0.85\n", + "step: 817, Red action: do-nothing, Blue reward:0.85\n", + "step: 818, Red action: do-nothing, Blue reward:0.85\n", + "step: 819, Red action: do-nothing, Blue reward:0.85\n", + "step: 820, Red action: do-nothing, Blue reward:0.85\n", + "step: 821, Red action: do-nothing, Blue reward:0.85\n", + "step: 822, Red action: do-nothing, Blue reward:0.85\n", + "step: 823, Red action: do-nothing, Blue reward:0.85\n", + "step: 824, Red action: do-nothing, Blue reward:0.85\n", + "step: 825, Red action: do-nothing, Blue reward:0.85\n", + "step: 826, Red action: do-nothing, Blue reward:0.85\n", + "step: 827, Red action: node-application-execute, Blue reward:0.85\n", + "step: 828, Red action: do-nothing, Blue reward:0.85\n", + "step: 829, Red action: do-nothing, Blue reward:0.85\n", + "step: 830, Red action: do-nothing, Blue reward:0.85\n", + "step: 831, Red action: do-nothing, Blue reward:0.85\n", + "step: 832, Red action: do-nothing, Blue reward:0.85\n", + "step: 833, Red action: do-nothing, Blue reward:0.85\n", + "step: 834, Red action: do-nothing, Blue reward:0.85\n", + "step: 835, Red action: do-nothing, Blue reward:0.85\n", + "step: 836, Red action: do-nothing, Blue reward:0.85\n", + "step: 837, Red action: do-nothing, Blue reward:0.85\n", + "step: 838, Red action: do-nothing, Blue reward:0.85\n", + "step: 839, Red action: do-nothing, Blue reward:0.85\n", + "step: 840, Red action: do-nothing, Blue reward:0.85\n", + "step: 841, Red action: do-nothing, Blue reward:0.85\n", + "step: 842, Red action: do-nothing, Blue reward:0.85\n", + "step: 843, Red action: do-nothing, Blue reward:0.85\n", + "step: 844, Red action: do-nothing, Blue reward:0.85\n", + "step: 845, Red action: do-nothing, Blue reward:0.85\n", + "step: 846, Red action: do-nothing, Blue reward:0.85\n", + "step: 847, Red action: node-application-execute, Blue reward:0.85\n", + "step: 848, Red action: do-nothing, Blue reward:0.85\n", + "step: 849, Red action: do-nothing, Blue reward:0.85\n", + "step: 850, Red action: do-nothing, Blue reward:0.85\n", + "step: 851, Red action: do-nothing, Blue reward:0.85\n", + "step: 852, Red action: do-nothing, Blue reward:0.85\n", + "step: 853, Red action: do-nothing, Blue reward:0.85\n", + "step: 854, Red action: do-nothing, Blue reward:0.85\n", + "step: 855, Red action: do-nothing, Blue reward:0.85\n", + "step: 856, Red action: do-nothing, Blue reward:0.85\n", + "step: 857, Red action: do-nothing, Blue reward:0.85\n", + "step: 858, Red action: do-nothing, Blue reward:0.85\n", + "step: 859, Red action: do-nothing, Blue reward:0.85\n", + "step: 860, Red action: do-nothing, Blue reward:0.85\n", + "step: 861, Red action: do-nothing, Blue reward:0.85\n", + "step: 862, Red action: do-nothing, Blue reward:0.85\n", + "step: 863, Red action: do-nothing, Blue reward:0.85\n", + "step: 864, Red action: do-nothing, Blue reward:0.85\n", + "step: 865, Red action: do-nothing, Blue reward:0.85\n", + "step: 866, Red action: do-nothing, Blue reward:0.85\n", + "step: 867, Red action: do-nothing, Blue reward:0.85\n", + "step: 868, Red action: do-nothing, Blue reward:0.85\n", + "step: 869, Red action: do-nothing, Blue reward:0.85\n", + "step: 870, Red action: do-nothing, Blue reward:0.85\n", + "step: 871, Red action: node-application-execute, Blue reward:0.85\n", + "step: 872, Red action: do-nothing, Blue reward:0.85\n", + "step: 873, Red action: do-nothing, Blue reward:0.85\n", + "step: 874, Red action: do-nothing, Blue reward:0.85\n", + "step: 875, Red action: do-nothing, Blue reward:0.85\n", + "step: 876, Red action: do-nothing, Blue reward:0.85\n", + "step: 877, Red action: do-nothing, Blue reward:0.85\n", + "step: 878, Red action: do-nothing, Blue reward:0.85\n", + "step: 879, Red action: do-nothing, Blue reward:0.85\n", + "step: 880, Red action: do-nothing, Blue reward:0.85\n", + "step: 881, Red action: do-nothing, Blue reward:0.85\n", + "step: 882, Red action: do-nothing, Blue reward:0.85\n", + "step: 883, Red action: do-nothing, Blue reward:0.85\n", + "step: 884, Red action: do-nothing, Blue reward:0.85\n", + "step: 885, Red action: do-nothing, Blue reward:0.85\n", + "step: 886, Red action: do-nothing, Blue reward:0.85\n", + "step: 887, Red action: do-nothing, Blue reward:0.85\n", + "step: 888, Red action: do-nothing, Blue reward:0.85\n", + "step: 889, Red action: do-nothing, Blue reward:0.85\n", + "step: 890, Red action: do-nothing, Blue reward:0.85\n", + "step: 891, Red action: node-application-execute, Blue reward:0.85\n", + "step: 892, Red action: do-nothing, Blue reward:0.85\n", + "step: 893, Red action: do-nothing, Blue reward:0.85\n", + "step: 894, Red action: do-nothing, Blue reward:0.85\n", + "step: 895, Red action: do-nothing, Blue reward:0.85\n", + "step: 896, Red action: do-nothing, Blue reward:0.85\n", + "step: 897, Red action: do-nothing, Blue reward:0.85\n", + "step: 898, Red action: do-nothing, Blue reward:0.85\n", + "step: 899, Red action: do-nothing, Blue reward:0.85\n", + "step: 900, Red action: do-nothing, Blue reward:0.85\n", + "step: 901, Red action: do-nothing, Blue reward:0.85\n", + "step: 902, Red action: do-nothing, Blue reward:0.85\n", + "step: 903, Red action: do-nothing, Blue reward:0.85\n", + "step: 904, Red action: do-nothing, Blue reward:0.85\n", + "step: 905, Red action: do-nothing, Blue reward:0.85\n", + "step: 906, Red action: node-application-execute, Blue reward:0.85\n", + "step: 907, Red action: do-nothing, Blue reward:0.85\n", + "step: 908, Red action: do-nothing, Blue reward:0.85\n", + "step: 909, Red action: do-nothing, Blue reward:0.85\n", + "step: 910, Red action: do-nothing, Blue reward:0.85\n", + "step: 911, Red action: do-nothing, Blue reward:0.85\n", + "step: 912, Red action: do-nothing, Blue reward:0.85\n", + "step: 913, Red action: do-nothing, Blue reward:0.85\n", + "step: 914, Red action: do-nothing, Blue reward:0.85\n", + "step: 915, Red action: do-nothing, Blue reward:0.85\n", + "step: 916, Red action: do-nothing, Blue reward:0.85\n", + "step: 917, Red action: do-nothing, Blue reward:0.85\n", + "step: 918, Red action: do-nothing, Blue reward:0.85\n", + "step: 919, Red action: do-nothing, Blue reward:0.85\n", + "step: 920, Red action: do-nothing, Blue reward:0.85\n", + "step: 921, Red action: do-nothing, Blue reward:0.85\n", + "step: 922, Red action: do-nothing, Blue reward:0.85\n", + "step: 923, Red action: do-nothing, Blue reward:0.85\n", + "step: 924, Red action: do-nothing, Blue reward:0.85\n", + "step: 925, Red action: do-nothing, Blue reward:0.85\n", + "step: 926, Red action: do-nothing, Blue reward:0.85\n", + "step: 927, Red action: do-nothing, Blue reward:0.85\n", + "step: 928, Red action: do-nothing, Blue reward:0.85\n", + "step: 929, Red action: do-nothing, Blue reward:0.85\n", + "step: 930, Red action: node-application-execute, Blue reward:0.85\n", + "step: 931, Red action: do-nothing, Blue reward:0.85\n", + "step: 932, Red action: do-nothing, Blue reward:0.85\n", + "step: 933, Red action: do-nothing, Blue reward:0.85\n", + "step: 934, Red action: do-nothing, Blue reward:0.85\n", + "step: 935, Red action: do-nothing, Blue reward:0.85\n", + "step: 936, Red action: do-nothing, Blue reward:0.85\n", + "step: 937, Red action: do-nothing, Blue reward:0.85\n", + "step: 938, Red action: do-nothing, Blue reward:0.85\n", + "step: 939, Red action: do-nothing, Blue reward:0.85\n", + "step: 940, Red action: do-nothing, Blue reward:0.85\n", + "step: 941, Red action: do-nothing, Blue reward:0.85\n", + "step: 942, Red action: do-nothing, Blue reward:0.85\n", + "step: 943, Red action: do-nothing, Blue reward:0.85\n", + "step: 944, Red action: do-nothing, Blue reward:0.85\n", + "step: 945, Red action: do-nothing, Blue reward:0.85\n", + "step: 946, Red action: do-nothing, Blue reward:0.85\n", + "step: 947, Red action: node-application-execute, Blue reward:0.85\n", + "step: 948, Red action: do-nothing, Blue reward:0.85\n", + "step: 949, Red action: do-nothing, Blue reward:0.85\n", + "step: 950, Red action: do-nothing, Blue reward:0.85\n", + "step: 951, Red action: do-nothing, Blue reward:0.85\n", + "step: 952, Red action: do-nothing, Blue reward:0.85\n", + "step: 953, Red action: do-nothing, Blue reward:0.85\n", + "step: 954, Red action: do-nothing, Blue reward:0.85\n", + "step: 955, Red action: do-nothing, Blue reward:0.85\n", + "step: 956, Red action: do-nothing, Blue reward:0.85\n", + "step: 957, Red action: do-nothing, Blue reward:0.85\n", + "step: 958, Red action: do-nothing, Blue reward:0.85\n", + "step: 959, Red action: do-nothing, Blue reward:0.85\n", + "step: 960, Red action: do-nothing, Blue reward:0.85\n", + "step: 961, Red action: do-nothing, Blue reward:0.85\n", + "step: 962, Red action: do-nothing, Blue reward:0.85\n", + "step: 963, Red action: node-application-execute, Blue reward:0.85\n", + "step: 964, Red action: do-nothing, Blue reward:0.85\n", + "step: 965, Red action: do-nothing, Blue reward:0.85\n", + "step: 966, Red action: do-nothing, Blue reward:0.85\n", + "step: 967, Red action: do-nothing, Blue reward:0.85\n", + "step: 968, Red action: do-nothing, Blue reward:0.85\n", + "step: 969, Red action: do-nothing, Blue reward:0.85\n", + "step: 970, Red action: do-nothing, Blue reward:0.85\n", + "step: 971, Red action: do-nothing, Blue reward:0.85\n", + "step: 972, Red action: do-nothing, Blue reward:0.85\n", + "step: 973, Red action: do-nothing, Blue reward:0.85\n", + "step: 974, Red action: do-nothing, Blue reward:0.85\n", + "step: 975, Red action: do-nothing, Blue reward:0.85\n", + "step: 976, Red action: do-nothing, Blue reward:0.85\n", + "step: 977, Red action: do-nothing, Blue reward:0.85\n", + "step: 978, Red action: do-nothing, Blue reward:0.85\n", + "step: 979, Red action: do-nothing, Blue reward:0.85\n", + "step: 980, Red action: do-nothing, Blue reward:0.85\n", + "step: 981, Red action: do-nothing, Blue reward:0.85\n", + "step: 982, Red action: do-nothing, Blue reward:0.85\n", + "step: 983, Red action: do-nothing, Blue reward:0.85\n", + "step: 984, Red action: do-nothing, Blue reward:0.85\n", + "step: 985, Red action: do-nothing, Blue reward:0.85\n", + "step: 986, Red action: do-nothing, Blue reward:0.85\n", + "step: 987, Red action: do-nothing, Blue reward:0.85\n", + "step: 988, Red action: node-application-execute, Blue reward:0.85\n", + "step: 989, Red action: do-nothing, Blue reward:0.85\n", + "step: 990, Red action: do-nothing, Blue reward:0.85\n", + "step: 991, Red action: do-nothing, Blue reward:0.85\n", + "step: 992, Red action: do-nothing, Blue reward:0.85\n", + "step: 993, Red action: do-nothing, Blue reward:0.85\n", + "step: 994, Red action: do-nothing, Blue reward:0.85\n", + "step: 995, Red action: do-nothing, Blue reward:0.85\n", + "step: 996, Red action: do-nothing, Blue reward:0.85\n", + "step: 997, Red action: do-nothing, Blue reward:0.85\n", + "step: 998, Red action: do-nothing, Blue reward:0.85\n", + "step: 999, Red action: do-nothing, Blue reward:0.85\n", + "step: 1000, Red action: do-nothing, Blue reward:0.85\n", + "step: 1001, Red action: do-nothing, Blue reward:0.85\n", + "step: 1002, Red action: do-nothing, Blue reward:0.85\n", + "step: 1003, Red action: do-nothing, Blue reward:0.85\n", + "step: 1004, Red action: do-nothing, Blue reward:0.85\n", + "step: 1005, Red action: do-nothing, Blue reward:0.85\n", + "step: 1006, Red action: do-nothing, Blue reward:0.85\n", + "step: 1007, Red action: do-nothing, Blue reward:0.85\n", + "step: 1008, Red action: do-nothing, Blue reward:0.85\n", + "step: 1009, Red action: do-nothing, Blue reward:0.85\n", + "step: 1010, Red action: do-nothing, Blue reward:0.85\n", + "step: 1011, Red action: do-nothing, Blue reward:0.85\n", + "step: 1012, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1013, Red action: do-nothing, Blue reward:0.85\n", + "step: 1014, Red action: do-nothing, Blue reward:0.85\n", + "step: 1015, Red action: do-nothing, Blue reward:0.85\n", + "step: 1016, Red action: do-nothing, Blue reward:0.85\n", + "step: 1017, Red action: do-nothing, Blue reward:0.85\n", + "step: 1018, Red action: do-nothing, Blue reward:0.85\n", + "step: 1019, Red action: do-nothing, Blue reward:0.85\n", + "step: 1020, Red action: do-nothing, Blue reward:0.85\n", + "step: 1021, Red action: do-nothing, Blue reward:0.85\n", + "step: 1022, Red action: do-nothing, Blue reward:0.85\n", + "step: 1023, Red action: do-nothing, Blue reward:0.85\n", + "step: 1024, Red action: do-nothing, Blue reward:0.85\n", + "step: 1025, Red action: do-nothing, Blue reward:0.85\n", + "step: 1026, Red action: do-nothing, Blue reward:0.85\n", + "step: 1027, Red action: do-nothing, Blue reward:0.85\n", + "step: 1028, Red action: do-nothing, Blue reward:0.85\n", + "step: 1029, Red action: do-nothing, Blue reward:0.85\n", + "step: 1030, Red action: do-nothing, Blue reward:0.85\n", + "step: 1031, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1032, Red action: do-nothing, Blue reward:0.85\n", + "step: 1033, Red action: do-nothing, Blue reward:0.85\n", + "step: 1034, Red action: do-nothing, Blue reward:0.85\n", + "step: 1035, Red action: do-nothing, Blue reward:0.85\n", + "step: 1036, Red action: do-nothing, Blue reward:0.85\n", + "step: 1037, Red action: do-nothing, Blue reward:0.85\n", + "step: 1038, Red action: do-nothing, Blue reward:0.85\n", + "step: 1039, Red action: do-nothing, Blue reward:0.85\n", + "step: 1040, Red action: do-nothing, Blue reward:0.85\n", + "step: 1041, Red action: do-nothing, Blue reward:0.85\n", + "step: 1042, Red action: do-nothing, Blue reward:0.85\n", + "step: 1043, Red action: do-nothing, Blue reward:0.85\n", + "step: 1044, Red action: do-nothing, Blue reward:0.85\n", + "step: 1045, Red action: do-nothing, Blue reward:0.85\n", + "step: 1046, Red action: do-nothing, Blue reward:0.85\n", + "step: 1047, Red action: do-nothing, Blue reward:0.85\n", + "step: 1048, Red action: do-nothing, Blue reward:0.85\n", + "step: 1049, Red action: do-nothing, Blue reward:0.85\n", + "step: 1050, Red action: do-nothing, Blue reward:0.85\n", + "step: 1051, Red action: do-nothing, Blue reward:0.85\n", + "step: 1052, Red action: do-nothing, Blue reward:0.85\n", + "step: 1053, Red action: do-nothing, Blue reward:0.85\n", + "step: 1054, Red action: do-nothing, Blue reward:0.85\n", + "step: 1055, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1056, Red action: do-nothing, Blue reward:0.85\n", + "step: 1057, Red action: do-nothing, Blue reward:0.85\n", + "step: 1058, Red action: do-nothing, Blue reward:0.85\n", + "step: 1059, Red action: do-nothing, Blue reward:0.85\n", + "step: 1060, Red action: do-nothing, Blue reward:0.85\n", + "step: 1061, Red action: do-nothing, Blue reward:0.85\n", + "step: 1062, Red action: do-nothing, Blue reward:0.85\n", + "step: 1063, Red action: do-nothing, Blue reward:0.85\n", + "step: 1064, Red action: do-nothing, Blue reward:0.85\n", + "step: 1065, Red action: do-nothing, Blue reward:0.85\n", + "step: 1066, Red action: do-nothing, Blue reward:0.85\n", + "step: 1067, Red action: do-nothing, Blue reward:0.85\n", + "step: 1068, Red action: do-nothing, Blue reward:0.85\n", + "step: 1069, Red action: do-nothing, Blue reward:0.85\n", + "step: 1070, Red action: do-nothing, Blue reward:0.85\n", + "step: 1071, Red action: do-nothing, Blue reward:0.85\n", + "step: 1072, Red action: do-nothing, Blue reward:0.85\n", + "step: 1073, Red action: do-nothing, Blue reward:0.85\n", + "step: 1074, Red action: do-nothing, Blue reward:0.85\n", + "step: 1075, Red action: do-nothing, Blue reward:0.85\n", + "step: 1076, Red action: do-nothing, Blue reward:0.85\n", + "step: 1077, Red action: do-nothing, Blue reward:0.85\n", + "step: 1078, Red action: do-nothing, Blue reward:0.85\n", + "step: 1079, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1080, Red action: do-nothing, Blue reward:0.85\n", + "step: 1081, Red action: do-nothing, Blue reward:0.85\n", + "step: 1082, Red action: do-nothing, Blue reward:0.85\n", + "step: 1083, Red action: do-nothing, Blue reward:0.85\n", + "step: 1084, Red action: do-nothing, Blue reward:0.85\n", + "step: 1085, Red action: do-nothing, Blue reward:0.85\n", + "step: 1086, Red action: do-nothing, Blue reward:0.85\n", + "step: 1087, Red action: do-nothing, Blue reward:0.85\n", + "step: 1088, Red action: do-nothing, Blue reward:0.85\n", + "step: 1089, Red action: do-nothing, Blue reward:0.85\n", + "step: 1090, Red action: do-nothing, Blue reward:0.85\n", + "step: 1091, Red action: do-nothing, Blue reward:0.85\n", + "step: 1092, Red action: do-nothing, Blue reward:0.85\n", + "step: 1093, Red action: do-nothing, Blue reward:0.85\n", + "step: 1094, Red action: do-nothing, Blue reward:0.85\n", + "step: 1095, Red action: do-nothing, Blue reward:0.85\n", + "step: 1096, Red action: do-nothing, Blue reward:0.85\n", + "step: 1097, Red action: do-nothing, Blue reward:0.85\n", + "step: 1098, Red action: do-nothing, Blue reward:0.85\n", + "step: 1099, Red action: do-nothing, Blue reward:0.85\n", + "step: 1100, Red action: do-nothing, Blue reward:0.85\n", + "step: 1101, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1102, Red action: do-nothing, Blue reward:0.85\n", + "step: 1103, Red action: do-nothing, Blue reward:0.85\n", + "step: 1104, Red action: do-nothing, Blue reward:0.85\n", + "step: 1105, Red action: do-nothing, Blue reward:0.85\n", + "step: 1106, Red action: do-nothing, Blue reward:0.85\n", + "step: 1107, Red action: do-nothing, Blue reward:0.85\n", + "step: 1108, Red action: do-nothing, Blue reward:0.85\n", + "step: 1109, Red action: do-nothing, Blue reward:0.85\n", + "step: 1110, Red action: do-nothing, Blue reward:0.85\n", + "step: 1111, Red action: do-nothing, Blue reward:0.85\n", + "step: 1112, Red action: do-nothing, Blue reward:0.85\n", + "step: 1113, Red action: do-nothing, Blue reward:0.85\n", + "step: 1114, Red action: do-nothing, Blue reward:0.85\n", + "step: 1115, Red action: do-nothing, Blue reward:0.85\n", + "step: 1116, Red action: do-nothing, Blue reward:0.85\n", + "step: 1117, Red action: do-nothing, Blue reward:0.85\n", + "step: 1118, Red action: do-nothing, Blue reward:0.85\n", + "step: 1119, Red action: do-nothing, Blue reward:0.85\n", + "step: 1120, Red action: do-nothing, Blue reward:0.85\n", + "step: 1121, Red action: do-nothing, Blue reward:0.85\n", + "step: 1122, Red action: do-nothing, Blue reward:0.85\n", + "step: 1123, Red action: do-nothing, Blue reward:0.85\n", + "step: 1124, Red action: do-nothing, Blue reward:0.85\n", + "step: 1125, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1126, Red action: do-nothing, Blue reward:0.85\n", + "step: 1127, Red action: do-nothing, Blue reward:0.85\n", + "step: 1128, Red action: do-nothing, Blue reward:0.85\n", + "step: 1129, Red action: do-nothing, Blue reward:0.85\n", + "step: 1130, Red action: do-nothing, Blue reward:0.85\n", + "step: 1131, Red action: do-nothing, Blue reward:0.85\n", + "step: 1132, Red action: do-nothing, Blue reward:0.85\n", + "step: 1133, Red action: do-nothing, Blue reward:0.85\n", + "step: 1134, Red action: do-nothing, Blue reward:0.85\n", + "step: 1135, Red action: do-nothing, Blue reward:0.85\n", + "step: 1136, Red action: do-nothing, Blue reward:0.85\n", + "step: 1137, Red action: do-nothing, Blue reward:0.85\n", + "step: 1138, Red action: do-nothing, Blue reward:0.85\n", + "step: 1139, Red action: do-nothing, Blue reward:0.85\n", + "step: 1140, Red action: do-nothing, Blue reward:0.85\n", + "step: 1141, Red action: do-nothing, Blue reward:0.85\n", + "step: 1142, Red action: do-nothing, Blue reward:0.85\n", + "step: 1143, Red action: do-nothing, Blue reward:0.85\n", + "step: 1144, Red action: do-nothing, Blue reward:0.85\n", + "step: 1145, Red action: do-nothing, Blue reward:0.85\n", + "step: 1146, Red action: do-nothing, Blue reward:0.85\n", + "step: 1147, Red action: do-nothing, Blue reward:0.85\n", + "step: 1148, Red action: do-nothing, Blue reward:0.85\n", + "step: 1149, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1150, Red action: do-nothing, Blue reward:0.85\n", + "step: 1151, Red action: do-nothing, Blue reward:0.85\n", + "step: 1152, Red action: do-nothing, Blue reward:0.85\n", + "step: 1153, Red action: do-nothing, Blue reward:0.85\n", + "step: 1154, Red action: do-nothing, Blue reward:0.85\n", + "step: 1155, Red action: do-nothing, Blue reward:0.85\n", + "step: 1156, Red action: do-nothing, Blue reward:0.85\n", + "step: 1157, Red action: do-nothing, Blue reward:0.85\n", + "step: 1158, Red action: do-nothing, Blue reward:0.85\n", + "step: 1159, Red action: do-nothing, Blue reward:0.85\n", + "step: 1160, Red action: do-nothing, Blue reward:0.85\n", + "step: 1161, Red action: do-nothing, Blue reward:0.85\n", + "step: 1162, Red action: do-nothing, Blue reward:0.85\n", + "step: 1163, Red action: do-nothing, Blue reward:0.85\n", + "step: 1164, Red action: do-nothing, Blue reward:0.85\n", + "step: 1165, Red action: do-nothing, Blue reward:0.85\n", + "step: 1166, Red action: do-nothing, Blue reward:0.85\n", + "step: 1167, Red action: do-nothing, Blue reward:0.85\n", + "step: 1168, Red action: do-nothing, Blue reward:0.85\n", + "step: 1169, Red action: do-nothing, Blue reward:0.85\n", + "step: 1170, Red action: do-nothing, Blue reward:0.85\n", + "step: 1171, Red action: do-nothing, Blue reward:0.85\n", + "step: 1172, Red action: do-nothing, Blue reward:0.85\n", + "step: 1173, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1174, Red action: do-nothing, Blue reward:0.85\n", + "step: 1175, Red action: do-nothing, Blue reward:0.85\n", + "step: 1176, Red action: do-nothing, Blue reward:0.85\n", + "step: 1177, Red action: do-nothing, Blue reward:0.85\n", + "step: 1178, Red action: do-nothing, Blue reward:0.85\n", + "step: 1179, Red action: do-nothing, Blue reward:0.85\n", + "step: 1180, Red action: do-nothing, Blue reward:0.85\n", + "step: 1181, Red action: do-nothing, Blue reward:0.85\n", + "step: 1182, Red action: do-nothing, Blue reward:0.85\n", + "step: 1183, Red action: do-nothing, Blue reward:0.85\n", + "step: 1184, Red action: do-nothing, Blue reward:0.85\n", + "step: 1185, Red action: do-nothing, Blue reward:0.85\n", + "step: 1186, Red action: do-nothing, Blue reward:0.85\n", + "step: 1187, Red action: do-nothing, Blue reward:0.85\n", + "step: 1188, Red action: do-nothing, Blue reward:0.85\n", + "step: 1189, Red action: do-nothing, Blue reward:0.85\n", + "step: 1190, Red action: do-nothing, Blue reward:0.85\n", + "step: 1191, Red action: do-nothing, Blue reward:0.85\n", + "step: 1192, Red action: do-nothing, Blue reward:0.85\n", + "step: 1193, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1194, Red action: do-nothing, Blue reward:0.85\n", + "step: 1195, Red action: do-nothing, Blue reward:0.85\n", + "step: 1196, Red action: do-nothing, Blue reward:0.85\n", + "step: 1197, Red action: do-nothing, Blue reward:0.85\n", + "step: 1198, Red action: do-nothing, Blue reward:0.85\n", + "step: 1199, Red action: do-nothing, Blue reward:0.85\n", + "step: 1200, Red action: do-nothing, Blue reward:0.85\n", + "step: 1201, Red action: do-nothing, Blue reward:0.85\n", + "step: 1202, Red action: do-nothing, Blue reward:0.85\n", + "step: 1203, Red action: do-nothing, Blue reward:0.85\n", + "step: 1204, Red action: do-nothing, Blue reward:0.85\n", + "step: 1205, Red action: do-nothing, Blue reward:0.85\n", + "step: 1206, Red action: do-nothing, Blue reward:0.85\n", + "step: 1207, Red action: do-nothing, Blue reward:0.85\n", + "step: 1208, Red action: do-nothing, Blue reward:0.85\n", + "step: 1209, Red action: do-nothing, Blue reward:0.85\n", + "step: 1210, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1211, Red action: do-nothing, Blue reward:0.85\n", + "step: 1212, Red action: do-nothing, Blue reward:0.85\n", + "step: 1213, Red action: do-nothing, Blue reward:0.85\n", + "step: 1214, Red action: do-nothing, Blue reward:0.85\n", + "step: 1215, Red action: do-nothing, Blue reward:0.85\n", + "step: 1216, Red action: do-nothing, Blue reward:0.85\n", + "step: 1217, Red action: do-nothing, Blue reward:0.85\n", + "step: 1218, Red action: do-nothing, Blue reward:0.85\n", + "step: 1219, Red action: do-nothing, Blue reward:0.85\n", + "step: 1220, Red action: do-nothing, Blue reward:0.85\n", + "step: 1221, Red action: do-nothing, Blue reward:0.85\n", + "step: 1222, Red action: do-nothing, Blue reward:0.85\n", + "step: 1223, Red action: do-nothing, Blue reward:0.85\n", + "step: 1224, Red action: do-nothing, Blue reward:0.85\n", + "step: 1225, Red action: do-nothing, Blue reward:0.85\n", + "step: 1226, Red action: do-nothing, Blue reward:0.85\n", + "step: 1227, Red action: do-nothing, Blue reward:0.85\n", + "step: 1228, Red action: do-nothing, Blue reward:0.85\n", + "step: 1229, Red action: do-nothing, Blue reward:0.85\n", + "step: 1230, Red action: do-nothing, Blue reward:0.85\n", + "step: 1231, Red action: do-nothing, Blue reward:0.85\n", + "step: 1232, Red action: do-nothing, Blue reward:0.85\n", + "step: 1233, Red action: do-nothing, Blue reward:0.85\n", + "step: 1234, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1235, Red action: do-nothing, Blue reward:0.85\n", + "step: 1236, Red action: do-nothing, Blue reward:0.85\n", + "step: 1237, Red action: do-nothing, Blue reward:0.85\n", + "step: 1238, Red action: do-nothing, Blue reward:0.85\n", + "step: 1239, Red action: do-nothing, Blue reward:0.85\n", + "step: 1240, Red action: do-nothing, Blue reward:0.85\n", + "step: 1241, Red action: do-nothing, Blue reward:0.85\n", + "step: 1242, Red action: do-nothing, Blue reward:0.85\n", + "step: 1243, Red action: do-nothing, Blue reward:0.85\n", + "step: 1244, Red action: do-nothing, Blue reward:0.85\n", + "step: 1245, Red action: do-nothing, Blue reward:0.85\n", + "step: 1246, Red action: do-nothing, Blue reward:0.85\n", + "step: 1247, Red action: do-nothing, Blue reward:0.85\n", + "step: 1248, Red action: do-nothing, Blue reward:0.85\n", + "step: 1249, Red action: do-nothing, Blue reward:0.85\n", + "step: 1250, Red action: do-nothing, Blue reward:0.85\n", + "step: 1251, Red action: do-nothing, Blue reward:0.85\n", + "step: 1252, Red action: do-nothing, Blue reward:0.85\n", + "step: 1253, Red action: do-nothing, Blue reward:0.85\n", + "step: 1254, Red action: do-nothing, Blue reward:0.85\n", + "step: 1255, Red action: do-nothing, Blue reward:0.85\n", + "step: 1256, Red action: do-nothing, Blue reward:0.85\n", + "step: 1257, Red action: do-nothing, Blue reward:0.85\n", + "step: 1258, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1259, Red action: do-nothing, Blue reward:0.85\n", + "step: 1260, Red action: do-nothing, Blue reward:0.85\n", + "step: 1261, Red action: do-nothing, Blue reward:0.85\n", + "step: 1262, Red action: do-nothing, Blue reward:0.85\n", + "step: 1263, Red action: do-nothing, Blue reward:0.85\n", + "step: 1264, Red action: do-nothing, Blue reward:0.85\n", + "step: 1265, Red action: do-nothing, Blue reward:0.85\n", + "step: 1266, Red action: do-nothing, Blue reward:0.85\n", + "step: 1267, Red action: do-nothing, Blue reward:0.85\n", + "step: 1268, Red action: do-nothing, Blue reward:0.85\n", + "step: 1269, Red action: do-nothing, Blue reward:0.85\n", + "step: 1270, Red action: do-nothing, Blue reward:0.85\n", + "step: 1271, Red action: do-nothing, Blue reward:0.85\n", + "step: 1272, Red action: do-nothing, Blue reward:0.85\n", + "step: 1273, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1274, Red action: do-nothing, Blue reward:0.85\n", + "step: 1275, Red action: do-nothing, Blue reward:0.85\n", + "step: 1276, Red action: do-nothing, Blue reward:0.85\n", + "step: 1277, Red action: do-nothing, Blue reward:0.85\n", + "step: 1278, Red action: do-nothing, Blue reward:0.85\n", + "step: 1279, Red action: do-nothing, Blue reward:0.85\n", + "step: 1280, Red action: do-nothing, Blue reward:0.85\n", + "step: 1281, Red action: do-nothing, Blue reward:0.85\n", + "step: 1282, Red action: do-nothing, Blue reward:0.85\n", + "step: 1283, Red action: do-nothing, Blue reward:0.85\n", + "step: 1284, Red action: do-nothing, Blue reward:0.85\n", + "step: 1285, Red action: do-nothing, Blue reward:0.85\n", + "step: 1286, Red action: do-nothing, Blue reward:0.85\n", + "step: 1287, Red action: do-nothing, Blue reward:0.85\n", + "step: 1288, Red action: do-nothing, Blue reward:0.85\n", + "step: 1289, Red action: do-nothing, Blue reward:0.85\n", + "step: 1290, Red action: do-nothing, Blue reward:0.85\n", + "step: 1291, Red action: do-nothing, Blue reward:0.85\n", + "step: 1292, Red action: do-nothing, Blue reward:0.85\n", + "step: 1293, Red action: do-nothing, Blue reward:0.85\n", + "step: 1294, Red action: do-nothing, Blue reward:0.85\n", + "step: 1295, Red action: do-nothing, Blue reward:0.85\n", + "step: 1296, Red action: do-nothing, Blue reward:0.85\n", + "step: 1297, Red action: do-nothing, Blue reward:0.85\n", + "step: 1298, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1299, Red action: do-nothing, Blue reward:0.85\n", + "step: 1300, Red action: do-nothing, Blue reward:0.85\n", + "step: 1301, Red action: do-nothing, Blue reward:0.85\n", + "step: 1302, Red action: do-nothing, Blue reward:0.85\n", + "step: 1303, Red action: do-nothing, Blue reward:0.85\n", + "step: 1304, Red action: do-nothing, Blue reward:0.85\n", + "step: 1305, Red action: do-nothing, Blue reward:0.85\n", + "step: 1306, Red action: do-nothing, Blue reward:0.85\n", + "step: 1307, Red action: do-nothing, Blue reward:0.85\n", + "step: 1308, Red action: do-nothing, Blue reward:0.85\n", + "step: 1309, Red action: do-nothing, Blue reward:0.85\n", + "step: 1310, Red action: do-nothing, Blue reward:0.85\n", + "step: 1311, Red action: do-nothing, Blue reward:0.85\n", + "step: 1312, Red action: do-nothing, Blue reward:0.85\n", + "step: 1313, Red action: do-nothing, Blue reward:0.85\n", + "step: 1314, Red action: do-nothing, Blue reward:0.85\n", + "step: 1315, Red action: do-nothing, Blue reward:0.85\n", + "step: 1316, Red action: do-nothing, Blue reward:0.85\n", + "step: 1317, Red action: do-nothing, Blue reward:0.85\n", + "step: 1318, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1319, Red action: do-nothing, Blue reward:0.85\n", + "step: 1320, Red action: do-nothing, Blue reward:0.85\n", + "step: 1321, Red action: do-nothing, Blue reward:0.85\n", + "step: 1322, Red action: do-nothing, Blue reward:0.85\n", + "step: 1323, Red action: do-nothing, Blue reward:0.85\n", + "step: 1324, Red action: do-nothing, Blue reward:0.85\n", + "step: 1325, Red action: do-nothing, Blue reward:0.85\n", + "step: 1326, Red action: do-nothing, Blue reward:0.85\n", + "step: 1327, Red action: do-nothing, Blue reward:0.85\n", + "step: 1328, Red action: do-nothing, Blue reward:0.85\n", + "step: 1329, Red action: do-nothing, Blue reward:0.85\n", + "step: 1330, Red action: do-nothing, Blue reward:0.85\n", + "step: 1331, Red action: do-nothing, Blue reward:0.85\n", + "step: 1332, Red action: do-nothing, Blue reward:0.85\n", + "step: 1333, Red action: do-nothing, Blue reward:0.85\n", + "step: 1334, Red action: do-nothing, Blue reward:0.85\n", + "step: 1335, Red action: do-nothing, Blue reward:0.85\n", + "step: 1336, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1337, Red action: do-nothing, Blue reward:0.85\n", + "step: 1338, Red action: do-nothing, Blue reward:0.85\n", + "step: 1339, Red action: do-nothing, Blue reward:0.85\n", + "step: 1340, Red action: do-nothing, Blue reward:0.85\n", + "step: 1341, Red action: do-nothing, Blue reward:0.85\n", + "step: 1342, Red action: do-nothing, Blue reward:0.85\n", + "step: 1343, Red action: do-nothing, Blue reward:0.85\n", + "step: 1344, Red action: do-nothing, Blue reward:0.85\n", + "step: 1345, Red action: do-nothing, Blue reward:0.85\n", + "step: 1346, Red action: do-nothing, Blue reward:0.85\n", + "step: 1347, Red action: do-nothing, Blue reward:0.85\n", + "step: 1348, Red action: do-nothing, Blue reward:0.85\n", + "step: 1349, Red action: do-nothing, Blue reward:0.85\n", + "step: 1350, Red action: do-nothing, Blue reward:0.85\n", + "step: 1351, Red action: do-nothing, Blue reward:0.85\n", + "step: 1352, Red action: do-nothing, Blue reward:0.85\n", + "step: 1353, Red action: do-nothing, Blue reward:0.85\n", + "step: 1354, Red action: do-nothing, Blue reward:0.85\n", + "step: 1355, Red action: do-nothing, Blue reward:0.85\n", + "step: 1356, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1357, Red action: do-nothing, Blue reward:0.85\n", + "step: 1358, Red action: do-nothing, Blue reward:0.85\n", + "step: 1359, Red action: do-nothing, Blue reward:0.85\n", + "step: 1360, Red action: do-nothing, Blue reward:0.85\n", + "step: 1361, Red action: do-nothing, Blue reward:0.85\n", + "step: 1362, Red action: do-nothing, Blue reward:0.85\n", + "step: 1363, Red action: do-nothing, Blue reward:0.85\n", + "step: 1364, Red action: do-nothing, Blue reward:0.85\n", + "step: 1365, Red action: do-nothing, Blue reward:0.85\n", + "step: 1366, Red action: do-nothing, Blue reward:0.85\n", + "step: 1367, Red action: do-nothing, Blue reward:0.85\n", + "step: 1368, Red action: do-nothing, Blue reward:0.85\n", + "step: 1369, Red action: do-nothing, Blue reward:0.85\n", + "step: 1370, Red action: do-nothing, Blue reward:0.85\n", + "step: 1371, Red action: do-nothing, Blue reward:0.85\n", + "step: 1372, Red action: do-nothing, Blue reward:0.85\n", + "step: 1373, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1374, Red action: do-nothing, Blue reward:0.85\n", + "step: 1375, Red action: do-nothing, Blue reward:0.85\n", + "step: 1376, Red action: do-nothing, Blue reward:0.85\n", + "step: 1377, Red action: do-nothing, Blue reward:0.85\n", + "step: 1378, Red action: do-nothing, Blue reward:0.85\n", + "step: 1379, Red action: do-nothing, Blue reward:0.85\n", + "step: 1380, Red action: do-nothing, Blue reward:0.85\n", + "step: 1381, Red action: do-nothing, Blue reward:0.85\n", + "step: 1382, Red action: do-nothing, Blue reward:0.85\n", + "step: 1383, Red action: do-nothing, Blue reward:0.85\n", + "step: 1384, Red action: do-nothing, Blue reward:0.85\n", + "step: 1385, Red action: do-nothing, Blue reward:0.85\n", + "step: 1386, Red action: do-nothing, Blue reward:0.85\n", + "step: 1387, Red action: do-nothing, Blue reward:0.85\n", + "step: 1388, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1389, Red action: do-nothing, Blue reward:0.85\n", + "step: 1390, Red action: do-nothing, Blue reward:0.85\n", + "step: 1391, Red action: do-nothing, Blue reward:0.85\n", + "step: 1392, Red action: do-nothing, Blue reward:0.85\n", + "step: 1393, Red action: do-nothing, Blue reward:0.85\n", + "step: 1394, Red action: do-nothing, Blue reward:0.85\n", + "step: 1395, Red action: do-nothing, Blue reward:0.85\n", + "step: 1396, Red action: do-nothing, Blue reward:0.85\n", + "step: 1397, Red action: do-nothing, Blue reward:0.85\n", + "step: 1398, Red action: do-nothing, Blue reward:0.85\n", + "step: 1399, Red action: do-nothing, Blue reward:0.85\n", + "step: 1400, Red action: do-nothing, Blue reward:0.85\n", + "step: 1401, Red action: do-nothing, Blue reward:0.85\n", + "step: 1402, Red action: do-nothing, Blue reward:0.85\n", + "step: 1403, Red action: do-nothing, Blue reward:0.85\n", + "step: 1404, Red action: do-nothing, Blue reward:0.85\n", + "step: 1405, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1406, Red action: do-nothing, Blue reward:0.85\n", + "step: 1407, Red action: do-nothing, Blue reward:0.85\n", + "step: 1408, Red action: do-nothing, Blue reward:0.85\n", + "step: 1409, Red action: do-nothing, Blue reward:0.85\n", + "step: 1410, Red action: do-nothing, Blue reward:0.85\n", + "step: 1411, Red action: do-nothing, Blue reward:0.85\n", + "step: 1412, Red action: do-nothing, Blue reward:0.85\n", + "step: 1413, Red action: do-nothing, Blue reward:0.85\n", + "step: 1414, Red action: do-nothing, Blue reward:0.85\n", + "step: 1415, Red action: do-nothing, Blue reward:0.85\n", + "step: 1416, Red action: do-nothing, Blue reward:0.85\n", + "step: 1417, Red action: do-nothing, Blue reward:0.85\n", + "step: 1418, Red action: do-nothing, Blue reward:0.85\n", + "step: 1419, Red action: do-nothing, Blue reward:0.85\n", + "step: 1420, Red action: do-nothing, Blue reward:0.85\n", + "step: 1421, Red action: do-nothing, Blue reward:0.85\n", + "step: 1422, Red action: do-nothing, Blue reward:0.85\n", + "step: 1423, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1424, Red action: do-nothing, Blue reward:0.85\n", + "step: 1425, Red action: do-nothing, Blue reward:0.85\n", + "step: 1426, Red action: do-nothing, Blue reward:0.85\n", + "step: 1427, Red action: do-nothing, Blue reward:0.85\n", + "step: 1428, Red action: do-nothing, Blue reward:0.85\n", + "step: 1429, Red action: do-nothing, Blue reward:0.85\n", + "step: 1430, Red action: do-nothing, Blue reward:0.85\n", + "step: 1431, Red action: do-nothing, Blue reward:0.85\n", + "step: 1432, Red action: do-nothing, Blue reward:0.85\n", + "step: 1433, Red action: do-nothing, Blue reward:0.85\n", + "step: 1434, Red action: do-nothing, Blue reward:0.85\n", + "step: 1435, Red action: do-nothing, Blue reward:0.85\n", + "step: 1436, Red action: do-nothing, Blue reward:0.85\n", + "step: 1437, Red action: do-nothing, Blue reward:0.85\n", + "step: 1438, Red action: do-nothing, Blue reward:0.85\n", + "step: 1439, Red action: do-nothing, Blue reward:0.85\n", + "step: 1440, Red action: do-nothing, Blue reward:0.85\n", + "step: 1441, Red action: do-nothing, Blue reward:0.85\n", + "step: 1442, Red action: do-nothing, Blue reward:0.85\n", + "step: 1443, Red action: do-nothing, Blue reward:0.85\n", + "step: 1444, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1445, Red action: do-nothing, Blue reward:0.85\n", + "step: 1446, Red action: do-nothing, Blue reward:0.85\n", + "step: 1447, Red action: do-nothing, Blue reward:0.85\n", + "step: 1448, Red action: do-nothing, Blue reward:0.85\n", + "step: 1449, Red action: do-nothing, Blue reward:0.85\n", + "step: 1450, Red action: do-nothing, Blue reward:0.85\n", + "step: 1451, Red action: do-nothing, Blue reward:0.85\n", + "step: 1452, Red action: do-nothing, Blue reward:0.85\n", + "step: 1453, Red action: do-nothing, Blue reward:0.85\n", + "step: 1454, Red action: do-nothing, Blue reward:0.85\n", + "step: 1455, Red action: do-nothing, Blue reward:0.85\n", + "step: 1456, Red action: do-nothing, Blue reward:0.85\n", + "step: 1457, Red action: do-nothing, Blue reward:0.85\n", + "step: 1458, Red action: do-nothing, Blue reward:0.85\n", + "step: 1459, Red action: do-nothing, Blue reward:0.85\n", + "step: 1460, Red action: do-nothing, Blue reward:0.85\n", + "step: 1461, Red action: do-nothing, Blue reward:0.85\n", + "step: 1462, Red action: do-nothing, Blue reward:0.85\n", + "step: 1463, Red action: do-nothing, Blue reward:0.85\n", + "step: 1464, Red action: do-nothing, Blue reward:0.85\n", + "step: 1465, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1466, Red action: do-nothing, Blue reward:0.85\n", + "step: 1467, Red action: do-nothing, Blue reward:0.85\n", + "step: 1468, Red action: do-nothing, Blue reward:0.85\n", + "step: 1469, Red action: do-nothing, Blue reward:0.85\n", + "step: 1470, Red action: do-nothing, Blue reward:0.85\n", + "step: 1471, Red action: do-nothing, Blue reward:0.85\n", + "step: 1472, Red action: do-nothing, Blue reward:0.85\n", + "step: 1473, Red action: do-nothing, Blue reward:0.85\n", + "step: 1474, Red action: do-nothing, Blue reward:0.85\n", + "step: 1475, Red action: do-nothing, Blue reward:0.85\n", + "step: 1476, Red action: do-nothing, Blue reward:0.85\n", + "step: 1477, Red action: do-nothing, Blue reward:0.85\n", + "step: 1478, Red action: do-nothing, Blue reward:0.85\n", + "step: 1479, Red action: do-nothing, Blue reward:0.85\n", + "step: 1480, Red action: do-nothing, Blue reward:0.85\n", + "step: 1481, Red action: do-nothing, Blue reward:0.85\n", + "step: 1482, Red action: do-nothing, Blue reward:0.85\n", + "step: 1483, Red action: do-nothing, Blue reward:0.85\n", + "step: 1484, Red action: do-nothing, Blue reward:0.85\n", + "step: 1485, Red action: do-nothing, Blue reward:0.85\n", + "step: 1486, Red action: do-nothing, Blue reward:0.85\n", + "step: 1487, Red action: do-nothing, Blue reward:0.85\n", + "step: 1488, Red action: do-nothing, Blue reward:0.85\n", + "step: 1489, Red action: do-nothing, Blue reward:0.85\n", + "step: 1490, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1491, Red action: do-nothing, Blue reward:0.85\n", + "step: 1492, Red action: do-nothing, Blue reward:0.85\n", + "step: 1493, Red action: do-nothing, Blue reward:0.85\n", + "step: 1494, Red action: do-nothing, Blue reward:0.85\n", + "step: 1495, Red action: do-nothing, Blue reward:0.85\n", + "step: 1496, Red action: do-nothing, Blue reward:0.85\n", + "step: 1497, Red action: do-nothing, Blue reward:0.85\n", + "step: 1498, Red action: do-nothing, Blue reward:0.85\n", + "step: 1499, Red action: do-nothing, Blue reward:0.85\n", + "step: 1500, Red action: do-nothing, Blue reward:0.85\n", + "step: 1501, Red action: do-nothing, Blue reward:0.85\n", + "step: 1502, Red action: do-nothing, Blue reward:0.85\n", + "step: 1503, Red action: do-nothing, Blue reward:0.85\n", + "step: 1504, Red action: do-nothing, Blue reward:0.85\n", + "step: 1505, Red action: do-nothing, Blue reward:0.85\n", + "step: 1506, Red action: do-nothing, Blue reward:0.85\n", + "step: 1507, Red action: do-nothing, Blue reward:0.85\n", + "step: 1508, Red action: do-nothing, Blue reward:0.85\n", + "step: 1509, Red action: do-nothing, Blue reward:0.85\n", + "step: 1510, Red action: do-nothing, Blue reward:0.85\n", + "step: 1511, Red action: do-nothing, Blue reward:0.85\n", + "step: 1512, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1513, Red action: do-nothing, Blue reward:0.85\n", + "step: 1514, Red action: do-nothing, Blue reward:0.85\n", + "step: 1515, Red action: do-nothing, Blue reward:0.85\n", + "step: 1516, Red action: do-nothing, Blue reward:0.85\n", + "step: 1517, Red action: do-nothing, Blue reward:0.85\n", + "step: 1518, Red action: do-nothing, Blue reward:0.85\n", + "step: 1519, Red action: do-nothing, Blue reward:0.85\n", + "step: 1520, Red action: do-nothing, Blue reward:0.85\n", + "step: 1521, Red action: do-nothing, Blue reward:0.85\n", + "step: 1522, Red action: do-nothing, Blue reward:0.85\n", + "step: 1523, Red action: do-nothing, Blue reward:0.85\n", + "step: 1524, Red action: do-nothing, Blue reward:0.85\n", + "step: 1525, Red action: do-nothing, Blue reward:0.85\n", + "step: 1526, Red action: do-nothing, Blue reward:0.85\n", + "step: 1527, Red action: do-nothing, Blue reward:0.85\n", + "step: 1528, Red action: do-nothing, Blue reward:0.85\n", + "step: 1529, Red action: do-nothing, Blue reward:0.85\n", + "step: 1530, Red action: do-nothing, Blue reward:0.85\n", + "step: 1531, Red action: do-nothing, Blue reward:0.85\n", + "step: 1532, Red action: do-nothing, Blue reward:0.85\n", + "step: 1533, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1534, Red action: do-nothing, Blue reward:0.85\n", + "step: 1535, Red action: do-nothing, Blue reward:0.85\n", + "step: 1536, Red action: do-nothing, Blue reward:0.85\n", + "step: 1537, Red action: do-nothing, Blue reward:0.85\n", + "step: 1538, Red action: do-nothing, Blue reward:0.85\n", + "step: 1539, Red action: do-nothing, Blue reward:0.85\n", + "step: 1540, Red action: do-nothing, Blue reward:0.85\n", + "step: 1541, Red action: do-nothing, Blue reward:0.85\n", + "step: 1542, Red action: do-nothing, Blue reward:0.85\n", + "step: 1543, Red action: do-nothing, Blue reward:0.85\n", + "step: 1544, Red action: do-nothing, Blue reward:0.85\n", + "step: 1545, Red action: do-nothing, Blue reward:0.85\n", + "step: 1546, Red action: do-nothing, Blue reward:0.85\n", + "step: 1547, Red action: do-nothing, Blue reward:0.85\n", + "step: 1548, Red action: do-nothing, Blue reward:0.85\n", + "step: 1549, Red action: do-nothing, Blue reward:0.85\n", + "step: 1550, Red action: do-nothing, Blue reward:0.85\n", + "step: 1551, Red action: do-nothing, Blue reward:0.85\n", + "step: 1552, Red action: do-nothing, Blue reward:0.85\n", + "step: 1553, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1554, Red action: do-nothing, Blue reward:0.85\n", + "step: 1555, Red action: do-nothing, Blue reward:0.85\n", + "step: 1556, Red action: do-nothing, Blue reward:0.85\n", + "step: 1557, Red action: do-nothing, Blue reward:0.85\n", + "step: 1558, Red action: do-nothing, Blue reward:0.85\n", + "step: 1559, Red action: do-nothing, Blue reward:0.85\n", + "step: 1560, Red action: do-nothing, Blue reward:0.85\n", + "step: 1561, Red action: do-nothing, Blue reward:0.85\n", + "step: 1562, Red action: do-nothing, Blue reward:0.85\n", + "step: 1563, Red action: do-nothing, Blue reward:0.85\n", + "step: 1564, Red action: do-nothing, Blue reward:0.85\n", + "step: 1565, Red action: do-nothing, Blue reward:0.85\n", + "step: 1566, Red action: do-nothing, Blue reward:0.85\n", + "step: 1567, Red action: do-nothing, Blue reward:0.85\n", + "step: 1568, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1569, Red action: do-nothing, Blue reward:0.85\n", + "step: 1570, Red action: do-nothing, Blue reward:0.85\n", + "step: 1571, Red action: do-nothing, Blue reward:0.85\n", + "step: 1572, Red action: do-nothing, Blue reward:0.85\n", + "step: 1573, Red action: do-nothing, Blue reward:0.85\n", + "step: 1574, Red action: do-nothing, Blue reward:0.85\n", + "step: 1575, Red action: do-nothing, Blue reward:0.85\n", + "step: 1576, Red action: do-nothing, Blue reward:0.85\n", + "step: 1577, Red action: do-nothing, Blue reward:0.85\n", + "step: 1578, Red action: do-nothing, Blue reward:0.85\n", + "step: 1579, Red action: do-nothing, Blue reward:0.85\n", + "step: 1580, Red action: do-nothing, Blue reward:0.85\n", + "step: 1581, Red action: do-nothing, Blue reward:0.85\n", + "step: 1582, Red action: do-nothing, Blue reward:0.85\n", + "step: 1583, Red action: do-nothing, Blue reward:0.85\n", + "step: 1584, Red action: do-nothing, Blue reward:0.85\n", + "step: 1585, Red action: do-nothing, Blue reward:0.85\n", + "step: 1586, Red action: do-nothing, Blue reward:0.85\n", + "step: 1587, Red action: do-nothing, Blue reward:0.85\n", + "step: 1588, Red action: do-nothing, Blue reward:0.85\n", + "step: 1589, Red action: do-nothing, Blue reward:0.85\n", + "step: 1590, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1591, Red action: do-nothing, Blue reward:0.85\n", + "step: 1592, Red action: do-nothing, Blue reward:0.85\n", + "step: 1593, Red action: do-nothing, Blue reward:0.85\n", + "step: 1594, Red action: do-nothing, Blue reward:0.85\n", + "step: 1595, Red action: do-nothing, Blue reward:0.85\n", + "step: 1596, Red action: do-nothing, Blue reward:0.85\n", + "step: 1597, Red action: do-nothing, Blue reward:0.85\n", + "step: 1598, Red action: do-nothing, Blue reward:0.85\n", + "step: 1599, Red action: do-nothing, Blue reward:0.85\n", + "step: 1600, Red action: do-nothing, Blue reward:0.85\n", + "step: 1601, Red action: do-nothing, Blue reward:0.85\n", + "step: 1602, Red action: do-nothing, Blue reward:0.85\n", + "step: 1603, Red action: do-nothing, Blue reward:0.85\n", + "step: 1604, Red action: do-nothing, Blue reward:0.85\n", + "step: 1605, Red action: do-nothing, Blue reward:0.85\n", + "step: 1606, Red action: do-nothing, Blue reward:0.85\n", + "step: 1607, Red action: do-nothing, Blue reward:0.85\n", + "step: 1608, Red action: do-nothing, Blue reward:0.85\n", + "step: 1609, Red action: do-nothing, Blue reward:0.85\n", + "step: 1610, Red action: do-nothing, Blue reward:0.85\n", + "step: 1611, Red action: do-nothing, Blue reward:0.85\n", + "step: 1612, Red action: do-nothing, Blue reward:0.85\n", + "step: 1613, Red action: do-nothing, Blue reward:0.85\n", + "step: 1614, Red action: do-nothing, Blue reward:0.85\n", + "step: 1615, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1616, Red action: do-nothing, Blue reward:0.85\n", + "step: 1617, Red action: do-nothing, Blue reward:0.85\n", + "step: 1618, Red action: do-nothing, Blue reward:0.85\n", + "step: 1619, Red action: do-nothing, Blue reward:0.85\n", + "step: 1620, Red action: do-nothing, Blue reward:0.85\n", + "step: 1621, Red action: do-nothing, Blue reward:0.85\n", + "step: 1622, Red action: do-nothing, Blue reward:0.85\n", + "step: 1623, Red action: do-nothing, Blue reward:0.85\n", + "step: 1624, Red action: do-nothing, Blue reward:0.85\n", + "step: 1625, Red action: do-nothing, Blue reward:0.85\n", + "step: 1626, Red action: do-nothing, Blue reward:0.85\n", + "step: 1627, Red action: do-nothing, Blue reward:0.85\n", + "step: 1628, Red action: do-nothing, Blue reward:0.85\n", + "step: 1629, Red action: do-nothing, Blue reward:0.85\n", + "step: 1630, Red action: do-nothing, Blue reward:0.85\n", + "step: 1631, Red action: do-nothing, Blue reward:0.85\n", + "step: 1632, Red action: do-nothing, Blue reward:0.85\n", + "step: 1633, Red action: do-nothing, Blue reward:0.85\n", + "step: 1634, Red action: do-nothing, Blue reward:0.85\n", + "step: 1635, Red action: do-nothing, Blue reward:0.85\n", + "step: 1636, Red action: do-nothing, Blue reward:0.85\n", + "step: 1637, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1638, Red action: do-nothing, Blue reward:0.85\n", + "step: 1639, Red action: do-nothing, Blue reward:0.85\n", + "step: 1640, Red action: do-nothing, Blue reward:0.85\n", + "step: 1641, Red action: do-nothing, Blue reward:0.85\n", + "step: 1642, Red action: do-nothing, Blue reward:0.85\n", + "step: 1643, Red action: do-nothing, Blue reward:0.85\n", + "step: 1644, Red action: do-nothing, Blue reward:0.85\n", + "step: 1645, Red action: do-nothing, Blue reward:0.85\n", + "step: 1646, Red action: do-nothing, Blue reward:0.85\n", + "step: 1647, Red action: do-nothing, Blue reward:0.85\n", + "step: 1648, Red action: do-nothing, Blue reward:0.85\n", + "step: 1649, Red action: do-nothing, Blue reward:0.85\n", + "step: 1650, Red action: do-nothing, Blue reward:0.85\n", + "step: 1651, Red action: do-nothing, Blue reward:0.85\n", + "step: 1652, Red action: do-nothing, Blue reward:0.85\n", + "step: 1653, Red action: do-nothing, Blue reward:0.85\n", + "step: 1654, Red action: do-nothing, Blue reward:0.85\n", + "step: 1655, Red action: do-nothing, Blue reward:0.85\n", + "step: 1656, Red action: do-nothing, Blue reward:0.85\n", + "step: 1657, Red action: do-nothing, Blue reward:0.85\n", + "step: 1658, Red action: do-nothing, Blue reward:0.85\n", + "step: 1659, Red action: do-nothing, Blue reward:0.85\n", + "step: 1660, Red action: do-nothing, Blue reward:0.85\n", + "step: 1661, Red action: do-nothing, Blue reward:0.85\n", + "step: 1662, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1663, Red action: do-nothing, Blue reward:0.85\n", + "step: 1664, Red action: do-nothing, Blue reward:0.85\n", + "step: 1665, Red action: do-nothing, Blue reward:0.85\n", + "step: 1666, Red action: do-nothing, Blue reward:0.85\n", + "step: 1667, Red action: do-nothing, Blue reward:0.85\n", + "step: 1668, Red action: do-nothing, Blue reward:0.85\n", + "step: 1669, Red action: do-nothing, Blue reward:0.85\n", + "step: 1670, Red action: do-nothing, Blue reward:0.85\n", + "step: 1671, Red action: do-nothing, Blue reward:0.85\n", + "step: 1672, Red action: do-nothing, Blue reward:0.85\n", + "step: 1673, Red action: do-nothing, Blue reward:0.85\n", + "step: 1674, Red action: do-nothing, Blue reward:0.85\n", + "step: 1675, Red action: do-nothing, Blue reward:0.85\n", + "step: 1676, Red action: do-nothing, Blue reward:0.85\n", + "step: 1677, Red action: do-nothing, Blue reward:0.85\n", + "step: 1678, Red action: do-nothing, Blue reward:0.85\n", + "step: 1679, Red action: do-nothing, Blue reward:0.85\n", + "step: 1680, Red action: do-nothing, Blue reward:0.85\n", + "step: 1681, Red action: do-nothing, Blue reward:0.85\n", + "step: 1682, Red action: do-nothing, Blue reward:0.85\n", + "step: 1683, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1684, Red action: do-nothing, Blue reward:0.85\n", + "step: 1685, Red action: do-nothing, Blue reward:0.85\n", + "step: 1686, Red action: do-nothing, Blue reward:0.85\n", + "step: 1687, Red action: do-nothing, Blue reward:0.85\n", + "step: 1688, Red action: do-nothing, Blue reward:0.85\n", + "step: 1689, Red action: do-nothing, Blue reward:0.85\n", + "step: 1690, Red action: do-nothing, Blue reward:0.85\n", + "step: 1691, Red action: do-nothing, Blue reward:0.85\n", + "step: 1692, Red action: do-nothing, Blue reward:0.85\n", + "step: 1693, Red action: do-nothing, Blue reward:0.85\n", + "step: 1694, Red action: do-nothing, Blue reward:0.85\n", + "step: 1695, Red action: do-nothing, Blue reward:0.85\n", + "step: 1696, Red action: do-nothing, Blue reward:0.85\n", + "step: 1697, Red action: do-nothing, Blue reward:0.85\n", + "step: 1698, Red action: do-nothing, Blue reward:0.85\n", + "step: 1699, Red action: do-nothing, Blue reward:0.85\n", + "step: 1700, Red action: do-nothing, Blue reward:0.85\n", + "step: 1701, Red action: do-nothing, Blue reward:0.85\n", + "step: 1702, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1703, Red action: do-nothing, Blue reward:0.85\n", + "step: 1704, Red action: do-nothing, Blue reward:0.85\n", + "step: 1705, Red action: do-nothing, Blue reward:0.85\n", + "step: 1706, Red action: do-nothing, Blue reward:0.85\n", + "step: 1707, Red action: do-nothing, Blue reward:0.85\n", + "step: 1708, Red action: do-nothing, Blue reward:0.85\n", + "step: 1709, Red action: do-nothing, Blue reward:0.85\n", + "step: 1710, Red action: do-nothing, Blue reward:0.85\n", + "step: 1711, Red action: do-nothing, Blue reward:0.85\n", + "step: 1712, Red action: do-nothing, Blue reward:0.85\n", + "step: 1713, Red action: do-nothing, Blue reward:0.85\n", + "step: 1714, Red action: do-nothing, Blue reward:0.85\n", + "step: 1715, Red action: do-nothing, Blue reward:0.85\n", + "step: 1716, Red action: do-nothing, Blue reward:0.85\n", + "step: 1717, Red action: do-nothing, Blue reward:0.85\n", + "step: 1718, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1719, Red action: do-nothing, Blue reward:0.85\n", + "step: 1720, Red action: do-nothing, Blue reward:0.85\n", + "step: 1721, Red action: do-nothing, Blue reward:0.85\n", + "step: 1722, Red action: do-nothing, Blue reward:0.85\n", + "step: 1723, Red action: do-nothing, Blue reward:0.85\n", + "step: 1724, Red action: do-nothing, Blue reward:0.85\n", + "step: 1725, Red action: do-nothing, Blue reward:0.85\n", + "step: 1726, Red action: do-nothing, Blue reward:0.85\n", + "step: 1727, Red action: do-nothing, Blue reward:0.85\n", + "step: 1728, Red action: do-nothing, Blue reward:0.85\n", + "step: 1729, Red action: do-nothing, Blue reward:0.85\n", + "step: 1730, Red action: do-nothing, Blue reward:0.85\n", + "step: 1731, Red action: do-nothing, Blue reward:0.85\n", + "step: 1732, Red action: do-nothing, Blue reward:0.85\n", + "step: 1733, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1734, Red action: do-nothing, Blue reward:0.85\n", + "step: 1735, Red action: do-nothing, Blue reward:0.85\n", + "step: 1736, Red action: do-nothing, Blue reward:0.85\n", + "step: 1737, Red action: do-nothing, Blue reward:0.85\n", + "step: 1738, Red action: do-nothing, Blue reward:0.85\n", + "step: 1739, Red action: do-nothing, Blue reward:0.85\n", + "step: 1740, Red action: do-nothing, Blue reward:0.85\n", + "step: 1741, Red action: do-nothing, Blue reward:0.85\n", + "step: 1742, Red action: do-nothing, Blue reward:0.85\n", + "step: 1743, Red action: do-nothing, Blue reward:0.85\n", + "step: 1744, Red action: do-nothing, Blue reward:0.85\n", + "step: 1745, Red action: do-nothing, Blue reward:0.85\n", + "step: 1746, Red action: do-nothing, Blue reward:0.85\n", + "step: 1747, Red action: do-nothing, Blue reward:0.85\n", + "step: 1748, Red action: do-nothing, Blue reward:0.85\n", + "step: 1749, Red action: do-nothing, Blue reward:0.85\n", + "step: 1750, Red action: do-nothing, Blue reward:0.85\n", + "step: 1751, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1752, Red action: do-nothing, Blue reward:0.85\n", + "step: 1753, Red action: do-nothing, Blue reward:0.85\n", + "step: 1754, Red action: do-nothing, Blue reward:0.85\n", + "step: 1755, Red action: do-nothing, Blue reward:0.85\n", + "step: 1756, Red action: do-nothing, Blue reward:0.85\n", + "step: 1757, Red action: do-nothing, Blue reward:0.85\n", + "step: 1758, Red action: do-nothing, Blue reward:0.85\n", + "step: 1759, Red action: do-nothing, Blue reward:0.85\n", + "step: 1760, Red action: do-nothing, Blue reward:0.85\n", + "step: 1761, Red action: do-nothing, Blue reward:0.85\n", + "step: 1762, Red action: do-nothing, Blue reward:0.85\n", + "step: 1763, Red action: do-nothing, Blue reward:0.85\n", + "step: 1764, Red action: do-nothing, Blue reward:0.85\n", + "step: 1765, Red action: do-nothing, Blue reward:0.85\n", + "step: 1766, Red action: do-nothing, Blue reward:0.85\n", + "step: 1767, Red action: do-nothing, Blue reward:0.85\n", + "step: 1768, Red action: do-nothing, Blue reward:0.85\n", + "step: 1769, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1770, Red action: do-nothing, Blue reward:0.85\n", + "step: 1771, Red action: do-nothing, Blue reward:0.85\n", + "step: 1772, Red action: do-nothing, Blue reward:0.85\n", + "step: 1773, Red action: do-nothing, Blue reward:0.85\n", + "step: 1774, Red action: do-nothing, Blue reward:0.85\n", + "step: 1775, Red action: do-nothing, Blue reward:0.85\n", + "step: 1776, Red action: do-nothing, Blue reward:0.85\n", + "step: 1777, Red action: do-nothing, Blue reward:0.85\n", + "step: 1778, Red action: do-nothing, Blue reward:0.85\n", + "step: 1779, Red action: do-nothing, Blue reward:0.85\n", + "step: 1780, Red action: do-nothing, Blue reward:0.85\n", + "step: 1781, Red action: do-nothing, Blue reward:0.85\n", + "step: 1782, Red action: do-nothing, Blue reward:0.85\n", + "step: 1783, Red action: do-nothing, Blue reward:0.85\n", + "step: 1784, Red action: do-nothing, Blue reward:0.85\n", + "step: 1785, Red action: do-nothing, Blue reward:0.85\n", + "step: 1786, Red action: do-nothing, Blue reward:0.85\n", + "step: 1787, Red action: do-nothing, Blue reward:0.85\n", + "step: 1788, Red action: do-nothing, Blue reward:0.85\n", + "step: 1789, Red action: do-nothing, Blue reward:0.85\n", + "step: 1790, Red action: do-nothing, Blue reward:0.85\n", + "step: 1791, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1792, Red action: do-nothing, Blue reward:0.85\n", + "step: 1793, Red action: do-nothing, Blue reward:0.85\n", + "step: 1794, Red action: do-nothing, Blue reward:0.85\n", + "step: 1795, Red action: do-nothing, Blue reward:0.85\n", + "step: 1796, Red action: do-nothing, Blue reward:0.85\n", + "step: 1797, Red action: do-nothing, Blue reward:0.85\n", + "step: 1798, Red action: do-nothing, Blue reward:0.85\n", + "step: 1799, Red action: do-nothing, Blue reward:0.85\n", + "step: 1800, Red action: do-nothing, Blue reward:0.85\n", + "step: 1801, Red action: do-nothing, Blue reward:0.85\n", + "step: 1802, Red action: do-nothing, Blue reward:0.85\n", + "step: 1803, Red action: do-nothing, Blue reward:0.85\n", + "step: 1804, Red action: do-nothing, Blue reward:0.85\n", + "step: 1805, Red action: do-nothing, Blue reward:0.85\n", + "step: 1806, Red action: do-nothing, Blue reward:0.85\n", + "step: 1807, Red action: do-nothing, Blue reward:0.85\n", + "step: 1808, Red action: do-nothing, Blue reward:0.85\n", + "step: 1809, Red action: do-nothing, Blue reward:0.85\n", + "step: 1810, Red action: do-nothing, Blue reward:0.85\n", + "step: 1811, Red action: do-nothing, Blue reward:0.85\n", + "step: 1812, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1813, Red action: do-nothing, Blue reward:0.85\n", + "step: 1814, Red action: do-nothing, Blue reward:0.85\n", + "step: 1815, Red action: do-nothing, Blue reward:0.85\n", + "step: 1816, Red action: do-nothing, Blue reward:0.85\n", + "step: 1817, Red action: do-nothing, Blue reward:0.85\n", + "step: 1818, Red action: do-nothing, Blue reward:0.85\n", + "step: 1819, Red action: do-nothing, Blue reward:0.85\n", + "step: 1820, Red action: do-nothing, Blue reward:0.85\n", + "step: 1821, Red action: do-nothing, Blue reward:0.85\n", + "step: 1822, Red action: do-nothing, Blue reward:0.85\n", + "step: 1823, Red action: do-nothing, Blue reward:0.85\n", + "step: 1824, Red action: do-nothing, Blue reward:0.85\n", + "step: 1825, Red action: do-nothing, Blue reward:0.85\n", + "step: 1826, Red action: do-nothing, Blue reward:0.85\n", + "step: 1827, Red action: do-nothing, Blue reward:0.85\n", + "step: 1828, Red action: do-nothing, Blue reward:0.85\n", + "step: 1829, Red action: do-nothing, Blue reward:0.85\n", + "step: 1830, Red action: do-nothing, Blue reward:0.85\n", + "step: 1831, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1832, Red action: do-nothing, Blue reward:0.85\n", + "step: 1833, Red action: do-nothing, Blue reward:0.85\n", + "step: 1834, Red action: do-nothing, Blue reward:0.85\n", + "step: 1835, Red action: do-nothing, Blue reward:0.85\n", + "step: 1836, Red action: do-nothing, Blue reward:0.85\n", + "step: 1837, Red action: do-nothing, Blue reward:0.85\n", + "step: 1838, Red action: do-nothing, Blue reward:0.85\n", + "step: 1839, Red action: do-nothing, Blue reward:0.85\n", + "step: 1840, Red action: do-nothing, Blue reward:0.85\n", + "step: 1841, Red action: do-nothing, Blue reward:0.85\n", + "step: 1842, Red action: do-nothing, Blue reward:0.85\n", + "step: 1843, Red action: do-nothing, Blue reward:0.85\n", + "step: 1844, Red action: do-nothing, Blue reward:0.85\n", + "step: 1845, Red action: do-nothing, Blue reward:0.85\n", + "step: 1846, Red action: do-nothing, Blue reward:0.85\n", + "step: 1847, Red action: do-nothing, Blue reward:0.85\n", + "step: 1848, Red action: do-nothing, Blue reward:0.85\n", + "step: 1849, Red action: do-nothing, Blue reward:0.85\n", + "step: 1850, Red action: do-nothing, Blue reward:0.85\n", + "step: 1851, Red action: do-nothing, Blue reward:0.85\n", + "step: 1852, Red action: do-nothing, Blue reward:0.85\n", + "step: 1853, Red action: do-nothing, Blue reward:0.85\n", + "step: 1854, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1855, Red action: do-nothing, Blue reward:0.85\n", + "step: 1856, Red action: do-nothing, Blue reward:0.85\n", + "step: 1857, Red action: do-nothing, Blue reward:0.85\n", + "step: 1858, Red action: do-nothing, Blue reward:0.85\n", + "step: 1859, Red action: do-nothing, Blue reward:0.85\n", + "step: 1860, Red action: do-nothing, Blue reward:0.85\n", + "step: 1861, Red action: do-nothing, Blue reward:0.85\n", + "step: 1862, Red action: do-nothing, Blue reward:0.85\n", + "step: 1863, Red action: do-nothing, Blue reward:0.85\n", + "step: 1864, Red action: do-nothing, Blue reward:0.85\n", + "step: 1865, Red action: do-nothing, Blue reward:0.85\n", + "step: 1866, Red action: do-nothing, Blue reward:0.85\n", + "step: 1867, Red action: do-nothing, Blue reward:0.85\n", + "step: 1868, Red action: do-nothing, Blue reward:0.85\n", + "step: 1869, Red action: do-nothing, Blue reward:0.85\n", + "step: 1870, Red action: do-nothing, Blue reward:0.85\n", + "step: 1871, Red action: do-nothing, Blue reward:0.85\n", + "step: 1872, Red action: do-nothing, Blue reward:0.85\n", + "step: 1873, Red action: do-nothing, Blue reward:0.85\n", + "step: 1874, Red action: do-nothing, Blue reward:0.85\n", + "step: 1875, Red action: do-nothing, Blue reward:0.85\n", + "step: 1876, Red action: do-nothing, Blue reward:0.85\n", + "step: 1877, Red action: do-nothing, Blue reward:0.85\n", + "step: 1878, Red action: do-nothing, Blue reward:0.85\n", + "step: 1879, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1880, Red action: do-nothing, Blue reward:0.85\n", + "step: 1881, Red action: do-nothing, Blue reward:0.85\n", + "step: 1882, Red action: do-nothing, Blue reward:0.85\n", + "step: 1883, Red action: do-nothing, Blue reward:0.85\n", + "step: 1884, Red action: do-nothing, Blue reward:0.85\n", + "step: 1885, Red action: do-nothing, Blue reward:0.85\n", + "step: 1886, Red action: do-nothing, Blue reward:0.85\n", + "step: 1887, Red action: do-nothing, Blue reward:0.85\n", + "step: 1888, Red action: do-nothing, Blue reward:0.85\n", + "step: 1889, Red action: do-nothing, Blue reward:0.85\n", + "step: 1890, Red action: do-nothing, Blue reward:0.85\n", + "step: 1891, Red action: do-nothing, Blue reward:0.85\n", + "step: 1892, Red action: do-nothing, Blue reward:0.85\n", + "step: 1893, Red action: do-nothing, Blue reward:0.85\n", + "step: 1894, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1895, Red action: do-nothing, Blue reward:0.85\n", + "step: 1896, Red action: do-nothing, Blue reward:0.85\n", + "step: 1897, Red action: do-nothing, Blue reward:0.85\n", + "step: 1898, Red action: do-nothing, Blue reward:0.85\n", + "step: 1899, Red action: do-nothing, Blue reward:0.85\n", + "step: 1900, Red action: do-nothing, Blue reward:0.85\n", + "step: 1901, Red action: do-nothing, Blue reward:0.85\n", + "step: 1902, Red action: do-nothing, Blue reward:0.85\n", + "step: 1903, Red action: do-nothing, Blue reward:0.85\n", + "step: 1904, Red action: do-nothing, Blue reward:0.85\n", + "step: 1905, Red action: do-nothing, Blue reward:0.85\n", + "step: 1906, Red action: do-nothing, Blue reward:0.85\n", + "step: 1907, Red action: do-nothing, Blue reward:0.85\n", + "step: 1908, Red action: do-nothing, Blue reward:0.85\n", + "step: 1909, Red action: do-nothing, Blue reward:0.85\n", + "step: 1910, Red action: do-nothing, Blue reward:0.85\n", + "step: 1911, Red action: do-nothing, Blue reward:0.85\n", + "step: 1912, Red action: do-nothing, Blue reward:0.85\n", + "step: 1913, Red action: do-nothing, Blue reward:0.85\n", + "step: 1914, Red action: do-nothing, Blue reward:0.85\n", + "step: 1915, Red action: do-nothing, Blue reward:0.85\n", + "step: 1916, Red action: do-nothing, Blue reward:0.85\n", + "step: 1917, Red action: do-nothing, Blue reward:0.85\n", + "step: 1918, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1919, Red action: do-nothing, Blue reward:0.85\n", + "step: 1920, Red action: do-nothing, Blue reward:0.85\n", + "step: 1921, Red action: do-nothing, Blue reward:0.85\n", + "step: 1922, Red action: do-nothing, Blue reward:0.85\n", + "step: 1923, Red action: do-nothing, Blue reward:0.85\n", + "step: 1924, Red action: do-nothing, Blue reward:0.85\n", + "step: 1925, Red action: do-nothing, Blue reward:0.85\n", + "step: 1926, Red action: do-nothing, Blue reward:0.85\n", + "step: 1927, Red action: do-nothing, Blue reward:0.85\n", + "step: 1928, Red action: do-nothing, Blue reward:0.85\n", + "step: 1929, Red action: do-nothing, Blue reward:0.85\n", + "step: 1930, Red action: do-nothing, Blue reward:0.85\n", + "step: 1931, Red action: do-nothing, Blue reward:0.85\n", + "step: 1932, Red action: do-nothing, Blue reward:0.85\n", + "step: 1933, Red action: do-nothing, Blue reward:0.85\n", + "step: 1934, Red action: do-nothing, Blue reward:0.85\n", + "step: 1935, Red action: do-nothing, Blue reward:0.85\n", + "step: 1936, Red action: do-nothing, Blue reward:0.85\n", + "step: 1937, Red action: do-nothing, Blue reward:0.85\n", + "step: 1938, Red action: do-nothing, Blue reward:0.85\n", + "step: 1939, Red action: do-nothing, Blue reward:0.85\n", + "step: 1940, Red action: do-nothing, Blue reward:0.85\n", + "step: 1941, Red action: do-nothing, Blue reward:0.85\n", + "step: 1942, Red action: do-nothing, Blue reward:0.85\n", + "step: 1943, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1944, Red action: do-nothing, Blue reward:0.85\n", + "step: 1945, Red action: do-nothing, Blue reward:0.85\n", + "step: 1946, Red action: do-nothing, Blue reward:0.85\n", + "step: 1947, Red action: do-nothing, Blue reward:0.85\n", + "step: 1948, Red action: do-nothing, Blue reward:0.85\n", + "step: 1949, Red action: do-nothing, Blue reward:0.85\n", + "step: 1950, Red action: do-nothing, Blue reward:0.85\n", + "step: 1951, Red action: do-nothing, Blue reward:0.85\n", + "step: 1952, Red action: do-nothing, Blue reward:0.85\n", + "step: 1953, Red action: do-nothing, Blue reward:0.85\n", + "step: 1954, Red action: do-nothing, Blue reward:0.85\n", + "step: 1955, Red action: do-nothing, Blue reward:0.85\n", + "step: 1956, Red action: do-nothing, Blue reward:0.85\n", + "step: 1957, Red action: do-nothing, Blue reward:0.85\n", + "step: 1958, Red action: do-nothing, Blue reward:0.85\n", + "step: 1959, Red action: do-nothing, Blue reward:0.85\n", + "step: 1960, Red action: do-nothing, Blue reward:0.85\n", + "step: 1961, Red action: do-nothing, Blue reward:0.85\n", + "step: 1962, Red action: do-nothing, Blue reward:0.85\n", + "step: 1963, Red action: do-nothing, Blue reward:0.85\n", + "step: 1964, Red action: do-nothing, Blue reward:0.85\n", + "step: 1965, Red action: do-nothing, Blue reward:0.85\n", + "step: 1966, Red action: do-nothing, Blue reward:0.85\n", + "step: 1967, Red action: do-nothing, Blue reward:0.85\n", + "step: 1968, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1969, Red action: do-nothing, Blue reward:0.85\n", + "step: 1970, Red action: do-nothing, Blue reward:0.85\n", + "step: 1971, Red action: do-nothing, Blue reward:0.85\n", + "step: 1972, Red action: do-nothing, Blue reward:0.85\n", + "step: 1973, Red action: do-nothing, Blue reward:0.85\n", + "step: 1974, Red action: do-nothing, Blue reward:0.85\n", + "step: 1975, Red action: do-nothing, Blue reward:0.85\n", + "step: 1976, Red action: do-nothing, Blue reward:0.85\n", + "step: 1977, Red action: do-nothing, Blue reward:0.85\n", + "step: 1978, Red action: do-nothing, Blue reward:0.85\n", + "step: 1979, Red action: do-nothing, Blue reward:0.85\n", + "step: 1980, Red action: do-nothing, Blue reward:0.85\n", + "step: 1981, Red action: do-nothing, Blue reward:0.85\n", + "step: 1982, Red action: do-nothing, Blue reward:0.85\n", + "step: 1983, Red action: do-nothing, Blue reward:0.85\n", + "step: 1984, Red action: do-nothing, Blue reward:0.85\n", + "step: 1985, Red action: do-nothing, Blue reward:0.85\n", + "step: 1986, Red action: do-nothing, Blue reward:0.85\n", + "step: 1987, Red action: node-application-execute, Blue reward:0.85\n", + "step: 1988, Red action: do-nothing, Blue reward:0.85\n", + "step: 1989, Red action: do-nothing, Blue reward:0.85\n", + "step: 1990, Red action: do-nothing, Blue reward:0.85\n", + "step: 1991, Red action: do-nothing, Blue reward:0.85\n", + "step: 1992, Red action: do-nothing, Blue reward:0.85\n", + "step: 1993, Red action: do-nothing, Blue reward:0.85\n", + "step: 1994, Red action: do-nothing, Blue reward:0.85\n", + "step: 1995, Red action: do-nothing, Blue reward:0.85\n", + "step: 1996, Red action: do-nothing, Blue reward:0.85\n", + "step: 1997, Red action: do-nothing, Blue reward:0.85\n", + "step: 1998, Red action: do-nothing, Blue reward:0.85\n", + "step: 1999, Red action: do-nothing, Blue reward:0.85\n", + "step: 2000, Red action: do-nothing, Blue reward:0.85\n", + "step: 2001, Red action: do-nothing, Blue reward:0.85\n", + "step: 2002, Red action: do-nothing, Blue reward:0.85\n", + "step: 2003, Red action: do-nothing, Blue reward:0.85\n", + "step: 2004, Red action: do-nothing, Blue reward:0.85\n", + "step: 2005, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2006, Red action: do-nothing, Blue reward:0.85\n", + "step: 2007, Red action: do-nothing, Blue reward:0.85\n", + "step: 2008, Red action: do-nothing, Blue reward:0.85\n", + "step: 2009, Red action: do-nothing, Blue reward:0.85\n", + "step: 2010, Red action: do-nothing, Blue reward:0.85\n", + "step: 2011, Red action: do-nothing, Blue reward:0.85\n", + "step: 2012, Red action: do-nothing, Blue reward:0.85\n", + "step: 2013, Red action: do-nothing, Blue reward:0.85\n", + "step: 2014, Red action: do-nothing, Blue reward:0.85\n", + "step: 2015, Red action: do-nothing, Blue reward:0.85\n", + "step: 2016, Red action: do-nothing, Blue reward:0.85\n", + "step: 2017, Red action: do-nothing, Blue reward:0.85\n", + "step: 2018, Red action: do-nothing, Blue reward:0.85\n", + "step: 2019, Red action: do-nothing, Blue reward:0.85\n", + "step: 2020, Red action: do-nothing, Blue reward:0.85\n", + "step: 2021, Red action: do-nothing, Blue reward:0.85\n", + "step: 2022, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2023, Red action: do-nothing, Blue reward:0.85\n", + "step: 2024, Red action: do-nothing, Blue reward:0.85\n", + "step: 2025, Red action: do-nothing, Blue reward:0.85\n", + "step: 2026, Red action: do-nothing, Blue reward:0.85\n", + "step: 2027, Red action: do-nothing, Blue reward:0.85\n", + "step: 2028, Red action: do-nothing, Blue reward:0.85\n", + "step: 2029, Red action: do-nothing, Blue reward:0.85\n", + "step: 2030, Red action: do-nothing, Blue reward:0.85\n", + "step: 2031, Red action: do-nothing, Blue reward:0.85\n", + "step: 2032, Red action: do-nothing, Blue reward:0.85\n", + "step: 2033, Red action: do-nothing, Blue reward:0.85\n", + "step: 2034, Red action: do-nothing, Blue reward:0.85\n", + "step: 2035, Red action: do-nothing, Blue reward:0.85\n", + "step: 2036, Red action: do-nothing, Blue reward:0.85\n", + "step: 2037, Red action: do-nothing, Blue reward:0.85\n", + "step: 2038, Red action: do-nothing, Blue reward:0.85\n", + "step: 2039, Red action: do-nothing, Blue reward:0.85\n", + "step: 2040, Red action: do-nothing, Blue reward:0.85\n", + "step: 2041, Red action: do-nothing, Blue reward:0.85\n", + "step: 2042, Red action: do-nothing, Blue reward:0.85\n", + "step: 2043, Red action: do-nothing, Blue reward:0.85\n", + "step: 2044, Red action: do-nothing, Blue reward:0.85\n", + "step: 2045, Red action: do-nothing, Blue reward:0.85\n", + "step: 2046, Red action: do-nothing, Blue reward:0.85\n", + "step: 2047, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2048, Red action: do-nothing, Blue reward:0.85\n", + "step: 2049, Red action: do-nothing, Blue reward:0.85\n", + "step: 2050, Red action: do-nothing, Blue reward:0.85\n", + "step: 2051, Red action: do-nothing, Blue reward:0.85\n", + "step: 2052, Red action: do-nothing, Blue reward:0.85\n", + "step: 2053, Red action: do-nothing, Blue reward:0.85\n", + "step: 2054, Red action: do-nothing, Blue reward:0.85\n", + "step: 2055, Red action: do-nothing, Blue reward:0.85\n", + "step: 2056, Red action: do-nothing, Blue reward:0.85\n", + "step: 2057, Red action: do-nothing, Blue reward:0.85\n", + "step: 2058, Red action: do-nothing, Blue reward:0.85\n", + "step: 2059, Red action: do-nothing, Blue reward:0.85\n", + "step: 2060, Red action: do-nothing, Blue reward:0.85\n", + "step: 2061, Red action: do-nothing, Blue reward:0.85\n", + "step: 2062, Red action: do-nothing, Blue reward:0.85\n", + "step: 2063, Red action: do-nothing, Blue reward:0.85\n", + "step: 2064, Red action: do-nothing, Blue reward:0.85\n", + "step: 2065, Red action: do-nothing, Blue reward:0.85\n", + "step: 2066, Red action: do-nothing, Blue reward:0.85\n", + "step: 2067, Red action: do-nothing, Blue reward:0.85\n", + "step: 2068, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2069, Red action: do-nothing, Blue reward:0.85\n", + "step: 2070, Red action: do-nothing, Blue reward:0.85\n", + "step: 2071, Red action: do-nothing, Blue reward:0.85\n", + "step: 2072, Red action: do-nothing, Blue reward:0.85\n", + "step: 2073, Red action: do-nothing, Blue reward:0.85\n", + "step: 2074, Red action: do-nothing, Blue reward:0.85\n", + "step: 2075, Red action: do-nothing, Blue reward:0.85\n", + "step: 2076, Red action: do-nothing, Blue reward:0.85\n", + "step: 2077, Red action: do-nothing, Blue reward:0.85\n", + "step: 2078, Red action: do-nothing, Blue reward:0.85\n", + "step: 2079, Red action: do-nothing, Blue reward:0.85\n", + "step: 2080, Red action: do-nothing, Blue reward:0.85\n", + "step: 2081, Red action: do-nothing, Blue reward:0.85\n", + "step: 2082, Red action: do-nothing, Blue reward:0.85\n", + "step: 2083, Red action: do-nothing, Blue reward:0.85\n", + "step: 2084, Red action: do-nothing, Blue reward:0.85\n", + "step: 2085, Red action: do-nothing, Blue reward:0.85\n", + "step: 2086, Red action: do-nothing, Blue reward:0.85\n", + "step: 2087, Red action: do-nothing, Blue reward:0.85\n", + "step: 2088, Red action: do-nothing, Blue reward:0.85\n", + "step: 2089, Red action: do-nothing, Blue reward:0.85\n", + "step: 2090, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2091, Red action: do-nothing, Blue reward:0.85\n", + "step: 2092, Red action: do-nothing, Blue reward:0.85\n", + "step: 2093, Red action: do-nothing, Blue reward:0.85\n", + "step: 2094, Red action: do-nothing, Blue reward:0.85\n", + "step: 2095, Red action: do-nothing, Blue reward:0.85\n", + "step: 2096, Red action: do-nothing, Blue reward:0.85\n", + "step: 2097, Red action: do-nothing, Blue reward:0.85\n", + "step: 2098, Red action: do-nothing, Blue reward:0.85\n", + "step: 2099, Red action: do-nothing, Blue reward:0.85\n", + "step: 2100, Red action: do-nothing, Blue reward:0.85\n", + "step: 2101, Red action: do-nothing, Blue reward:0.85\n", + "step: 2102, Red action: do-nothing, Blue reward:0.85\n", + "step: 2103, Red action: do-nothing, Blue reward:0.85\n", + "step: 2104, Red action: do-nothing, Blue reward:0.85\n", + "step: 2105, Red action: do-nothing, Blue reward:0.85\n", + "step: 2106, Red action: do-nothing, Blue reward:0.85\n", + "step: 2107, Red action: do-nothing, Blue reward:0.85\n", + "step: 2108, Red action: do-nothing, Blue reward:0.85\n", + "step: 2109, Red action: do-nothing, Blue reward:0.85\n", + "step: 2110, Red action: do-nothing, Blue reward:0.85\n", + "step: 2111, Red action: do-nothing, Blue reward:0.85\n", + "step: 2112, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2113, Red action: do-nothing, Blue reward:0.85\n", + "step: 2114, Red action: do-nothing, Blue reward:0.85\n", + "step: 2115, Red action: do-nothing, Blue reward:0.85\n", + "step: 2116, Red action: do-nothing, Blue reward:0.85\n", + "step: 2117, Red action: do-nothing, Blue reward:0.85\n", + "step: 2118, Red action: do-nothing, Blue reward:0.85\n", + "step: 2119, Red action: do-nothing, Blue reward:0.85\n", + "step: 2120, Red action: do-nothing, Blue reward:0.85\n", + "step: 2121, Red action: do-nothing, Blue reward:0.85\n", + "step: 2122, Red action: do-nothing, Blue reward:0.85\n", + "step: 2123, Red action: do-nothing, Blue reward:0.85\n", + "step: 2124, Red action: do-nothing, Blue reward:0.85\n", + "step: 2125, Red action: do-nothing, Blue reward:0.85\n", + "step: 2126, Red action: do-nothing, Blue reward:0.85\n", + "step: 2127, Red action: do-nothing, Blue reward:0.85\n", + "step: 2128, Red action: do-nothing, Blue reward:0.85\n", + "step: 2129, Red action: do-nothing, Blue reward:0.85\n", + "step: 2130, Red action: do-nothing, Blue reward:0.85\n", + "step: 2131, Red action: do-nothing, Blue reward:0.85\n", + "step: 2132, Red action: do-nothing, Blue reward:0.85\n", + "step: 2133, Red action: do-nothing, Blue reward:0.85\n", + "step: 2134, Red action: do-nothing, Blue reward:0.85\n", + "step: 2135, Red action: do-nothing, Blue reward:0.85\n", + "step: 2136, Red action: do-nothing, Blue reward:0.85\n", + "step: 2137, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2138, Red action: do-nothing, Blue reward:0.85\n", + "step: 2139, Red action: do-nothing, Blue reward:0.85\n", + "step: 2140, Red action: do-nothing, Blue reward:0.85\n", + "step: 2141, Red action: do-nothing, Blue reward:0.85\n", + "step: 2142, Red action: do-nothing, Blue reward:0.85\n", + "step: 2143, Red action: do-nothing, Blue reward:0.85\n", + "step: 2144, Red action: do-nothing, Blue reward:0.85\n", + "step: 2145, Red action: do-nothing, Blue reward:0.85\n", + "step: 2146, Red action: do-nothing, Blue reward:0.85\n", + "step: 2147, Red action: do-nothing, Blue reward:0.85\n", + "step: 2148, Red action: do-nothing, Blue reward:0.85\n", + "step: 2149, Red action: do-nothing, Blue reward:0.85\n", + "step: 2150, Red action: do-nothing, Blue reward:0.85\n", + "step: 2151, Red action: do-nothing, Blue reward:0.85\n", + "step: 2152, Red action: do-nothing, Blue reward:0.85\n", + "step: 2153, Red action: do-nothing, Blue reward:0.85\n", + "step: 2154, Red action: do-nothing, Blue reward:0.85\n", + "step: 2155, Red action: do-nothing, Blue reward:0.85\n", + "step: 2156, Red action: do-nothing, Blue reward:0.85\n", + "step: 2157, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2158, Red action: do-nothing, Blue reward:0.85\n", + "step: 2159, Red action: do-nothing, Blue reward:0.85\n", + "step: 2160, Red action: do-nothing, Blue reward:0.85\n", + "step: 2161, Red action: do-nothing, Blue reward:0.85\n", + "step: 2162, Red action: do-nothing, Blue reward:0.85\n", + "step: 2163, Red action: do-nothing, Blue reward:0.85\n", + "step: 2164, Red action: do-nothing, Blue reward:0.85\n", + "step: 2165, Red action: do-nothing, Blue reward:0.85\n", + "step: 2166, Red action: do-nothing, Blue reward:0.85\n", + "step: 2167, Red action: do-nothing, Blue reward:0.85\n", + "step: 2168, Red action: do-nothing, Blue reward:0.85\n", + "step: 2169, Red action: do-nothing, Blue reward:0.85\n", + "step: 2170, Red action: do-nothing, Blue reward:0.85\n", + "step: 2171, Red action: do-nothing, Blue reward:0.85\n", + "step: 2172, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2173, Red action: do-nothing, Blue reward:0.85\n", + "step: 2174, Red action: do-nothing, Blue reward:0.85\n", + "step: 2175, Red action: do-nothing, Blue reward:0.85\n", + "step: 2176, Red action: do-nothing, Blue reward:0.85\n", + "step: 2177, Red action: do-nothing, Blue reward:0.85\n", + "step: 2178, Red action: do-nothing, Blue reward:0.85\n", + "step: 2179, Red action: do-nothing, Blue reward:0.85\n", + "step: 2180, Red action: do-nothing, Blue reward:0.85\n", + "step: 2181, Red action: do-nothing, Blue reward:0.85\n", + "step: 2182, Red action: do-nothing, Blue reward:0.85\n", + "step: 2183, Red action: do-nothing, Blue reward:0.85\n", + "step: 2184, Red action: do-nothing, Blue reward:0.85\n", + "step: 2185, Red action: do-nothing, Blue reward:0.85\n", + "step: 2186, Red action: do-nothing, Blue reward:0.85\n", + "step: 2187, Red action: do-nothing, Blue reward:0.85\n", + "step: 2188, Red action: do-nothing, Blue reward:0.85\n", + "step: 2189, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2190, Red action: do-nothing, Blue reward:0.85\n", + "step: 2191, Red action: do-nothing, Blue reward:0.85\n", + "step: 2192, Red action: do-nothing, Blue reward:0.85\n", + "step: 2193, Red action: do-nothing, Blue reward:0.85\n", + "step: 2194, Red action: do-nothing, Blue reward:0.85\n", + "step: 2195, Red action: do-nothing, Blue reward:0.85\n", + "step: 2196, Red action: do-nothing, Blue reward:0.85\n", + "step: 2197, Red action: do-nothing, Blue reward:0.85\n", + "step: 2198, Red action: do-nothing, Blue reward:0.85\n", + "step: 2199, Red action: do-nothing, Blue reward:0.85\n", + "step: 2200, Red action: do-nothing, Blue reward:0.85\n", + "step: 2201, Red action: do-nothing, Blue reward:0.85\n", + "step: 2202, Red action: do-nothing, Blue reward:0.85\n", + "step: 2203, Red action: do-nothing, Blue reward:0.85\n", + "step: 2204, Red action: do-nothing, Blue reward:0.85\n", + "step: 2205, Red action: do-nothing, Blue reward:0.85\n", + "step: 2206, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2207, Red action: do-nothing, Blue reward:0.85\n", + "step: 2208, Red action: do-nothing, Blue reward:0.85\n", + "step: 2209, Red action: do-nothing, Blue reward:0.85\n", + "step: 2210, Red action: do-nothing, Blue reward:0.85\n", + "step: 2211, Red action: do-nothing, Blue reward:0.85\n", + "step: 2212, Red action: do-nothing, Blue reward:0.85\n", + "step: 2213, Red action: do-nothing, Blue reward:0.85\n", + "step: 2214, Red action: do-nothing, Blue reward:0.85\n", + "step: 2215, Red action: do-nothing, Blue reward:0.85\n", + "step: 2216, Red action: do-nothing, Blue reward:0.85\n", + "step: 2217, Red action: do-nothing, Blue reward:0.85\n", + "step: 2218, Red action: do-nothing, Blue reward:0.85\n", + "step: 2219, Red action: do-nothing, Blue reward:0.85\n", + "step: 2220, Red action: do-nothing, Blue reward:0.85\n", + "step: 2221, Red action: do-nothing, Blue reward:0.85\n", + "step: 2222, Red action: do-nothing, Blue reward:0.85\n", + "step: 2223, Red action: do-nothing, Blue reward:0.85\n", + "step: 2224, Red action: do-nothing, Blue reward:0.85\n", + "step: 2225, Red action: do-nothing, Blue reward:0.85\n", + "step: 2226, Red action: do-nothing, Blue reward:0.85\n", + "step: 2227, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2228, Red action: do-nothing, Blue reward:0.85\n", + "step: 2229, Red action: do-nothing, Blue reward:0.85\n", + "step: 2230, Red action: do-nothing, Blue reward:0.85\n", + "step: 2231, Red action: do-nothing, Blue reward:0.85\n", + "step: 2232, Red action: do-nothing, Blue reward:0.85\n", + "step: 2233, Red action: do-nothing, Blue reward:0.85\n", + "step: 2234, Red action: do-nothing, Blue reward:0.85\n", + "step: 2235, Red action: do-nothing, Blue reward:0.85\n", + "step: 2236, Red action: do-nothing, Blue reward:0.85\n", + "step: 2237, Red action: do-nothing, Blue reward:0.85\n", + "step: 2238, Red action: do-nothing, Blue reward:0.85\n", + "step: 2239, Red action: do-nothing, Blue reward:0.85\n", + "step: 2240, Red action: do-nothing, Blue reward:0.85\n", + "step: 2241, Red action: do-nothing, Blue reward:0.85\n", + "step: 2242, Red action: do-nothing, Blue reward:0.85\n", + "step: 2243, Red action: do-nothing, Blue reward:0.85\n", + "step: 2244, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2245, Red action: do-nothing, Blue reward:0.85\n", + "step: 2246, Red action: do-nothing, Blue reward:0.85\n", + "step: 2247, Red action: do-nothing, Blue reward:0.85\n", + "step: 2248, Red action: do-nothing, Blue reward:0.85\n", + "step: 2249, Red action: do-nothing, Blue reward:0.85\n", + "step: 2250, Red action: do-nothing, Blue reward:0.85\n", + "step: 2251, Red action: do-nothing, Blue reward:0.85\n", + "step: 2252, Red action: do-nothing, Blue reward:0.85\n", + "step: 2253, Red action: do-nothing, Blue reward:0.85\n", + "step: 2254, Red action: do-nothing, Blue reward:0.85\n", + "step: 2255, Red action: do-nothing, Blue reward:0.85\n", + "step: 2256, Red action: do-nothing, Blue reward:0.85\n", + "step: 2257, Red action: do-nothing, Blue reward:0.85\n", + "step: 2258, Red action: do-nothing, Blue reward:0.85\n", + "step: 2259, Red action: do-nothing, Blue reward:0.85\n", + "step: 2260, Red action: do-nothing, Blue reward:0.85\n", + "step: 2261, Red action: do-nothing, Blue reward:0.85\n", + "step: 2262, Red action: do-nothing, Blue reward:0.85\n", + "step: 2263, Red action: do-nothing, Blue reward:0.85\n", + "step: 2264, Red action: do-nothing, Blue reward:0.85\n", + "step: 2265, Red action: do-nothing, Blue reward:0.85\n", + "step: 2266, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2267, Red action: do-nothing, Blue reward:0.85\n", + "step: 2268, Red action: do-nothing, Blue reward:0.85\n", + "step: 2269, Red action: do-nothing, Blue reward:0.85\n", + "step: 2270, Red action: do-nothing, Blue reward:0.85\n", + "step: 2271, Red action: do-nothing, Blue reward:0.85\n", + "step: 2272, Red action: do-nothing, Blue reward:0.85\n", + "step: 2273, Red action: do-nothing, Blue reward:0.85\n", + "step: 2274, Red action: do-nothing, Blue reward:0.85\n", + "step: 2275, Red action: do-nothing, Blue reward:0.85\n", + "step: 2276, Red action: do-nothing, Blue reward:0.85\n", + "step: 2277, Red action: do-nothing, Blue reward:0.85\n", + "step: 2278, Red action: do-nothing, Blue reward:0.85\n", + "step: 2279, Red action: do-nothing, Blue reward:0.85\n", + "step: 2280, Red action: do-nothing, Blue reward:0.85\n", + "step: 2281, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2282, Red action: do-nothing, Blue reward:0.85\n", + "step: 2283, Red action: do-nothing, Blue reward:0.85\n", + "step: 2284, Red action: do-nothing, Blue reward:0.85\n", + "step: 2285, Red action: do-nothing, Blue reward:0.85\n", + "step: 2286, Red action: do-nothing, Blue reward:0.85\n", + "step: 2287, Red action: do-nothing, Blue reward:0.85\n", + "step: 2288, Red action: do-nothing, Blue reward:0.85\n", + "step: 2289, Red action: do-nothing, Blue reward:0.85\n", + "step: 2290, Red action: do-nothing, Blue reward:0.85\n", + "step: 2291, Red action: do-nothing, Blue reward:0.85\n", + "step: 2292, Red action: do-nothing, Blue reward:0.85\n", + "step: 2293, Red action: do-nothing, Blue reward:0.85\n", + "step: 2294, Red action: do-nothing, Blue reward:0.85\n", + "step: 2295, Red action: do-nothing, Blue reward:0.85\n", + "step: 2296, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2297, Red action: do-nothing, Blue reward:0.85\n", + "step: 2298, Red action: do-nothing, Blue reward:0.85\n", + "step: 2299, Red action: do-nothing, Blue reward:0.85\n", + "step: 2300, Red action: do-nothing, Blue reward:0.85\n", + "step: 2301, Red action: do-nothing, Blue reward:0.85\n", + "step: 2302, Red action: do-nothing, Blue reward:0.85\n", + "step: 2303, Red action: do-nothing, Blue reward:0.85\n", + "step: 2304, Red action: do-nothing, Blue reward:0.85\n", + "step: 2305, Red action: do-nothing, Blue reward:0.85\n", + "step: 2306, Red action: do-nothing, Blue reward:0.85\n", + "step: 2307, Red action: do-nothing, Blue reward:0.85\n", + "step: 2308, Red action: do-nothing, Blue reward:0.85\n", + "step: 2309, Red action: do-nothing, Blue reward:0.85\n", + "step: 2310, Red action: do-nothing, Blue reward:0.85\n", + "step: 2311, Red action: do-nothing, Blue reward:0.85\n", + "step: 2312, Red action: do-nothing, Blue reward:0.85\n", + "step: 2313, Red action: do-nothing, Blue reward:0.85\n", + "step: 2314, Red action: do-nothing, Blue reward:0.85\n", + "step: 2315, Red action: do-nothing, Blue reward:0.85\n", + "step: 2316, Red action: do-nothing, Blue reward:0.85\n", + "step: 2317, Red action: do-nothing, Blue reward:0.85\n", + "step: 2318, Red action: do-nothing, Blue reward:0.85\n", + "step: 2319, Red action: do-nothing, Blue reward:0.85\n", + "step: 2320, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2321, Red action: do-nothing, Blue reward:0.85\n", + "step: 2322, Red action: do-nothing, Blue reward:0.85\n", + "step: 2323, Red action: do-nothing, Blue reward:0.85\n", + "step: 2324, Red action: do-nothing, Blue reward:0.85\n", + "step: 2325, Red action: do-nothing, Blue reward:0.85\n", + "step: 2326, Red action: do-nothing, Blue reward:0.85\n", + "step: 2327, Red action: do-nothing, Blue reward:0.85\n", + "step: 2328, Red action: do-nothing, Blue reward:0.85\n", + "step: 2329, Red action: do-nothing, Blue reward:0.85\n", + "step: 2330, Red action: do-nothing, Blue reward:0.85\n", + "step: 2331, Red action: do-nothing, Blue reward:0.85\n", + "step: 2332, Red action: do-nothing, Blue reward:0.85\n", + "step: 2333, Red action: do-nothing, Blue reward:0.85\n", + "step: 2334, Red action: do-nothing, Blue reward:0.85\n", + "step: 2335, Red action: do-nothing, Blue reward:0.85\n", + "step: 2336, Red action: do-nothing, Blue reward:0.85\n", + "step: 2337, Red action: do-nothing, Blue reward:0.85\n", + "step: 2338, Red action: do-nothing, Blue reward:0.85\n", + "step: 2339, Red action: do-nothing, Blue reward:0.85\n", + "step: 2340, Red action: do-nothing, Blue reward:0.85\n", + "step: 2341, Red action: do-nothing, Blue reward:0.85\n", + "step: 2342, Red action: do-nothing, Blue reward:0.85\n", + "step: 2343, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2344, Red action: do-nothing, Blue reward:0.85\n", + "step: 2345, Red action: do-nothing, Blue reward:0.85\n", + "step: 2346, Red action: do-nothing, Blue reward:0.85\n", + "step: 2347, Red action: do-nothing, Blue reward:0.85\n", + "step: 2348, Red action: do-nothing, Blue reward:0.85\n", + "step: 2349, Red action: do-nothing, Blue reward:0.85\n", + "step: 2350, Red action: do-nothing, Blue reward:0.85\n", + "step: 2351, Red action: do-nothing, Blue reward:0.85\n", + "step: 2352, Red action: do-nothing, Blue reward:0.85\n", + "step: 2353, Red action: do-nothing, Blue reward:0.85\n", + "step: 2354, Red action: do-nothing, Blue reward:0.85\n", + "step: 2355, Red action: do-nothing, Blue reward:0.85\n", + "step: 2356, Red action: do-nothing, Blue reward:0.85\n", + "step: 2357, Red action: do-nothing, Blue reward:0.85\n", + "step: 2358, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2359, Red action: do-nothing, Blue reward:0.85\n", + "step: 2360, Red action: do-nothing, Blue reward:0.85\n", + "step: 2361, Red action: do-nothing, Blue reward:0.85\n", + "step: 2362, Red action: do-nothing, Blue reward:0.85\n", + "step: 2363, Red action: do-nothing, Blue reward:0.85\n", + "step: 2364, Red action: do-nothing, Blue reward:0.85\n", + "step: 2365, Red action: do-nothing, Blue reward:0.85\n", + "step: 2366, Red action: do-nothing, Blue reward:0.85\n", + "step: 2367, Red action: do-nothing, Blue reward:0.85\n", + "step: 2368, Red action: do-nothing, Blue reward:0.85\n", + "step: 2369, Red action: do-nothing, Blue reward:0.85\n", + "step: 2370, Red action: do-nothing, Blue reward:0.85\n", + "step: 2371, Red action: do-nothing, Blue reward:0.85\n", + "step: 2372, Red action: do-nothing, Blue reward:0.85\n", + "step: 2373, Red action: do-nothing, Blue reward:0.85\n", + "step: 2374, Red action: do-nothing, Blue reward:0.85\n", + "step: 2375, Red action: do-nothing, Blue reward:0.85\n", + "step: 2376, Red action: do-nothing, Blue reward:0.85\n", + "step: 2377, Red action: do-nothing, Blue reward:0.85\n", + "step: 2378, Red action: do-nothing, Blue reward:0.85\n", + "step: 2379, Red action: do-nothing, Blue reward:0.85\n", + "step: 2380, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2381, Red action: do-nothing, Blue reward:0.85\n", + "step: 2382, Red action: do-nothing, Blue reward:0.85\n", + "step: 2383, Red action: do-nothing, Blue reward:0.85\n", + "step: 2384, Red action: do-nothing, Blue reward:0.85\n", + "step: 2385, Red action: do-nothing, Blue reward:0.85\n", + "step: 2386, Red action: do-nothing, Blue reward:0.85\n", + "step: 2387, Red action: do-nothing, Blue reward:0.85\n", + "step: 2388, Red action: do-nothing, Blue reward:0.85\n", + "step: 2389, Red action: do-nothing, Blue reward:0.85\n", + "step: 2390, Red action: do-nothing, Blue reward:0.85\n", + "step: 2391, Red action: do-nothing, Blue reward:0.85\n", + "step: 2392, Red action: do-nothing, Blue reward:0.85\n", + "step: 2393, Red action: do-nothing, Blue reward:0.85\n", + "step: 2394, Red action: do-nothing, Blue reward:0.85\n", + "step: 2395, Red action: do-nothing, Blue reward:0.85\n", + "step: 2396, Red action: do-nothing, Blue reward:0.85\n", + "step: 2397, Red action: do-nothing, Blue reward:0.85\n", + "step: 2398, Red action: do-nothing, Blue reward:0.85\n", + "step: 2399, Red action: do-nothing, Blue reward:0.85\n", + "step: 2400, Red action: do-nothing, Blue reward:0.85\n", + "step: 2401, Red action: do-nothing, Blue reward:0.85\n", + "step: 2402, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2403, Red action: do-nothing, Blue reward:0.85\n", + "step: 2404, Red action: do-nothing, Blue reward:0.85\n", + "step: 2405, Red action: do-nothing, Blue reward:0.85\n", + "step: 2406, Red action: do-nothing, Blue reward:0.85\n", + "step: 2407, Red action: do-nothing, Blue reward:0.85\n", + "step: 2408, Red action: do-nothing, Blue reward:0.85\n", + "step: 2409, Red action: do-nothing, Blue reward:0.85\n", + "step: 2410, Red action: do-nothing, Blue reward:0.85\n", + "step: 2411, Red action: do-nothing, Blue reward:0.85\n", + "step: 2412, Red action: do-nothing, Blue reward:0.85\n", + "step: 2413, Red action: do-nothing, Blue reward:0.85\n", + "step: 2414, Red action: do-nothing, Blue reward:0.85\n", + "step: 2415, Red action: do-nothing, Blue reward:0.85\n", + "step: 2416, Red action: do-nothing, Blue reward:0.85\n", + "step: 2417, Red action: do-nothing, Blue reward:0.85\n", + "step: 2418, Red action: do-nothing, Blue reward:0.85\n", + "step: 2419, Red action: do-nothing, Blue reward:0.85\n", + "step: 2420, Red action: do-nothing, Blue reward:0.85\n", + "step: 2421, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2422, Red action: do-nothing, Blue reward:0.85\n", + "step: 2423, Red action: do-nothing, Blue reward:0.85\n", + "step: 2424, Red action: do-nothing, Blue reward:0.85\n", + "step: 2425, Red action: do-nothing, Blue reward:0.85\n", + "step: 2426, Red action: do-nothing, Blue reward:0.85\n", + "step: 2427, Red action: do-nothing, Blue reward:0.85\n", + "step: 2428, Red action: do-nothing, Blue reward:0.85\n", + "step: 2429, Red action: do-nothing, Blue reward:0.85\n", + "step: 2430, Red action: do-nothing, Blue reward:0.85\n", + "step: 2431, Red action: do-nothing, Blue reward:0.85\n", + "step: 2432, Red action: do-nothing, Blue reward:0.85\n", + "step: 2433, Red action: do-nothing, Blue reward:0.85\n", + "step: 2434, Red action: do-nothing, Blue reward:0.85\n", + "step: 2435, Red action: do-nothing, Blue reward:0.85\n", + "step: 2436, Red action: do-nothing, Blue reward:0.85\n", + "step: 2437, Red action: do-nothing, Blue reward:0.85\n", + "step: 2438, Red action: do-nothing, Blue reward:0.85\n", + "step: 2439, Red action: do-nothing, Blue reward:0.85\n", + "step: 2440, Red action: do-nothing, Blue reward:0.85\n", + "step: 2441, Red action: do-nothing, Blue reward:0.85\n", + "step: 2442, Red action: do-nothing, Blue reward:0.85\n", + "step: 2443, Red action: do-nothing, Blue reward:0.85\n", + "step: 2444, Red action: do-nothing, Blue reward:0.85\n", + "step: 2445, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2446, Red action: do-nothing, Blue reward:0.85\n", + "step: 2447, Red action: do-nothing, Blue reward:0.85\n", + "step: 2448, Red action: do-nothing, Blue reward:0.85\n", + "step: 2449, Red action: do-nothing, Blue reward:0.85\n", + "step: 2450, Red action: do-nothing, Blue reward:0.85\n", + "step: 2451, Red action: do-nothing, Blue reward:0.85\n", + "step: 2452, Red action: do-nothing, Blue reward:0.85\n", + "step: 2453, Red action: do-nothing, Blue reward:0.85\n", + "step: 2454, Red action: do-nothing, Blue reward:0.85\n", + "step: 2455, Red action: do-nothing, Blue reward:0.85\n", + "step: 2456, Red action: do-nothing, Blue reward:0.85\n", + "step: 2457, Red action: do-nothing, Blue reward:0.85\n", + "step: 2458, Red action: do-nothing, Blue reward:0.85\n", + "step: 2459, Red action: do-nothing, Blue reward:0.85\n", + "step: 2460, Red action: do-nothing, Blue reward:0.85\n", + "step: 2461, Red action: do-nothing, Blue reward:0.85\n", + "step: 2462, Red action: do-nothing, Blue reward:0.85\n", + "step: 2463, Red action: do-nothing, Blue reward:0.85\n", + "step: 2464, Red action: do-nothing, Blue reward:0.85\n", + "step: 2465, Red action: do-nothing, Blue reward:0.85\n", + "step: 2466, Red action: do-nothing, Blue reward:0.85\n", + "step: 2467, Red action: do-nothing, Blue reward:0.85\n", + "step: 2468, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2469, Red action: do-nothing, Blue reward:0.85\n", + "step: 2470, Red action: do-nothing, Blue reward:0.85\n", + "step: 2471, Red action: do-nothing, Blue reward:0.85\n", + "step: 2472, Red action: do-nothing, Blue reward:0.85\n", + "step: 2473, Red action: do-nothing, Blue reward:0.85\n", + "step: 2474, Red action: do-nothing, Blue reward:0.85\n", + "step: 2475, Red action: do-nothing, Blue reward:0.85\n", + "step: 2476, Red action: do-nothing, Blue reward:0.85\n", + "step: 2477, Red action: do-nothing, Blue reward:0.85\n", + "step: 2478, Red action: do-nothing, Blue reward:0.85\n", + "step: 2479, Red action: do-nothing, Blue reward:0.85\n", + "step: 2480, Red action: do-nothing, Blue reward:0.85\n", + "step: 2481, Red action: do-nothing, Blue reward:0.85\n", + "step: 2482, Red action: do-nothing, Blue reward:0.85\n", + "step: 2483, Red action: do-nothing, Blue reward:0.85\n", + "step: 2484, Red action: do-nothing, Blue reward:0.85\n", + "step: 2485, Red action: do-nothing, Blue reward:0.85\n", + "step: 2486, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2487, Red action: do-nothing, Blue reward:0.85\n", + "step: 2488, Red action: do-nothing, Blue reward:0.85\n", + "step: 2489, Red action: do-nothing, Blue reward:0.85\n", + "step: 2490, Red action: do-nothing, Blue reward:0.85\n", + "step: 2491, Red action: do-nothing, Blue reward:0.85\n", + "step: 2492, Red action: do-nothing, Blue reward:0.85\n", + "step: 2493, Red action: do-nothing, Blue reward:0.85\n", + "step: 2494, Red action: do-nothing, Blue reward:0.85\n", + "step: 2495, Red action: do-nothing, Blue reward:0.85\n", + "step: 2496, Red action: do-nothing, Blue reward:0.85\n", + "step: 2497, Red action: do-nothing, Blue reward:0.85\n", + "step: 2498, Red action: do-nothing, Blue reward:0.85\n", + "step: 2499, Red action: do-nothing, Blue reward:0.85\n", + "step: 2500, Red action: do-nothing, Blue reward:0.85\n", + "step: 2501, Red action: do-nothing, Blue reward:0.85\n", + "step: 2502, Red action: do-nothing, Blue reward:0.85\n", + "step: 2503, Red action: do-nothing, Blue reward:0.85\n", + "step: 2504, Red action: do-nothing, Blue reward:0.85\n", + "step: 2505, Red action: do-nothing, Blue reward:0.85\n", + "step: 2506, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2507, Red action: do-nothing, Blue reward:0.85\n", + "step: 2508, Red action: do-nothing, Blue reward:0.85\n", + "step: 2509, Red action: do-nothing, Blue reward:0.85\n", + "step: 2510, Red action: do-nothing, Blue reward:0.85\n", + "step: 2511, Red action: do-nothing, Blue reward:0.85\n", + "step: 2512, Red action: do-nothing, Blue reward:0.85\n", + "step: 2513, Red action: do-nothing, Blue reward:0.85\n", + "step: 2514, Red action: do-nothing, Blue reward:0.85\n", + "step: 2515, Red action: do-nothing, Blue reward:0.85\n", + "step: 2516, Red action: do-nothing, Blue reward:0.85\n", + "step: 2517, Red action: do-nothing, Blue reward:0.85\n", + "step: 2518, Red action: do-nothing, Blue reward:0.85\n", + "step: 2519, Red action: do-nothing, Blue reward:0.85\n", + "step: 2520, Red action: do-nothing, Blue reward:0.85\n", + "step: 2521, Red action: do-nothing, Blue reward:0.85\n", + "step: 2522, Red action: do-nothing, Blue reward:0.85\n", + "step: 2523, Red action: do-nothing, Blue reward:0.85\n", + "step: 2524, Red action: do-nothing, Blue reward:0.85\n", + "step: 2525, Red action: do-nothing, Blue reward:0.85\n", + "step: 2526, Red action: do-nothing, Blue reward:0.85\n", + "step: 2527, Red action: do-nothing, Blue reward:0.85\n", + "step: 2528, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2529, Red action: do-nothing, Blue reward:0.85\n", + "step: 2530, Red action: do-nothing, Blue reward:0.85\n", + "step: 2531, Red action: do-nothing, Blue reward:0.85\n", + "step: 2532, Red action: do-nothing, Blue reward:0.85\n", + "step: 2533, Red action: do-nothing, Blue reward:0.85\n", + "step: 2534, Red action: do-nothing, Blue reward:0.85\n", + "step: 2535, Red action: do-nothing, Blue reward:0.85\n", + "step: 2536, Red action: do-nothing, Blue reward:0.85\n", + "step: 2537, Red action: do-nothing, Blue reward:0.85\n", + "step: 2538, Red action: do-nothing, Blue reward:0.85\n", + "step: 2539, Red action: do-nothing, Blue reward:0.85\n", + "step: 2540, Red action: do-nothing, Blue reward:0.85\n", + "step: 2541, Red action: do-nothing, Blue reward:0.85\n", + "step: 2542, Red action: do-nothing, Blue reward:0.85\n", + "step: 2543, Red action: do-nothing, Blue reward:0.85\n", + "step: 2544, Red action: do-nothing, Blue reward:0.85\n", + "step: 2545, Red action: do-nothing, Blue reward:0.85\n", + "step: 2546, Red action: do-nothing, Blue reward:0.85\n", + "step: 2547, Red action: do-nothing, Blue reward:0.85\n", + "step: 2548, Red action: do-nothing, Blue reward:0.85\n", + "step: 2549, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2550, Red action: do-nothing, Blue reward:0.85\n", + "step: 2551, Red action: do-nothing, Blue reward:0.85\n", + "step: 2552, Red action: do-nothing, Blue reward:0.85\n", + "step: 2553, Red action: do-nothing, Blue reward:0.85\n", + "step: 2554, Red action: do-nothing, Blue reward:0.85\n", + "step: 2555, Red action: do-nothing, Blue reward:0.85\n", + "step: 2556, Red action: do-nothing, Blue reward:0.85\n", + "step: 2557, Red action: do-nothing, Blue reward:0.85\n", + "step: 2558, Red action: do-nothing, Blue reward:0.85\n", + "step: 2559, Red action: do-nothing, Blue reward:0.85\n", + "step: 2560, Red action: do-nothing, Blue reward:0.85\n", + "step: 2561, Red action: do-nothing, Blue reward:0.85\n", + "step: 2562, Red action: do-nothing, Blue reward:0.85\n", + "step: 2563, Red action: do-nothing, Blue reward:0.85\n", + "step: 2564, Red action: do-nothing, Blue reward:0.85\n", + "step: 2565, Red action: do-nothing, Blue reward:0.85\n", + "step: 2566, Red action: do-nothing, Blue reward:0.85\n", + "step: 2567, Red action: do-nothing, Blue reward:0.85\n", + "step: 2568, Red action: do-nothing, Blue reward:0.85\n", + "step: 2569, Red action: do-nothing, Blue reward:0.85\n", + "step: 2570, Red action: do-nothing, Blue reward:0.85\n", + "step: 2571, Red action: do-nothing, Blue reward:0.85\n", + "step: 2572, Red action: do-nothing, Blue reward:0.85\n", + "step: 2573, Red action: do-nothing, Blue reward:0.85\n", + "step: 2574, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2575, Red action: do-nothing, Blue reward:0.85\n", + "step: 2576, Red action: do-nothing, Blue reward:0.85\n", + "step: 2577, Red action: do-nothing, Blue reward:0.85\n", + "step: 2578, Red action: do-nothing, Blue reward:0.85\n", + "step: 2579, Red action: do-nothing, Blue reward:0.85\n", + "step: 2580, Red action: do-nothing, Blue reward:0.85\n", + "step: 2581, Red action: do-nothing, Blue reward:0.85\n", + "step: 2582, Red action: do-nothing, Blue reward:0.85\n", + "step: 2583, Red action: do-nothing, Blue reward:0.85\n", + "step: 2584, Red action: do-nothing, Blue reward:0.85\n", + "step: 2585, Red action: do-nothing, Blue reward:0.85\n", + "step: 2586, Red action: do-nothing, Blue reward:0.85\n", + "step: 2587, Red action: do-nothing, Blue reward:0.85\n", + "step: 2588, Red action: do-nothing, Blue reward:0.85\n", + "step: 2589, Red action: do-nothing, Blue reward:0.85\n", + "step: 2590, Red action: do-nothing, Blue reward:0.85\n", + "step: 2591, Red action: do-nothing, Blue reward:0.85\n", + "step: 2592, Red action: do-nothing, Blue reward:0.85\n", + "step: 2593, Red action: do-nothing, Blue reward:0.85\n", + "step: 2594, Red action: do-nothing, Blue reward:0.85\n", + "step: 2595, Red action: do-nothing, Blue reward:0.85\n", + "step: 2596, Red action: do-nothing, Blue reward:0.85\n", + "step: 2597, Red action: do-nothing, Blue reward:0.85\n", + "step: 2598, Red action: do-nothing, Blue reward:0.85\n", + "step: 2599, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2600, Red action: do-nothing, Blue reward:0.85\n", + "step: 2601, Red action: do-nothing, Blue reward:0.85\n", + "step: 2602, Red action: do-nothing, Blue reward:0.85\n", + "step: 2603, Red action: do-nothing, Blue reward:0.85\n", + "step: 2604, Red action: do-nothing, Blue reward:0.85\n", + "step: 2605, Red action: do-nothing, Blue reward:0.85\n", + "step: 2606, Red action: do-nothing, Blue reward:0.85\n", + "step: 2607, Red action: do-nothing, Blue reward:0.85\n", + "step: 2608, Red action: do-nothing, Blue reward:0.85\n", + "step: 2609, Red action: do-nothing, Blue reward:0.85\n", + "step: 2610, Red action: do-nothing, Blue reward:0.85\n", + "step: 2611, Red action: do-nothing, Blue reward:0.85\n", + "step: 2612, Red action: do-nothing, Blue reward:0.85\n", + "step: 2613, Red action: do-nothing, Blue reward:0.85\n", + "step: 2614, Red action: do-nothing, Blue reward:0.85\n", + "step: 2615, Red action: do-nothing, Blue reward:0.85\n", + "step: 2616, Red action: do-nothing, Blue reward:0.85\n", + "step: 2617, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2618, Red action: do-nothing, Blue reward:0.85\n", + "step: 2619, Red action: do-nothing, Blue reward:0.85\n", + "step: 2620, Red action: do-nothing, Blue reward:0.85\n", + "step: 2621, Red action: do-nothing, Blue reward:0.85\n", + "step: 2622, Red action: do-nothing, Blue reward:0.85\n", + "step: 2623, Red action: do-nothing, Blue reward:0.85\n", + "step: 2624, Red action: do-nothing, Blue reward:0.85\n", + "step: 2625, Red action: do-nothing, Blue reward:0.85\n", + "step: 2626, Red action: do-nothing, Blue reward:0.85\n", + "step: 2627, Red action: do-nothing, Blue reward:0.85\n", + "step: 2628, Red action: do-nothing, Blue reward:0.85\n", + "step: 2629, Red action: do-nothing, Blue reward:0.85\n", + "step: 2630, Red action: do-nothing, Blue reward:0.85\n", + "step: 2631, Red action: do-nothing, Blue reward:0.85\n", + "step: 2632, Red action: do-nothing, Blue reward:0.85\n", + "step: 2633, Red action: do-nothing, Blue reward:0.85\n", + "step: 2634, Red action: do-nothing, Blue reward:0.85\n", + "step: 2635, Red action: do-nothing, Blue reward:0.85\n", + "step: 2636, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2637, Red action: do-nothing, Blue reward:0.85\n", + "step: 2638, Red action: do-nothing, Blue reward:0.85\n", + "step: 2639, Red action: do-nothing, Blue reward:0.85\n", + "step: 2640, Red action: do-nothing, Blue reward:0.85\n", + "step: 2641, Red action: do-nothing, Blue reward:0.85\n", + "step: 2642, Red action: do-nothing, Blue reward:0.85\n", + "step: 2643, Red action: do-nothing, Blue reward:0.85\n", + "step: 2644, Red action: do-nothing, Blue reward:0.85\n", + "step: 2645, Red action: do-nothing, Blue reward:0.85\n", + "step: 2646, Red action: do-nothing, Blue reward:0.85\n", + "step: 2647, Red action: do-nothing, Blue reward:0.85\n", + "step: 2648, Red action: do-nothing, Blue reward:0.85\n", + "step: 2649, Red action: do-nothing, Blue reward:0.85\n", + "step: 2650, Red action: do-nothing, Blue reward:0.85\n", + "step: 2651, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2652, Red action: do-nothing, Blue reward:0.85\n", + "step: 2653, Red action: do-nothing, Blue reward:0.85\n", + "step: 2654, Red action: do-nothing, Blue reward:0.85\n", + "step: 2655, Red action: do-nothing, Blue reward:0.85\n", + "step: 2656, Red action: do-nothing, Blue reward:0.85\n", + "step: 2657, Red action: do-nothing, Blue reward:0.85\n", + "step: 2658, Red action: do-nothing, Blue reward:0.85\n", + "step: 2659, Red action: do-nothing, Blue reward:0.85\n", + "step: 2660, Red action: do-nothing, Blue reward:0.85\n", + "step: 2661, Red action: do-nothing, Blue reward:0.85\n", + "step: 2662, Red action: do-nothing, Blue reward:0.85\n", + "step: 2663, Red action: do-nothing, Blue reward:0.85\n", + "step: 2664, Red action: do-nothing, Blue reward:0.85\n", + "step: 2665, Red action: do-nothing, Blue reward:0.85\n", + "step: 2666, Red action: do-nothing, Blue reward:0.85\n", + "step: 2667, Red action: do-nothing, Blue reward:0.85\n", + "step: 2668, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2669, Red action: do-nothing, Blue reward:0.85\n", + "step: 2670, Red action: do-nothing, Blue reward:0.85\n", + "step: 2671, Red action: do-nothing, Blue reward:0.85\n", + "step: 2672, Red action: do-nothing, Blue reward:0.85\n", + "step: 2673, Red action: do-nothing, Blue reward:0.85\n", + "step: 2674, Red action: do-nothing, Blue reward:0.85\n", + "step: 2675, Red action: do-nothing, Blue reward:0.85\n", + "step: 2676, Red action: do-nothing, Blue reward:0.85\n", + "step: 2677, Red action: do-nothing, Blue reward:0.85\n", + "step: 2678, Red action: do-nothing, Blue reward:0.85\n", + "step: 2679, Red action: do-nothing, Blue reward:0.85\n", + "step: 2680, Red action: do-nothing, Blue reward:0.85\n", + "step: 2681, Red action: do-nothing, Blue reward:0.85\n", + "step: 2682, Red action: do-nothing, Blue reward:0.85\n", + "step: 2683, Red action: do-nothing, Blue reward:0.85\n", + "step: 2684, Red action: do-nothing, Blue reward:0.85\n", + "step: 2685, Red action: do-nothing, Blue reward:0.85\n", + "step: 2686, Red action: do-nothing, Blue reward:0.85\n", + "step: 2687, Red action: do-nothing, Blue reward:0.85\n", + "step: 2688, Red action: do-nothing, Blue reward:0.85\n", + "step: 2689, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2690, Red action: do-nothing, Blue reward:0.85\n", + "step: 2691, Red action: do-nothing, Blue reward:0.85\n", + "step: 2692, Red action: do-nothing, Blue reward:0.85\n", + "step: 2693, Red action: do-nothing, Blue reward:0.85\n", + "step: 2694, Red action: do-nothing, Blue reward:0.85\n", + "step: 2695, Red action: do-nothing, Blue reward:0.85\n", + "step: 2696, Red action: do-nothing, Blue reward:0.85\n", + "step: 2697, Red action: do-nothing, Blue reward:0.85\n", + "step: 2698, Red action: do-nothing, Blue reward:0.85\n", + "step: 2699, Red action: do-nothing, Blue reward:0.85\n", + "step: 2700, Red action: do-nothing, Blue reward:0.85\n", + "step: 2701, Red action: do-nothing, Blue reward:0.85\n", + "step: 2702, Red action: do-nothing, Blue reward:0.85\n", + "step: 2703, Red action: do-nothing, Blue reward:0.85\n", + "step: 2704, Red action: do-nothing, Blue reward:0.85\n", + "step: 2705, Red action: do-nothing, Blue reward:0.85\n", + "step: 2706, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2707, Red action: do-nothing, Blue reward:0.85\n", + "step: 2708, Red action: do-nothing, Blue reward:0.85\n", + "step: 2709, Red action: do-nothing, Blue reward:0.85\n", + "step: 2710, Red action: do-nothing, Blue reward:0.85\n", + "step: 2711, Red action: do-nothing, Blue reward:0.85\n", + "step: 2712, Red action: do-nothing, Blue reward:0.85\n", + "step: 2713, Red action: do-nothing, Blue reward:0.85\n", + "step: 2714, Red action: do-nothing, Blue reward:0.85\n", + "step: 2715, Red action: do-nothing, Blue reward:0.85\n", + "step: 2716, Red action: do-nothing, Blue reward:0.85\n", + "step: 2717, Red action: do-nothing, Blue reward:0.85\n", + "step: 2718, Red action: do-nothing, Blue reward:0.85\n", + "step: 2719, Red action: do-nothing, Blue reward:0.85\n", + "step: 2720, Red action: do-nothing, Blue reward:0.85\n", + "step: 2721, Red action: do-nothing, Blue reward:0.85\n", + "step: 2722, Red action: do-nothing, Blue reward:0.85\n", + "step: 2723, Red action: do-nothing, Blue reward:0.85\n", + "step: 2724, Red action: do-nothing, Blue reward:0.85\n", + "step: 2725, Red action: do-nothing, Blue reward:0.85\n", + "step: 2726, Red action: do-nothing, Blue reward:0.85\n", + "step: 2727, Red action: do-nothing, Blue reward:0.85\n", + "step: 2728, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2729, Red action: do-nothing, Blue reward:0.85\n", + "step: 2730, Red action: do-nothing, Blue reward:0.85\n", + "step: 2731, Red action: do-nothing, Blue reward:0.85\n", + "step: 2732, Red action: do-nothing, Blue reward:0.85\n", + "step: 2733, Red action: do-nothing, Blue reward:0.85\n", + "step: 2734, Red action: do-nothing, Blue reward:0.85\n", + "step: 2735, Red action: do-nothing, Blue reward:0.85\n", + "step: 2736, Red action: do-nothing, Blue reward:0.85\n", + "step: 2737, Red action: do-nothing, Blue reward:0.85\n", + "step: 2738, Red action: do-nothing, Blue reward:0.85\n", + "step: 2739, Red action: do-nothing, Blue reward:0.85\n", + "step: 2740, Red action: do-nothing, Blue reward:0.85\n", + "step: 2741, Red action: do-nothing, Blue reward:0.85\n", + "step: 2742, Red action: do-nothing, Blue reward:0.85\n", + "step: 2743, Red action: do-nothing, Blue reward:0.85\n", + "step: 2744, Red action: do-nothing, Blue reward:0.85\n", + "step: 2745, Red action: do-nothing, Blue reward:0.85\n", + "step: 2746, Red action: do-nothing, Blue reward:0.85\n", + "step: 2747, Red action: do-nothing, Blue reward:0.85\n", + "step: 2748, Red action: do-nothing, Blue reward:0.85\n", + "step: 2749, Red action: do-nothing, Blue reward:0.85\n", + "step: 2750, Red action: do-nothing, Blue reward:0.85\n", + "step: 2751, Red action: do-nothing, Blue reward:0.85\n", + "step: 2752, Red action: do-nothing, Blue reward:0.85\n", + "step: 2753, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2754, Red action: do-nothing, Blue reward:0.85\n", + "step: 2755, Red action: do-nothing, Blue reward:0.85\n", + "step: 2756, Red action: do-nothing, Blue reward:0.85\n", + "step: 2757, Red action: do-nothing, Blue reward:0.85\n", + "step: 2758, Red action: do-nothing, Blue reward:0.85\n", + "step: 2759, Red action: do-nothing, Blue reward:0.85\n", + "step: 2760, Red action: do-nothing, Blue reward:0.85\n", + "step: 2761, Red action: do-nothing, Blue reward:0.85\n", + "step: 2762, Red action: do-nothing, Blue reward:0.85\n", + "step: 2763, Red action: do-nothing, Blue reward:0.85\n", + "step: 2764, Red action: do-nothing, Blue reward:0.85\n", + "step: 2765, Red action: do-nothing, Blue reward:0.85\n", + "step: 2766, Red action: do-nothing, Blue reward:0.85\n", + "step: 2767, Red action: do-nothing, Blue reward:0.85\n", + "step: 2768, Red action: do-nothing, Blue reward:0.85\n", + "step: 2769, Red action: do-nothing, Blue reward:0.85\n", + "step: 2770, Red action: do-nothing, Blue reward:0.85\n", + "step: 2771, Red action: do-nothing, Blue reward:0.85\n", + "step: 2772, Red action: do-nothing, Blue reward:0.85\n", + "step: 2773, Red action: do-nothing, Blue reward:0.85\n", + "step: 2774, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2775, Red action: do-nothing, Blue reward:0.85\n", + "step: 2776, Red action: do-nothing, Blue reward:0.85\n", + "step: 2777, Red action: do-nothing, Blue reward:0.85\n", + "step: 2778, Red action: do-nothing, Blue reward:0.85\n", + "step: 2779, Red action: do-nothing, Blue reward:0.85\n", + "step: 2780, Red action: do-nothing, Blue reward:0.85\n", + "step: 2781, Red action: do-nothing, Blue reward:0.85\n", + "step: 2782, Red action: do-nothing, Blue reward:0.85\n", + "step: 2783, Red action: do-nothing, Blue reward:0.85\n", + "step: 2784, Red action: do-nothing, Blue reward:0.85\n", + "step: 2785, Red action: do-nothing, Blue reward:0.85\n", + "step: 2786, Red action: do-nothing, Blue reward:0.85\n", + "step: 2787, Red action: do-nothing, Blue reward:0.85\n", + "step: 2788, Red action: do-nothing, Blue reward:0.85\n", + "step: 2789, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2790, Red action: do-nothing, Blue reward:0.85\n", + "step: 2791, Red action: do-nothing, Blue reward:0.85\n", + "step: 2792, Red action: do-nothing, Blue reward:0.85\n", + "step: 2793, Red action: do-nothing, Blue reward:0.85\n", + "step: 2794, Red action: do-nothing, Blue reward:0.85\n", + "step: 2795, Red action: do-nothing, Blue reward:0.85\n", + "step: 2796, Red action: do-nothing, Blue reward:0.85\n", + "step: 2797, Red action: do-nothing, Blue reward:0.85\n", + "step: 2798, Red action: do-nothing, Blue reward:0.85\n", + "step: 2799, Red action: do-nothing, Blue reward:0.85\n", + "step: 2800, Red action: do-nothing, Blue reward:0.85\n", + "step: 2801, Red action: do-nothing, Blue reward:0.85\n", + "step: 2802, Red action: do-nothing, Blue reward:0.85\n", + "step: 2803, Red action: do-nothing, Blue reward:0.85\n", + "step: 2804, Red action: do-nothing, Blue reward:0.85\n", + "step: 2805, Red action: do-nothing, Blue reward:0.85\n", + "step: 2806, Red action: do-nothing, Blue reward:0.85\n", + "step: 2807, Red action: do-nothing, Blue reward:0.85\n", + "step: 2808, Red action: do-nothing, Blue reward:0.85\n", + "step: 2809, Red action: do-nothing, Blue reward:0.85\n", + "step: 2810, Red action: do-nothing, Blue reward:0.85\n", + "step: 2811, Red action: do-nothing, Blue reward:0.85\n", + "step: 2812, Red action: do-nothing, Blue reward:0.85\n", + "step: 2813, Red action: do-nothing, Blue reward:0.85\n", + "step: 2814, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2815, Red action: do-nothing, Blue reward:0.85\n", + "step: 2816, Red action: do-nothing, Blue reward:0.85\n", + "step: 2817, Red action: do-nothing, Blue reward:0.85\n", + "step: 2818, Red action: do-nothing, Blue reward:0.85\n", + "step: 2819, Red action: do-nothing, Blue reward:0.85\n", + "step: 2820, Red action: do-nothing, Blue reward:0.85\n", + "step: 2821, Red action: do-nothing, Blue reward:0.85\n", + "step: 2822, Red action: do-nothing, Blue reward:0.85\n", + "step: 2823, Red action: do-nothing, Blue reward:0.85\n", + "step: 2824, Red action: do-nothing, Blue reward:0.85\n", + "step: 2825, Red action: do-nothing, Blue reward:0.85\n", + "step: 2826, Red action: do-nothing, Blue reward:0.85\n", + "step: 2827, Red action: do-nothing, Blue reward:0.85\n", + "step: 2828, Red action: do-nothing, Blue reward:0.85\n", + "step: 2829, Red action: do-nothing, Blue reward:0.85\n", + "step: 2830, Red action: do-nothing, Blue reward:0.85\n", + "step: 2831, Red action: do-nothing, Blue reward:0.85\n", + "step: 2832, Red action: do-nothing, Blue reward:0.85\n", + "step: 2833, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2834, Red action: do-nothing, Blue reward:0.85\n", + "step: 2835, Red action: do-nothing, Blue reward:0.85\n", + "step: 2836, Red action: do-nothing, Blue reward:0.85\n", + "step: 2837, Red action: do-nothing, Blue reward:0.85\n", + "step: 2838, Red action: do-nothing, Blue reward:0.85\n", + "step: 2839, Red action: do-nothing, Blue reward:0.85\n", + "step: 2840, Red action: do-nothing, Blue reward:0.85\n", + "step: 2841, Red action: do-nothing, Blue reward:0.85\n", + "step: 2842, Red action: do-nothing, Blue reward:0.85\n", + "step: 2843, Red action: do-nothing, Blue reward:0.85\n", + "step: 2844, Red action: do-nothing, Blue reward:0.85\n", + "step: 2845, Red action: do-nothing, Blue reward:0.85\n", + "step: 2846, Red action: do-nothing, Blue reward:0.85\n", + "step: 2847, Red action: do-nothing, Blue reward:0.85\n", + "step: 2848, Red action: do-nothing, Blue reward:0.85\n", + "step: 2849, Red action: do-nothing, Blue reward:0.85\n", + "step: 2850, Red action: do-nothing, Blue reward:0.85\n", + "step: 2851, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2852, Red action: do-nothing, Blue reward:0.85\n", + "step: 2853, Red action: do-nothing, Blue reward:0.85\n", + "step: 2854, Red action: do-nothing, Blue reward:0.85\n", + "step: 2855, Red action: do-nothing, Blue reward:0.85\n", + "step: 2856, Red action: do-nothing, Blue reward:0.85\n", + "step: 2857, Red action: do-nothing, Blue reward:0.85\n", + "step: 2858, Red action: do-nothing, Blue reward:0.85\n", + "step: 2859, Red action: do-nothing, Blue reward:0.85\n", + "step: 2860, Red action: do-nothing, Blue reward:0.85\n", + "step: 2861, Red action: do-nothing, Blue reward:0.85\n", + "step: 2862, Red action: do-nothing, Blue reward:0.85\n", + "step: 2863, Red action: do-nothing, Blue reward:0.85\n", + "step: 2864, Red action: do-nothing, Blue reward:0.85\n", + "step: 2865, Red action: do-nothing, Blue reward:0.85\n", + "step: 2866, Red action: do-nothing, Blue reward:0.85\n", + "step: 2867, Red action: do-nothing, Blue reward:0.85\n", + "step: 2868, Red action: do-nothing, Blue reward:0.85\n", + "step: 2869, Red action: do-nothing, Blue reward:0.85\n", + "step: 2870, Red action: do-nothing, Blue reward:0.85\n", + "step: 2871, Red action: do-nothing, Blue reward:0.85\n", + "step: 2872, Red action: do-nothing, Blue reward:0.85\n", + "step: 2873, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2874, Red action: do-nothing, Blue reward:0.85\n", + "step: 2875, Red action: do-nothing, Blue reward:0.85\n", + "step: 2876, Red action: do-nothing, Blue reward:0.85\n", + "step: 2877, Red action: do-nothing, Blue reward:0.85\n", + "step: 2878, Red action: do-nothing, Blue reward:0.85\n", + "step: 2879, Red action: do-nothing, Blue reward:0.85\n", + "step: 2880, Red action: do-nothing, Blue reward:0.85\n", + "step: 2881, Red action: do-nothing, Blue reward:0.85\n", + "step: 2882, Red action: do-nothing, Blue reward:0.85\n", + "step: 2883, Red action: do-nothing, Blue reward:0.85\n", + "step: 2884, Red action: do-nothing, Blue reward:0.85\n", + "step: 2885, Red action: do-nothing, Blue reward:0.85\n", + "step: 2886, Red action: do-nothing, Blue reward:0.85\n", + "step: 2887, Red action: do-nothing, Blue reward:0.85\n", + "step: 2888, Red action: do-nothing, Blue reward:0.85\n", + "step: 2889, Red action: do-nothing, Blue reward:0.85\n", + "step: 2890, Red action: do-nothing, Blue reward:0.85\n", + "step: 2891, Red action: do-nothing, Blue reward:0.85\n", + "step: 2892, Red action: do-nothing, Blue reward:0.85\n", + "step: 2893, Red action: do-nothing, Blue reward:0.85\n", + "step: 2894, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2895, Red action: do-nothing, Blue reward:0.85\n", + "step: 2896, Red action: do-nothing, Blue reward:0.85\n", + "step: 2897, Red action: do-nothing, Blue reward:0.85\n", + "step: 2898, Red action: do-nothing, Blue reward:0.85\n", + "step: 2899, Red action: do-nothing, Blue reward:0.85\n", + "step: 2900, Red action: do-nothing, Blue reward:0.85\n", + "step: 2901, Red action: do-nothing, Blue reward:0.85\n", + "step: 2902, Red action: do-nothing, Blue reward:0.85\n", + "step: 2903, Red action: do-nothing, Blue reward:0.85\n", + "step: 2904, Red action: do-nothing, Blue reward:0.85\n", + "step: 2905, Red action: do-nothing, Blue reward:0.85\n", + "step: 2906, Red action: do-nothing, Blue reward:0.85\n", + "step: 2907, Red action: do-nothing, Blue reward:0.85\n", + "step: 2908, Red action: do-nothing, Blue reward:0.85\n", + "step: 2909, Red action: do-nothing, Blue reward:0.85\n", + "step: 2910, Red action: do-nothing, Blue reward:0.85\n", + "step: 2911, Red action: do-nothing, Blue reward:0.85\n", + "step: 2912, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2913, Red action: do-nothing, Blue reward:0.85\n", + "step: 2914, Red action: do-nothing, Blue reward:0.85\n", + "step: 2915, Red action: do-nothing, Blue reward:0.85\n", + "step: 2916, Red action: do-nothing, Blue reward:0.85\n", + "step: 2917, Red action: do-nothing, Blue reward:0.85\n", + "step: 2918, Red action: do-nothing, Blue reward:0.85\n", + "step: 2919, Red action: do-nothing, Blue reward:0.85\n", + "step: 2920, Red action: do-nothing, Blue reward:0.85\n", + "step: 2921, Red action: do-nothing, Blue reward:0.85\n", + "step: 2922, Red action: do-nothing, Blue reward:0.85\n", + "step: 2923, Red action: do-nothing, Blue reward:0.85\n", + "step: 2924, Red action: do-nothing, Blue reward:0.85\n", + "step: 2925, Red action: do-nothing, Blue reward:0.85\n", + "step: 2926, Red action: do-nothing, Blue reward:0.85\n", + "step: 2927, Red action: do-nothing, Blue reward:0.85\n", + "step: 2928, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2929, Red action: do-nothing, Blue reward:0.85\n", + "step: 2930, Red action: do-nothing, Blue reward:0.85\n", + "step: 2931, Red action: do-nothing, Blue reward:0.85\n", + "step: 2932, Red action: do-nothing, Blue reward:0.85\n", + "step: 2933, Red action: do-nothing, Blue reward:0.85\n", + "step: 2934, Red action: do-nothing, Blue reward:0.85\n", + "step: 2935, Red action: do-nothing, Blue reward:0.85\n", + "step: 2936, Red action: do-nothing, Blue reward:0.85\n", + "step: 2937, Red action: do-nothing, Blue reward:0.85\n", + "step: 2938, Red action: do-nothing, Blue reward:0.85\n", + "step: 2939, Red action: do-nothing, Blue reward:0.85\n", + "step: 2940, Red action: do-nothing, Blue reward:0.85\n", + "step: 2941, Red action: do-nothing, Blue reward:0.85\n", + "step: 2942, Red action: do-nothing, Blue reward:0.85\n", + "step: 2943, Red action: do-nothing, Blue reward:0.85\n", + "step: 2944, Red action: do-nothing, Blue reward:0.85\n", + "step: 2945, Red action: do-nothing, Blue reward:0.85\n", + "step: 2946, Red action: do-nothing, Blue reward:0.85\n", + "step: 2947, Red action: do-nothing, Blue reward:0.85\n", + "step: 2948, Red action: do-nothing, Blue reward:0.85\n", + "step: 2949, Red action: do-nothing, Blue reward:0.85\n", + "step: 2950, Red action: do-nothing, Blue reward:0.85\n", + "step: 2951, Red action: do-nothing, Blue reward:0.85\n", + "step: 2952, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2953, Red action: do-nothing, Blue reward:0.85\n", + "step: 2954, Red action: do-nothing, Blue reward:0.85\n", + "step: 2955, Red action: do-nothing, Blue reward:0.85\n", + "step: 2956, Red action: do-nothing, Blue reward:0.85\n", + "step: 2957, Red action: do-nothing, Blue reward:0.85\n", + "step: 2958, Red action: do-nothing, Blue reward:0.85\n", + "step: 2959, Red action: do-nothing, Blue reward:0.85\n", + "step: 2960, Red action: do-nothing, Blue reward:0.85\n", + "step: 2961, Red action: do-nothing, Blue reward:0.85\n", + "step: 2962, Red action: do-nothing, Blue reward:0.85\n", + "step: 2963, Red action: do-nothing, Blue reward:0.85\n", + "step: 2964, Red action: do-nothing, Blue reward:0.85\n", + "step: 2965, Red action: do-nothing, Blue reward:0.85\n", + "step: 2966, Red action: do-nothing, Blue reward:0.85\n", + "step: 2967, Red action: do-nothing, Blue reward:0.85\n", + "step: 2968, Red action: do-nothing, Blue reward:0.85\n", + "step: 2969, Red action: do-nothing, Blue reward:0.85\n", + "step: 2970, Red action: do-nothing, Blue reward:0.85\n", + "step: 2971, Red action: do-nothing, Blue reward:0.85\n", + "step: 2972, Red action: do-nothing, Blue reward:0.85\n", + "step: 2973, Red action: do-nothing, Blue reward:0.85\n", + "step: 2974, Red action: do-nothing, Blue reward:0.85\n", + "step: 2975, Red action: do-nothing, Blue reward:0.85\n", + "step: 2976, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2977, Red action: do-nothing, Blue reward:0.85\n", + "step: 2978, Red action: do-nothing, Blue reward:0.85\n", + "step: 2979, Red action: do-nothing, Blue reward:0.85\n", + "step: 2980, Red action: do-nothing, Blue reward:0.85\n", + "step: 2981, Red action: do-nothing, Blue reward:0.85\n", + "step: 2982, Red action: do-nothing, Blue reward:0.85\n", + "step: 2983, Red action: do-nothing, Blue reward:0.85\n", + "step: 2984, Red action: do-nothing, Blue reward:0.85\n", + "step: 2985, Red action: do-nothing, Blue reward:0.85\n", + "step: 2986, Red action: do-nothing, Blue reward:0.85\n", + "step: 2987, Red action: do-nothing, Blue reward:0.85\n", + "step: 2988, Red action: do-nothing, Blue reward:0.85\n", + "step: 2989, Red action: do-nothing, Blue reward:0.85\n", + "step: 2990, Red action: do-nothing, Blue reward:0.85\n", + "step: 2991, Red action: do-nothing, Blue reward:0.85\n", + "step: 2992, Red action: do-nothing, Blue reward:0.85\n", + "step: 2993, Red action: do-nothing, Blue reward:0.85\n", + "step: 2994, Red action: do-nothing, Blue reward:0.85\n", + "step: 2995, Red action: do-nothing, Blue reward:0.85\n", + "step: 2996, Red action: do-nothing, Blue reward:0.85\n", + "step: 2997, Red action: node-application-execute, Blue reward:0.85\n", + "step: 2998, Red action: do-nothing, Blue reward:0.85\n", + "step: 2999, Red action: do-nothing, Blue reward:0.85\n", + "step: 3000, Red action: do-nothing, Blue reward:0.85\n", + "step: 3001, Red action: do-nothing, Blue reward:0.85\n", + "step: 3002, Red action: do-nothing, Blue reward:0.85\n", + "step: 3003, Red action: do-nothing, Blue reward:0.85\n", + "step: 3004, Red action: do-nothing, Blue reward:0.85\n", + "step: 3005, Red action: do-nothing, Blue reward:0.85\n", + "step: 3006, Red action: do-nothing, Blue reward:0.85\n", + "step: 3007, Red action: do-nothing, Blue reward:0.85\n", + "step: 3008, Red action: do-nothing, Blue reward:0.85\n", + "step: 3009, Red action: do-nothing, Blue reward:0.85\n", + "step: 3010, Red action: do-nothing, Blue reward:0.85\n", + "step: 3011, Red action: do-nothing, Blue reward:0.85\n", + "step: 3012, Red action: do-nothing, Blue reward:0.85\n", + "step: 3013, Red action: do-nothing, Blue reward:0.85\n", + "step: 3014, Red action: do-nothing, Blue reward:0.85\n", + "step: 3015, Red action: do-nothing, Blue reward:0.85\n", + "step: 3016, Red action: do-nothing, Blue reward:0.85\n", + "step: 3017, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3018, Red action: do-nothing, Blue reward:0.85\n", + "step: 3019, Red action: do-nothing, Blue reward:0.85\n", + "step: 3020, Red action: do-nothing, Blue reward:0.85\n", + "step: 3021, Red action: do-nothing, Blue reward:0.85\n", + "step: 3022, Red action: do-nothing, Blue reward:0.85\n", + "step: 3023, Red action: do-nothing, Blue reward:0.85\n", + "step: 3024, Red action: do-nothing, Blue reward:0.85\n", + "step: 3025, Red action: do-nothing, Blue reward:0.85\n", + "step: 3026, Red action: do-nothing, Blue reward:0.85\n", + "step: 3027, Red action: do-nothing, Blue reward:0.85\n", + "step: 3028, Red action: do-nothing, Blue reward:0.85\n", + "step: 3029, Red action: do-nothing, Blue reward:0.85\n", + "step: 3030, Red action: do-nothing, Blue reward:0.85\n", + "step: 3031, Red action: do-nothing, Blue reward:0.85\n", + "step: 3032, Red action: do-nothing, Blue reward:0.85\n", + "step: 3033, Red action: do-nothing, Blue reward:0.85\n", + "step: 3034, Red action: do-nothing, Blue reward:0.85\n", + "step: 3035, Red action: do-nothing, Blue reward:0.85\n", + "step: 3036, Red action: do-nothing, Blue reward:0.85\n", + "step: 3037, Red action: do-nothing, Blue reward:0.85\n", + "step: 3038, Red action: do-nothing, Blue reward:0.85\n", + "step: 3039, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3040, Red action: do-nothing, Blue reward:0.85\n", + "step: 3041, Red action: do-nothing, Blue reward:0.85\n", + "step: 3042, Red action: do-nothing, Blue reward:0.85\n", + "step: 3043, Red action: do-nothing, Blue reward:0.85\n", + "step: 3044, Red action: do-nothing, Blue reward:0.85\n", + "step: 3045, Red action: do-nothing, Blue reward:0.85\n", + "step: 3046, Red action: do-nothing, Blue reward:0.85\n", + "step: 3047, Red action: do-nothing, Blue reward:0.85\n", + "step: 3048, Red action: do-nothing, Blue reward:0.85\n", + "step: 3049, Red action: do-nothing, Blue reward:0.85\n", + "step: 3050, Red action: do-nothing, Blue reward:0.85\n", + "step: 3051, Red action: do-nothing, Blue reward:0.85\n", + "step: 3052, Red action: do-nothing, Blue reward:0.85\n", + "step: 3053, Red action: do-nothing, Blue reward:0.85\n", + "step: 3054, Red action: do-nothing, Blue reward:0.85\n", + "step: 3055, Red action: do-nothing, Blue reward:0.85\n", + "step: 3056, Red action: do-nothing, Blue reward:0.85\n", + "step: 3057, Red action: do-nothing, Blue reward:0.85\n", + "step: 3058, Red action: do-nothing, Blue reward:0.85\n", + "step: 3059, Red action: do-nothing, Blue reward:0.85\n", + "step: 3060, Red action: do-nothing, Blue reward:0.85\n", + "step: 3061, Red action: do-nothing, Blue reward:0.85\n", + "step: 3062, Red action: do-nothing, Blue reward:0.85\n", + "step: 3063, Red action: do-nothing, Blue reward:0.85\n", + "step: 3064, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3065, Red action: do-nothing, Blue reward:0.85\n", + "step: 3066, Red action: do-nothing, Blue reward:0.85\n", + "step: 3067, Red action: do-nothing, Blue reward:0.85\n", + "step: 3068, Red action: do-nothing, Blue reward:0.85\n", + "step: 3069, Red action: do-nothing, Blue reward:0.85\n", + "step: 3070, Red action: do-nothing, Blue reward:0.85\n", + "step: 3071, Red action: do-nothing, Blue reward:0.85\n", + "step: 3072, Red action: do-nothing, Blue reward:0.85\n", + "step: 3073, Red action: do-nothing, Blue reward:0.85\n", + "step: 3074, Red action: do-nothing, Blue reward:0.85\n", + "step: 3075, Red action: do-nothing, Blue reward:0.85\n", + "step: 3076, Red action: do-nothing, Blue reward:0.85\n", + "step: 3077, Red action: do-nothing, Blue reward:0.85\n", + "step: 3078, Red action: do-nothing, Blue reward:0.85\n", + "step: 3079, Red action: do-nothing, Blue reward:0.85\n", + "step: 3080, Red action: do-nothing, Blue reward:0.85\n", + "step: 3081, Red action: do-nothing, Blue reward:0.85\n", + "step: 3082, Red action: do-nothing, Blue reward:0.85\n", + "step: 3083, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3084, Red action: do-nothing, Blue reward:0.85\n", + "step: 3085, Red action: do-nothing, Blue reward:0.85\n", + "step: 3086, Red action: do-nothing, Blue reward:0.85\n", + "step: 3087, Red action: do-nothing, Blue reward:0.85\n", + "step: 3088, Red action: do-nothing, Blue reward:0.85\n", + "step: 3089, Red action: do-nothing, Blue reward:0.85\n", + "step: 3090, Red action: do-nothing, Blue reward:0.85\n", + "step: 3091, Red action: do-nothing, Blue reward:0.85\n", + "step: 3092, Red action: do-nothing, Blue reward:0.85\n", + "step: 3093, Red action: do-nothing, Blue reward:0.85\n", + "step: 3094, Red action: do-nothing, Blue reward:0.85\n", + "step: 3095, Red action: do-nothing, Blue reward:0.85\n", + "step: 3096, Red action: do-nothing, Blue reward:0.85\n", + "step: 3097, Red action: do-nothing, Blue reward:0.85\n", + "step: 3098, Red action: do-nothing, Blue reward:0.85\n", + "step: 3099, Red action: do-nothing, Blue reward:0.85\n", + "step: 3100, Red action: do-nothing, Blue reward:0.85\n", + "step: 3101, Red action: do-nothing, Blue reward:0.85\n", + "step: 3102, Red action: do-nothing, Blue reward:0.85\n", + "step: 3103, Red action: do-nothing, Blue reward:0.85\n", + "step: 3104, Red action: do-nothing, Blue reward:0.85\n", + "step: 3105, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3106, Red action: do-nothing, Blue reward:0.85\n", + "step: 3107, Red action: do-nothing, Blue reward:0.85\n", + "step: 3108, Red action: do-nothing, Blue reward:0.85\n", + "step: 3109, Red action: do-nothing, Blue reward:0.85\n", + "step: 3110, Red action: do-nothing, Blue reward:0.85\n", + "step: 3111, Red action: do-nothing, Blue reward:0.85\n", + "step: 3112, Red action: do-nothing, Blue reward:0.85\n", + "step: 3113, Red action: do-nothing, Blue reward:0.85\n", + "step: 3114, Red action: do-nothing, Blue reward:0.85\n", + "step: 3115, Red action: do-nothing, Blue reward:0.85\n", + "step: 3116, Red action: do-nothing, Blue reward:0.85\n", + "step: 3117, Red action: do-nothing, Blue reward:0.85\n", + "step: 3118, Red action: do-nothing, Blue reward:0.85\n", + "step: 3119, Red action: do-nothing, Blue reward:0.85\n", + "step: 3120, Red action: do-nothing, Blue reward:0.85\n", + "step: 3121, Red action: do-nothing, Blue reward:0.85\n", + "step: 3122, Red action: do-nothing, Blue reward:0.85\n", + "step: 3123, Red action: do-nothing, Blue reward:0.85\n", + "step: 3124, Red action: do-nothing, Blue reward:0.85\n", + "step: 3125, Red action: do-nothing, Blue reward:0.85\n", + "step: 3126, Red action: do-nothing, Blue reward:0.85\n", + "step: 3127, Red action: do-nothing, Blue reward:0.85\n", + "step: 3128, Red action: do-nothing, Blue reward:0.85\n", + "step: 3129, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3130, Red action: do-nothing, Blue reward:0.85\n", + "step: 3131, Red action: do-nothing, Blue reward:0.85\n", + "step: 3132, Red action: do-nothing, Blue reward:0.85\n", + "step: 3133, Red action: do-nothing, Blue reward:0.85\n", + "step: 3134, Red action: do-nothing, Blue reward:0.85\n", + "step: 3135, Red action: do-nothing, Blue reward:0.85\n", + "step: 3136, Red action: do-nothing, Blue reward:0.85\n", + "step: 3137, Red action: do-nothing, Blue reward:0.85\n", + "step: 3138, Red action: do-nothing, Blue reward:0.85\n", + "step: 3139, Red action: do-nothing, Blue reward:0.85\n", + "step: 3140, Red action: do-nothing, Blue reward:0.85\n", + "step: 3141, Red action: do-nothing, Blue reward:0.85\n", + "step: 3142, Red action: do-nothing, Blue reward:0.85\n", + "step: 3143, Red action: do-nothing, Blue reward:0.85\n", + "step: 3144, Red action: do-nothing, Blue reward:0.85\n", + "step: 3145, Red action: do-nothing, Blue reward:0.85\n", + "step: 3146, Red action: do-nothing, Blue reward:0.85\n", + "step: 3147, Red action: do-nothing, Blue reward:0.85\n", + "step: 3148, Red action: do-nothing, Blue reward:0.85\n", + "step: 3149, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3150, Red action: do-nothing, Blue reward:0.85\n", + "step: 3151, Red action: do-nothing, Blue reward:0.85\n", + "step: 3152, Red action: do-nothing, Blue reward:0.85\n", + "step: 3153, Red action: do-nothing, Blue reward:0.85\n", + "step: 3154, Red action: do-nothing, Blue reward:0.85\n", + "step: 3155, Red action: do-nothing, Blue reward:0.85\n", + "step: 3156, Red action: do-nothing, Blue reward:0.85\n", + "step: 3157, Red action: do-nothing, Blue reward:0.85\n", + "step: 3158, Red action: do-nothing, Blue reward:0.85\n", + "step: 3159, Red action: do-nothing, Blue reward:0.85\n", + "step: 3160, Red action: do-nothing, Blue reward:0.85\n", + "step: 3161, Red action: do-nothing, Blue reward:0.85\n", + "step: 3162, Red action: do-nothing, Blue reward:0.85\n", + "step: 3163, Red action: do-nothing, Blue reward:0.85\n", + "step: 3164, Red action: do-nothing, Blue reward:0.85\n", + "step: 3165, Red action: do-nothing, Blue reward:0.85\n", + "step: 3166, Red action: do-nothing, Blue reward:0.85\n", + "step: 3167, Red action: do-nothing, Blue reward:0.85\n", + "step: 3168, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3169, Red action: do-nothing, Blue reward:0.85\n", + "step: 3170, Red action: do-nothing, Blue reward:0.85\n", + "step: 3171, Red action: do-nothing, Blue reward:0.85\n", + "step: 3172, Red action: do-nothing, Blue reward:0.85\n", + "step: 3173, Red action: do-nothing, Blue reward:0.85\n", + "step: 3174, Red action: do-nothing, Blue reward:0.85\n", + "step: 3175, Red action: do-nothing, Blue reward:0.85\n", + "step: 3176, Red action: do-nothing, Blue reward:0.85\n", + "step: 3177, Red action: do-nothing, Blue reward:0.85\n", + "step: 3178, Red action: do-nothing, Blue reward:0.85\n", + "step: 3179, Red action: do-nothing, Blue reward:0.85\n", + "step: 3180, Red action: do-nothing, Blue reward:0.85\n", + "step: 3181, Red action: do-nothing, Blue reward:0.85\n", + "step: 3182, Red action: do-nothing, Blue reward:0.85\n", + "step: 3183, Red action: do-nothing, Blue reward:0.85\n", + "step: 3184, Red action: do-nothing, Blue reward:0.85\n", + "step: 3185, Red action: do-nothing, Blue reward:0.85\n", + "step: 3186, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3187, Red action: do-nothing, Blue reward:0.85\n", + "step: 3188, Red action: do-nothing, Blue reward:0.85\n", + "step: 3189, Red action: do-nothing, Blue reward:0.85\n", + "step: 3190, Red action: do-nothing, Blue reward:0.85\n", + "step: 3191, Red action: do-nothing, Blue reward:0.85\n", + "step: 3192, Red action: do-nothing, Blue reward:0.85\n", + "step: 3193, Red action: do-nothing, Blue reward:0.85\n", + "step: 3194, Red action: do-nothing, Blue reward:0.85\n", + "step: 3195, Red action: do-nothing, Blue reward:0.85\n", + "step: 3196, Red action: do-nothing, Blue reward:0.85\n", + "step: 3197, Red action: do-nothing, Blue reward:0.85\n", + "step: 3198, Red action: do-nothing, Blue reward:0.85\n", + "step: 3199, Red action: do-nothing, Blue reward:0.85\n", + "step: 3200, Red action: do-nothing, Blue reward:0.85\n", + "step: 3201, Red action: do-nothing, Blue reward:0.85\n", + "step: 3202, Red action: do-nothing, Blue reward:0.85\n", + "step: 3203, Red action: do-nothing, Blue reward:0.85\n", + "step: 3204, Red action: do-nothing, Blue reward:0.85\n", + "step: 3205, Red action: do-nothing, Blue reward:0.85\n", + "step: 3206, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3207, Red action: do-nothing, Blue reward:0.85\n", + "step: 3208, Red action: do-nothing, Blue reward:0.85\n", + "step: 3209, Red action: do-nothing, Blue reward:0.85\n", + "step: 3210, Red action: do-nothing, Blue reward:0.85\n", + "step: 3211, Red action: do-nothing, Blue reward:0.85\n", + "step: 3212, Red action: do-nothing, Blue reward:0.85\n", + "step: 3213, Red action: do-nothing, Blue reward:0.85\n", + "step: 3214, Red action: do-nothing, Blue reward:0.85\n", + "step: 3215, Red action: do-nothing, Blue reward:0.85\n", + "step: 3216, Red action: do-nothing, Blue reward:0.85\n", + "step: 3217, Red action: do-nothing, Blue reward:0.85\n", + "step: 3218, Red action: do-nothing, Blue reward:0.85\n", + "step: 3219, Red action: do-nothing, Blue reward:0.85\n", + "step: 3220, Red action: do-nothing, Blue reward:0.85\n", + "step: 3221, Red action: do-nothing, Blue reward:0.85\n", + "step: 3222, Red action: do-nothing, Blue reward:0.85\n", + "step: 3223, Red action: do-nothing, Blue reward:0.85\n", + "step: 3224, Red action: do-nothing, Blue reward:0.85\n", + "step: 3225, Red action: do-nothing, Blue reward:0.85\n", + "step: 3226, Red action: do-nothing, Blue reward:0.85\n", + "step: 3227, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3228, Red action: do-nothing, Blue reward:0.85\n", + "step: 3229, Red action: do-nothing, Blue reward:0.85\n", + "step: 3230, Red action: do-nothing, Blue reward:0.85\n", + "step: 3231, Red action: do-nothing, Blue reward:0.85\n", + "step: 3232, Red action: do-nothing, Blue reward:0.85\n", + "step: 3233, Red action: do-nothing, Blue reward:0.85\n", + "step: 3234, Red action: do-nothing, Blue reward:0.85\n", + "step: 3235, Red action: do-nothing, Blue reward:0.85\n", + "step: 3236, Red action: do-nothing, Blue reward:0.85\n", + "step: 3237, Red action: do-nothing, Blue reward:0.85\n", + "step: 3238, Red action: do-nothing, Blue reward:0.85\n", + "step: 3239, Red action: do-nothing, Blue reward:0.85\n", + "step: 3240, Red action: do-nothing, Blue reward:0.85\n", + "step: 3241, Red action: do-nothing, Blue reward:0.85\n", + "step: 3242, Red action: do-nothing, Blue reward:0.85\n", + "step: 3243, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3244, Red action: do-nothing, Blue reward:0.85\n", + "step: 3245, Red action: do-nothing, Blue reward:0.85\n", + "step: 3246, Red action: do-nothing, Blue reward:0.85\n", + "step: 3247, Red action: do-nothing, Blue reward:0.85\n", + "step: 3248, Red action: do-nothing, Blue reward:0.85\n", + "step: 3249, Red action: do-nothing, Blue reward:0.85\n", + "step: 3250, Red action: do-nothing, Blue reward:0.85\n", + "step: 3251, Red action: do-nothing, Blue reward:0.85\n", + "step: 3252, Red action: do-nothing, Blue reward:0.85\n", + "step: 3253, Red action: do-nothing, Blue reward:0.85\n", + "step: 3254, Red action: do-nothing, Blue reward:0.85\n", + "step: 3255, Red action: do-nothing, Blue reward:0.85\n", + "step: 3256, Red action: do-nothing, Blue reward:0.85\n", + "step: 3257, Red action: do-nothing, Blue reward:0.85\n", + "step: 3258, Red action: do-nothing, Blue reward:0.85\n", + "step: 3259, Red action: do-nothing, Blue reward:0.85\n", + "step: 3260, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3261, Red action: do-nothing, Blue reward:0.85\n", + "step: 3262, Red action: do-nothing, Blue reward:0.85\n", + "step: 3263, Red action: do-nothing, Blue reward:0.85\n", + "step: 3264, Red action: do-nothing, Blue reward:0.85\n", + "step: 3265, Red action: do-nothing, Blue reward:0.85\n", + "step: 3266, Red action: do-nothing, Blue reward:0.85\n", + "step: 3267, Red action: do-nothing, Blue reward:0.85\n", + "step: 3268, Red action: do-nothing, Blue reward:0.85\n", + "step: 3269, Red action: do-nothing, Blue reward:0.85\n", + "step: 3270, Red action: do-nothing, Blue reward:0.85\n", + "step: 3271, Red action: do-nothing, Blue reward:0.85\n", + "step: 3272, Red action: do-nothing, Blue reward:0.85\n", + "step: 3273, Red action: do-nothing, Blue reward:0.85\n", + "step: 3274, Red action: do-nothing, Blue reward:0.85\n", + "step: 3275, Red action: do-nothing, Blue reward:0.85\n", + "step: 3276, Red action: do-nothing, Blue reward:0.85\n", + "step: 3277, Red action: do-nothing, Blue reward:0.85\n", + "step: 3278, Red action: do-nothing, Blue reward:0.85\n", + "step: 3279, Red action: do-nothing, Blue reward:0.85\n", + "step: 3280, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3281, Red action: do-nothing, Blue reward:0.85\n", + "step: 3282, Red action: do-nothing, Blue reward:0.85\n", + "step: 3283, Red action: do-nothing, Blue reward:0.85\n", + "step: 3284, Red action: do-nothing, Blue reward:0.85\n", + "step: 3285, Red action: do-nothing, Blue reward:0.85\n", + "step: 3286, Red action: do-nothing, Blue reward:0.85\n", + "step: 3287, Red action: do-nothing, Blue reward:0.85\n", + "step: 3288, Red action: do-nothing, Blue reward:0.85\n", + "step: 3289, Red action: do-nothing, Blue reward:0.85\n", + "step: 3290, Red action: do-nothing, Blue reward:0.85\n", + "step: 3291, Red action: do-nothing, Blue reward:0.85\n", + "step: 3292, Red action: do-nothing, Blue reward:0.85\n", + "step: 3293, Red action: do-nothing, Blue reward:0.85\n", + "step: 3294, Red action: do-nothing, Blue reward:0.85\n", + "step: 3295, Red action: do-nothing, Blue reward:0.85\n", + "step: 3296, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3297, Red action: do-nothing, Blue reward:0.85\n", + "step: 3298, Red action: do-nothing, Blue reward:0.85\n", + "step: 3299, Red action: do-nothing, Blue reward:0.85\n", + "step: 3300, Red action: do-nothing, Blue reward:0.85\n", + "step: 3301, Red action: do-nothing, Blue reward:0.85\n", + "step: 3302, Red action: do-nothing, Blue reward:0.85\n", + "step: 3303, Red action: do-nothing, Blue reward:0.85\n", + "step: 3304, Red action: do-nothing, Blue reward:0.85\n", + "step: 3305, Red action: do-nothing, Blue reward:0.85\n", + "step: 3306, Red action: do-nothing, Blue reward:0.85\n", + "step: 3307, Red action: do-nothing, Blue reward:0.85\n", + "step: 3308, Red action: do-nothing, Blue reward:0.85\n", + "step: 3309, Red action: do-nothing, Blue reward:0.85\n", + "step: 3310, Red action: do-nothing, Blue reward:0.85\n", + "step: 3311, Red action: do-nothing, Blue reward:0.85\n", + "step: 3312, Red action: do-nothing, Blue reward:0.85\n", + "step: 3313, Red action: do-nothing, Blue reward:0.85\n", + "step: 3314, Red action: do-nothing, Blue reward:0.85\n", + "step: 3315, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3316, Red action: do-nothing, Blue reward:0.85\n", + "step: 3317, Red action: do-nothing, Blue reward:0.85\n", + "step: 3318, Red action: do-nothing, Blue reward:0.85\n", + "step: 3319, Red action: do-nothing, Blue reward:0.85\n", + "step: 3320, Red action: do-nothing, Blue reward:0.85\n", + "step: 3321, Red action: do-nothing, Blue reward:0.85\n", + "step: 3322, Red action: do-nothing, Blue reward:0.85\n", + "step: 3323, Red action: do-nothing, Blue reward:0.85\n", + "step: 3324, Red action: do-nothing, Blue reward:0.85\n", + "step: 3325, Red action: do-nothing, Blue reward:0.85\n", + "step: 3326, Red action: do-nothing, Blue reward:0.85\n", + "step: 3327, Red action: do-nothing, Blue reward:0.85\n", + "step: 3328, Red action: do-nothing, Blue reward:0.85\n", + "step: 3329, Red action: do-nothing, Blue reward:0.85\n", + "step: 3330, Red action: do-nothing, Blue reward:0.85\n", + "step: 3331, Red action: do-nothing, Blue reward:0.85\n", + "step: 3332, Red action: do-nothing, Blue reward:0.85\n", + "step: 3333, Red action: do-nothing, Blue reward:0.85\n", + "step: 3334, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3335, Red action: do-nothing, Blue reward:0.85\n", + "step: 3336, Red action: do-nothing, Blue reward:0.85\n", + "step: 3337, Red action: do-nothing, Blue reward:0.85\n", + "step: 3338, Red action: do-nothing, Blue reward:0.85\n", + "step: 3339, Red action: do-nothing, Blue reward:0.85\n", + "step: 3340, Red action: do-nothing, Blue reward:0.85\n", + "step: 3341, Red action: do-nothing, Blue reward:0.85\n", + "step: 3342, Red action: do-nothing, Blue reward:0.85\n", + "step: 3343, Red action: do-nothing, Blue reward:0.85\n", + "step: 3344, Red action: do-nothing, Blue reward:0.85\n", + "step: 3345, Red action: do-nothing, Blue reward:0.85\n", + "step: 3346, Red action: do-nothing, Blue reward:0.85\n", + "step: 3347, Red action: do-nothing, Blue reward:0.85\n", + "step: 3348, Red action: do-nothing, Blue reward:0.85\n", + "step: 3349, Red action: do-nothing, Blue reward:0.85\n", + "step: 3350, Red action: do-nothing, Blue reward:0.85\n", + "step: 3351, Red action: do-nothing, Blue reward:0.85\n", + "step: 3352, Red action: do-nothing, Blue reward:0.85\n", + "step: 3353, Red action: do-nothing, Blue reward:0.85\n", + "step: 3354, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3355, Red action: do-nothing, Blue reward:0.85\n", + "step: 3356, Red action: do-nothing, Blue reward:0.85\n", + "step: 3357, Red action: do-nothing, Blue reward:0.85\n", + "step: 3358, Red action: do-nothing, Blue reward:0.85\n", + "step: 3359, Red action: do-nothing, Blue reward:0.85\n", + "step: 3360, Red action: do-nothing, Blue reward:0.85\n", + "step: 3361, Red action: do-nothing, Blue reward:0.85\n", + "step: 3362, Red action: do-nothing, Blue reward:0.85\n", + "step: 3363, Red action: do-nothing, Blue reward:0.85\n", + "step: 3364, Red action: do-nothing, Blue reward:0.85\n", + "step: 3365, Red action: do-nothing, Blue reward:0.85\n", + "step: 3366, Red action: do-nothing, Blue reward:0.85\n", + "step: 3367, Red action: do-nothing, Blue reward:0.85\n", + "step: 3368, Red action: do-nothing, Blue reward:0.85\n", + "step: 3369, Red action: do-nothing, Blue reward:0.85\n", + "step: 3370, Red action: do-nothing, Blue reward:0.85\n", + "step: 3371, Red action: do-nothing, Blue reward:0.85\n", + "step: 3372, Red action: do-nothing, Blue reward:0.85\n", + "step: 3373, Red action: do-nothing, Blue reward:0.85\n", + "step: 3374, Red action: do-nothing, Blue reward:0.85\n", + "step: 3375, Red action: do-nothing, Blue reward:0.85\n", + "step: 3376, Red action: do-nothing, Blue reward:0.85\n", + "step: 3377, Red action: do-nothing, Blue reward:0.85\n", + "step: 3378, Red action: do-nothing, Blue reward:0.85\n", + "step: 3379, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3380, Red action: do-nothing, Blue reward:0.85\n", + "step: 3381, Red action: do-nothing, Blue reward:0.85\n", + "step: 3382, Red action: do-nothing, Blue reward:0.85\n", + "step: 3383, Red action: do-nothing, Blue reward:0.85\n", + "step: 3384, Red action: do-nothing, Blue reward:0.85\n", + "step: 3385, Red action: do-nothing, Blue reward:0.85\n", + "step: 3386, Red action: do-nothing, Blue reward:0.85\n", + "step: 3387, Red action: do-nothing, Blue reward:0.85\n", + "step: 3388, Red action: do-nothing, Blue reward:0.85\n", + "step: 3389, Red action: do-nothing, Blue reward:0.85\n", + "step: 3390, Red action: do-nothing, Blue reward:0.85\n", + "step: 3391, Red action: do-nothing, Blue reward:0.85\n", + "step: 3392, Red action: do-nothing, Blue reward:0.85\n", + "step: 3393, Red action: do-nothing, Blue reward:0.85\n", + "step: 3394, Red action: do-nothing, Blue reward:0.85\n", + "step: 3395, Red action: do-nothing, Blue reward:0.85\n", + "step: 3396, Red action: do-nothing, Blue reward:0.85\n", + "step: 3397, Red action: do-nothing, Blue reward:0.85\n", + "step: 3398, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3399, Red action: do-nothing, Blue reward:0.85\n", + "step: 3400, Red action: do-nothing, Blue reward:0.85\n", + "step: 3401, Red action: do-nothing, Blue reward:0.85\n", + "step: 3402, Red action: do-nothing, Blue reward:0.85\n", + "step: 3403, Red action: do-nothing, Blue reward:0.85\n", + "step: 3404, Red action: do-nothing, Blue reward:0.85\n", + "step: 3405, Red action: do-nothing, Blue reward:0.85\n", + "step: 3406, Red action: do-nothing, Blue reward:0.85\n", + "step: 3407, Red action: do-nothing, Blue reward:0.85\n", + "step: 3408, Red action: do-nothing, Blue reward:0.85\n", + "step: 3409, Red action: do-nothing, Blue reward:0.85\n", + "step: 3410, Red action: do-nothing, Blue reward:0.85\n", + "step: 3411, Red action: do-nothing, Blue reward:0.85\n", + "step: 3412, Red action: do-nothing, Blue reward:0.85\n", + "step: 3413, Red action: do-nothing, Blue reward:0.85\n", + "step: 3414, Red action: do-nothing, Blue reward:0.85\n", + "step: 3415, Red action: do-nothing, Blue reward:0.85\n", + "step: 3416, Red action: do-nothing, Blue reward:0.85\n", + "step: 3417, Red action: do-nothing, Blue reward:0.85\n", + "step: 3418, Red action: do-nothing, Blue reward:0.85\n", + "step: 3419, Red action: do-nothing, Blue reward:0.85\n", + "step: 3420, Red action: do-nothing, Blue reward:0.85\n", + "step: 3421, Red action: do-nothing, Blue reward:0.85\n", + "step: 3422, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3423, Red action: do-nothing, Blue reward:0.85\n", + "step: 3424, Red action: do-nothing, Blue reward:0.85\n", + "step: 3425, Red action: do-nothing, Blue reward:0.85\n", + "step: 3426, Red action: do-nothing, Blue reward:0.85\n", + "step: 3427, Red action: do-nothing, Blue reward:0.85\n", + "step: 3428, Red action: do-nothing, Blue reward:0.85\n", + "step: 3429, Red action: do-nothing, Blue reward:0.85\n", + "step: 3430, Red action: do-nothing, Blue reward:0.85\n", + "step: 3431, Red action: do-nothing, Blue reward:0.85\n", + "step: 3432, Red action: do-nothing, Blue reward:0.85\n", + "step: 3433, Red action: do-nothing, Blue reward:0.85\n", + "step: 3434, Red action: do-nothing, Blue reward:0.85\n", + "step: 3435, Red action: do-nothing, Blue reward:0.85\n", + "step: 3436, Red action: do-nothing, Blue reward:0.85\n", + "step: 3437, Red action: do-nothing, Blue reward:0.85\n", + "step: 3438, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3439, Red action: do-nothing, Blue reward:0.85\n", + "step: 3440, Red action: do-nothing, Blue reward:0.85\n", + "step: 3441, Red action: do-nothing, Blue reward:0.85\n", + "step: 3442, Red action: do-nothing, Blue reward:0.85\n", + "step: 3443, Red action: do-nothing, Blue reward:0.85\n", + "step: 3444, Red action: do-nothing, Blue reward:0.85\n", + "step: 3445, Red action: do-nothing, Blue reward:0.85\n", + "step: 3446, Red action: do-nothing, Blue reward:0.85\n", + "step: 3447, Red action: do-nothing, Blue reward:0.85\n", + "step: 3448, Red action: do-nothing, Blue reward:0.85\n", + "step: 3449, Red action: do-nothing, Blue reward:0.85\n", + "step: 3450, Red action: do-nothing, Blue reward:0.85\n", + "step: 3451, Red action: do-nothing, Blue reward:0.85\n", + "step: 3452, Red action: do-nothing, Blue reward:0.85\n", + "step: 3453, Red action: do-nothing, Blue reward:0.85\n", + "step: 3454, Red action: do-nothing, Blue reward:0.85\n", + "step: 3455, Red action: do-nothing, Blue reward:0.85\n", + "step: 3456, Red action: do-nothing, Blue reward:0.85\n", + "step: 3457, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3458, Red action: do-nothing, Blue reward:0.85\n", + "step: 3459, Red action: do-nothing, Blue reward:0.85\n", + "step: 3460, Red action: do-nothing, Blue reward:0.85\n", + "step: 3461, Red action: do-nothing, Blue reward:0.85\n", + "step: 3462, Red action: do-nothing, Blue reward:0.85\n", + "step: 3463, Red action: do-nothing, Blue reward:0.85\n", + "step: 3464, Red action: do-nothing, Blue reward:0.85\n", + "step: 3465, Red action: do-nothing, Blue reward:0.85\n", + "step: 3466, Red action: do-nothing, Blue reward:0.85\n", + "step: 3467, Red action: do-nothing, Blue reward:0.85\n", + "step: 3468, Red action: do-nothing, Blue reward:0.85\n", + "step: 3469, Red action: do-nothing, Blue reward:0.85\n", + "step: 3470, Red action: do-nothing, Blue reward:0.85\n", + "step: 3471, Red action: do-nothing, Blue reward:0.85\n", + "step: 3472, Red action: do-nothing, Blue reward:0.85\n", + "step: 3473, Red action: do-nothing, Blue reward:0.85\n", + "step: 3474, Red action: do-nothing, Blue reward:0.85\n", + "step: 3475, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3476, Red action: do-nothing, Blue reward:0.85\n", + "step: 3477, Red action: do-nothing, Blue reward:0.85\n", + "step: 3478, Red action: do-nothing, Blue reward:0.85\n", + "step: 3479, Red action: do-nothing, Blue reward:0.85\n", + "step: 3480, Red action: do-nothing, Blue reward:0.85\n", + "step: 3481, Red action: do-nothing, Blue reward:0.85\n", + "step: 3482, Red action: do-nothing, Blue reward:0.85\n", + "step: 3483, Red action: do-nothing, Blue reward:0.85\n", + "step: 3484, Red action: do-nothing, Blue reward:0.85\n", + "step: 3485, Red action: do-nothing, Blue reward:0.85\n", + "step: 3486, Red action: do-nothing, Blue reward:0.85\n", + "step: 3487, Red action: do-nothing, Blue reward:0.85\n", + "step: 3488, Red action: do-nothing, Blue reward:0.85\n", + "step: 3489, Red action: do-nothing, Blue reward:0.85\n", + "step: 3490, Red action: do-nothing, Blue reward:0.85\n", + "step: 3491, Red action: do-nothing, Blue reward:0.85\n", + "step: 3492, Red action: do-nothing, Blue reward:0.85\n", + "step: 3493, Red action: do-nothing, Blue reward:0.85\n", + "step: 3494, Red action: do-nothing, Blue reward:0.85\n", + "step: 3495, Red action: do-nothing, Blue reward:0.85\n", + "step: 3496, Red action: do-nothing, Blue reward:0.85\n", + "step: 3497, Red action: do-nothing, Blue reward:0.85\n", + "step: 3498, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3499, Red action: do-nothing, Blue reward:0.85\n", + "step: 3500, Red action: do-nothing, Blue reward:0.85\n", + "step: 3501, Red action: do-nothing, Blue reward:0.85\n", + "step: 3502, Red action: do-nothing, Blue reward:0.85\n", + "step: 3503, Red action: do-nothing, Blue reward:0.85\n", + "step: 3504, Red action: do-nothing, Blue reward:0.85\n", + "step: 3505, Red action: do-nothing, Blue reward:0.85\n", + "step: 3506, Red action: do-nothing, Blue reward:0.85\n", + "step: 3507, Red action: do-nothing, Blue reward:0.85\n", + "step: 3508, Red action: do-nothing, Blue reward:0.85\n", + "step: 3509, Red action: do-nothing, Blue reward:0.85\n", + "step: 3510, Red action: do-nothing, Blue reward:0.85\n", + "step: 3511, Red action: do-nothing, Blue reward:0.85\n", + "step: 3512, Red action: do-nothing, Blue reward:0.85\n", + "step: 3513, Red action: do-nothing, Blue reward:0.85\n", + "step: 3514, Red action: do-nothing, Blue reward:0.85\n", + "step: 3515, Red action: do-nothing, Blue reward:0.85\n", + "step: 3516, Red action: do-nothing, Blue reward:0.85\n", + "step: 3517, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3518, Red action: do-nothing, Blue reward:0.85\n", + "step: 3519, Red action: do-nothing, Blue reward:0.85\n", + "step: 3520, Red action: do-nothing, Blue reward:0.85\n", + "step: 3521, Red action: do-nothing, Blue reward:0.85\n", + "step: 3522, Red action: do-nothing, Blue reward:0.85\n", + "step: 3523, Red action: do-nothing, Blue reward:0.85\n", + "step: 3524, Red action: do-nothing, Blue reward:0.85\n", + "step: 3525, Red action: do-nothing, Blue reward:0.85\n", + "step: 3526, Red action: do-nothing, Blue reward:0.85\n", + "step: 3527, Red action: do-nothing, Blue reward:0.85\n", + "step: 3528, Red action: do-nothing, Blue reward:0.85\n", + "step: 3529, Red action: do-nothing, Blue reward:0.85\n", + "step: 3530, Red action: do-nothing, Blue reward:0.85\n", + "step: 3531, Red action: do-nothing, Blue reward:0.85\n", + "step: 3532, Red action: do-nothing, Blue reward:0.85\n", + "step: 3533, Red action: do-nothing, Blue reward:0.85\n", + "step: 3534, Red action: do-nothing, Blue reward:0.85\n", + "step: 3535, Red action: do-nothing, Blue reward:0.85\n", + "step: 3536, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3537, Red action: do-nothing, Blue reward:0.85\n", + "step: 3538, Red action: do-nothing, Blue reward:0.85\n", + "step: 3539, Red action: do-nothing, Blue reward:0.85\n", + "step: 3540, Red action: do-nothing, Blue reward:0.85\n", + "step: 3541, Red action: do-nothing, Blue reward:0.85\n", + "step: 3542, Red action: do-nothing, Blue reward:0.85\n", + "step: 3543, Red action: do-nothing, Blue reward:0.85\n", + "step: 3544, Red action: do-nothing, Blue reward:0.85\n", + "step: 3545, Red action: do-nothing, Blue reward:0.85\n", + "step: 3546, Red action: do-nothing, Blue reward:0.85\n", + "step: 3547, Red action: do-nothing, Blue reward:0.85\n", + "step: 3548, Red action: do-nothing, Blue reward:0.85\n", + "step: 3549, Red action: do-nothing, Blue reward:0.85\n", + "step: 3550, Red action: do-nothing, Blue reward:0.85\n", + "step: 3551, Red action: do-nothing, Blue reward:0.85\n", + "step: 3552, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3553, Red action: do-nothing, Blue reward:0.85\n", + "step: 3554, Red action: do-nothing, Blue reward:0.85\n", + "step: 3555, Red action: do-nothing, Blue reward:0.85\n", + "step: 3556, Red action: do-nothing, Blue reward:0.85\n", + "step: 3557, Red action: do-nothing, Blue reward:0.85\n", + "step: 3558, Red action: do-nothing, Blue reward:0.85\n", + "step: 3559, Red action: do-nothing, Blue reward:0.85\n", + "step: 3560, Red action: do-nothing, Blue reward:0.85\n", + "step: 3561, Red action: do-nothing, Blue reward:0.85\n", + "step: 3562, Red action: do-nothing, Blue reward:0.85\n", + "step: 3563, Red action: do-nothing, Blue reward:0.85\n", + "step: 3564, Red action: do-nothing, Blue reward:0.85\n", + "step: 3565, Red action: do-nothing, Blue reward:0.85\n", + "step: 3566, Red action: do-nothing, Blue reward:0.85\n", + "step: 3567, Red action: do-nothing, Blue reward:0.85\n", + "step: 3568, Red action: do-nothing, Blue reward:0.85\n", + "step: 3569, Red action: do-nothing, Blue reward:0.85\n", + "step: 3570, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3571, Red action: do-nothing, Blue reward:0.85\n", + "step: 3572, Red action: do-nothing, Blue reward:0.85\n", + "step: 3573, Red action: do-nothing, Blue reward:0.85\n", + "step: 3574, Red action: do-nothing, Blue reward:0.85\n", + "step: 3575, Red action: do-nothing, Blue reward:0.85\n", + "step: 3576, Red action: do-nothing, Blue reward:0.85\n", + "step: 3577, Red action: do-nothing, Blue reward:0.85\n", + "step: 3578, Red action: do-nothing, Blue reward:0.85\n", + "step: 3579, Red action: do-nothing, Blue reward:0.85\n", + "step: 3580, Red action: do-nothing, Blue reward:0.85\n", + "step: 3581, Red action: do-nothing, Blue reward:0.85\n", + "step: 3582, Red action: do-nothing, Blue reward:0.85\n", + "step: 3583, Red action: do-nothing, Blue reward:0.85\n", + "step: 3584, Red action: do-nothing, Blue reward:0.85\n", + "step: 3585, Red action: do-nothing, Blue reward:0.85\n", + "step: 3586, Red action: do-nothing, Blue reward:0.85\n", + "step: 3587, Red action: do-nothing, Blue reward:0.85\n", + "step: 3588, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3589, Red action: do-nothing, Blue reward:0.85\n", + "step: 3590, Red action: do-nothing, Blue reward:0.85\n", + "step: 3591, Red action: do-nothing, Blue reward:0.85\n", + "step: 3592, Red action: do-nothing, Blue reward:0.85\n", + "step: 3593, Red action: do-nothing, Blue reward:0.85\n", + "step: 3594, Red action: do-nothing, Blue reward:0.85\n", + "step: 3595, Red action: do-nothing, Blue reward:0.85\n", + "step: 3596, Red action: do-nothing, Blue reward:0.85\n", + "step: 3597, Red action: do-nothing, Blue reward:0.85\n", + "step: 3598, Red action: do-nothing, Blue reward:0.85\n", + "step: 3599, Red action: do-nothing, Blue reward:0.85\n", + "step: 3600, Red action: do-nothing, Blue reward:0.85\n", + "step: 3601, Red action: do-nothing, Blue reward:0.85\n", + "step: 3602, Red action: do-nothing, Blue reward:0.85\n", + "step: 3603, Red action: do-nothing, Blue reward:0.85\n", + "step: 3604, Red action: do-nothing, Blue reward:0.85\n", + "step: 3605, Red action: do-nothing, Blue reward:0.85\n", + "step: 3606, Red action: do-nothing, Blue reward:0.85\n", + "step: 3607, Red action: do-nothing, Blue reward:0.85\n", + "step: 3608, Red action: do-nothing, Blue reward:0.85\n", + "step: 3609, Red action: do-nothing, Blue reward:0.85\n", + "step: 3610, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3611, Red action: do-nothing, Blue reward:0.85\n", + "step: 3612, Red action: do-nothing, Blue reward:0.85\n", + "step: 3613, Red action: do-nothing, Blue reward:0.85\n", + "step: 3614, Red action: do-nothing, Blue reward:0.85\n", + "step: 3615, Red action: do-nothing, Blue reward:0.85\n", + "step: 3616, Red action: do-nothing, Blue reward:0.85\n", + "step: 3617, Red action: do-nothing, Blue reward:0.85\n", + "step: 3618, Red action: do-nothing, Blue reward:0.85\n", + "step: 3619, Red action: do-nothing, Blue reward:0.85\n", + "step: 3620, Red action: do-nothing, Blue reward:0.85\n", + "step: 3621, Red action: do-nothing, Blue reward:0.85\n", + "step: 3622, Red action: do-nothing, Blue reward:0.85\n", + "step: 3623, Red action: do-nothing, Blue reward:0.85\n", + "step: 3624, Red action: do-nothing, Blue reward:0.85\n", + "step: 3625, Red action: do-nothing, Blue reward:0.85\n", + "step: 3626, Red action: do-nothing, Blue reward:0.85\n", + "step: 3627, Red action: do-nothing, Blue reward:0.85\n", + "step: 3628, Red action: do-nothing, Blue reward:0.85\n", + "step: 3629, Red action: do-nothing, Blue reward:0.85\n", + "step: 3630, Red action: do-nothing, Blue reward:0.85\n", + "step: 3631, Red action: do-nothing, Blue reward:0.85\n", + "step: 3632, Red action: do-nothing, Blue reward:0.85\n", + "step: 3633, Red action: do-nothing, Blue reward:0.85\n", + "step: 3634, Red action: do-nothing, Blue reward:0.85\n", + "step: 3635, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3636, Red action: do-nothing, Blue reward:0.85\n", + "step: 3637, Red action: do-nothing, Blue reward:0.85\n", + "step: 3638, Red action: do-nothing, Blue reward:0.85\n", + "step: 3639, Red action: do-nothing, Blue reward:0.85\n", + "step: 3640, Red action: do-nothing, Blue reward:0.85\n", + "step: 3641, Red action: do-nothing, Blue reward:0.85\n", + "step: 3642, Red action: do-nothing, Blue reward:0.85\n", + "step: 3643, Red action: do-nothing, Blue reward:0.85\n", + "step: 3644, Red action: do-nothing, Blue reward:0.85\n", + "step: 3645, Red action: do-nothing, Blue reward:0.85\n", + "step: 3646, Red action: do-nothing, Blue reward:0.85\n", + "step: 3647, Red action: do-nothing, Blue reward:0.85\n", + "step: 3648, Red action: do-nothing, Blue reward:0.85\n", + "step: 3649, Red action: do-nothing, Blue reward:0.85\n", + "step: 3650, Red action: do-nothing, Blue reward:0.85\n", + "step: 3651, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3652, Red action: do-nothing, Blue reward:0.85\n", + "step: 3653, Red action: do-nothing, Blue reward:0.85\n", + "step: 3654, Red action: do-nothing, Blue reward:0.85\n", + "step: 3655, Red action: do-nothing, Blue reward:0.85\n", + "step: 3656, Red action: do-nothing, Blue reward:0.85\n", + "step: 3657, Red action: do-nothing, Blue reward:0.85\n", + "step: 3658, Red action: do-nothing, Blue reward:0.85\n", + "step: 3659, Red action: do-nothing, Blue reward:0.85\n", + "step: 3660, Red action: do-nothing, Blue reward:0.85\n", + "step: 3661, Red action: do-nothing, Blue reward:0.85\n", + "step: 3662, Red action: do-nothing, Blue reward:0.85\n", + "step: 3663, Red action: do-nothing, Blue reward:0.85\n", + "step: 3664, Red action: do-nothing, Blue reward:0.85\n", + "step: 3665, Red action: do-nothing, Blue reward:0.85\n", + "step: 3666, Red action: do-nothing, Blue reward:0.85\n", + "step: 3667, Red action: do-nothing, Blue reward:0.85\n", + "step: 3668, Red action: do-nothing, Blue reward:0.85\n", + "step: 3669, Red action: do-nothing, Blue reward:0.85\n", + "step: 3670, Red action: do-nothing, Blue reward:0.85\n", + "step: 3671, Red action: do-nothing, Blue reward:0.85\n", + "step: 3672, Red action: do-nothing, Blue reward:0.85\n", + "step: 3673, Red action: do-nothing, Blue reward:0.85\n", + "step: 3674, Red action: do-nothing, Blue reward:0.85\n", + "step: 3675, Red action: do-nothing, Blue reward:0.85\n", + "step: 3676, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3677, Red action: do-nothing, Blue reward:0.85\n", + "step: 3678, Red action: do-nothing, Blue reward:0.85\n", + "step: 3679, Red action: do-nothing, Blue reward:0.85\n", + "step: 3680, Red action: do-nothing, Blue reward:0.85\n", + "step: 3681, Red action: do-nothing, Blue reward:0.85\n", + "step: 3682, Red action: do-nothing, Blue reward:0.85\n", + "step: 3683, Red action: do-nothing, Blue reward:0.85\n", + "step: 3684, Red action: do-nothing, Blue reward:0.85\n", + "step: 3685, Red action: do-nothing, Blue reward:0.85\n", + "step: 3686, Red action: do-nothing, Blue reward:0.85\n", + "step: 3687, Red action: do-nothing, Blue reward:0.85\n", + "step: 3688, Red action: do-nothing, Blue reward:0.85\n", + "step: 3689, Red action: do-nothing, Blue reward:0.85\n", + "step: 3690, Red action: do-nothing, Blue reward:0.85\n", + "step: 3691, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3692, Red action: do-nothing, Blue reward:0.85\n", + "step: 3693, Red action: do-nothing, Blue reward:0.85\n", + "step: 3694, Red action: do-nothing, Blue reward:0.85\n", + "step: 3695, Red action: do-nothing, Blue reward:0.85\n", + "step: 3696, Red action: do-nothing, Blue reward:0.85\n", + "step: 3697, Red action: do-nothing, Blue reward:0.85\n", + "step: 3698, Red action: do-nothing, Blue reward:0.85\n", + "step: 3699, Red action: do-nothing, Blue reward:0.85\n", + "step: 3700, Red action: do-nothing, Blue reward:0.85\n", + "step: 3701, Red action: do-nothing, Blue reward:0.85\n", + "step: 3702, Red action: do-nothing, Blue reward:0.85\n", + "step: 3703, Red action: do-nothing, Blue reward:0.85\n", + "step: 3704, Red action: do-nothing, Blue reward:0.85\n", + "step: 3705, Red action: do-nothing, Blue reward:0.85\n", + "step: 3706, Red action: do-nothing, Blue reward:0.85\n", + "step: 3707, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3708, Red action: do-nothing, Blue reward:0.85\n", + "step: 3709, Red action: do-nothing, Blue reward:0.85\n", + "step: 3710, Red action: do-nothing, Blue reward:0.85\n", + "step: 3711, Red action: do-nothing, Blue reward:0.85\n", + "step: 3712, Red action: do-nothing, Blue reward:0.85\n", + "step: 3713, Red action: do-nothing, Blue reward:0.85\n", + "step: 3714, Red action: do-nothing, Blue reward:0.85\n", + "step: 3715, Red action: do-nothing, Blue reward:0.85\n", + "step: 3716, Red action: do-nothing, Blue reward:0.85\n", + "step: 3717, Red action: do-nothing, Blue reward:0.85\n", + "step: 3718, Red action: do-nothing, Blue reward:0.85\n", + "step: 3719, Red action: do-nothing, Blue reward:0.85\n", + "step: 3720, Red action: do-nothing, Blue reward:0.85\n", + "step: 3721, Red action: do-nothing, Blue reward:0.85\n", + "step: 3722, Red action: do-nothing, Blue reward:0.85\n", + "step: 3723, Red action: do-nothing, Blue reward:0.85\n", + "step: 3724, Red action: do-nothing, Blue reward:0.85\n", + "step: 3725, Red action: do-nothing, Blue reward:0.85\n", + "step: 3726, Red action: do-nothing, Blue reward:0.85\n", + "step: 3727, Red action: do-nothing, Blue reward:0.85\n", + "step: 3728, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3729, Red action: do-nothing, Blue reward:0.85\n", + "step: 3730, Red action: do-nothing, Blue reward:0.85\n", + "step: 3731, Red action: do-nothing, Blue reward:0.85\n", + "step: 3732, Red action: do-nothing, Blue reward:0.85\n", + "step: 3733, Red action: do-nothing, Blue reward:0.85\n", + "step: 3734, Red action: do-nothing, Blue reward:0.85\n", + "step: 3735, Red action: do-nothing, Blue reward:0.85\n", + "step: 3736, Red action: do-nothing, Blue reward:0.85\n", + "step: 3737, Red action: do-nothing, Blue reward:0.85\n", + "step: 3738, Red action: do-nothing, Blue reward:0.85\n", + "step: 3739, Red action: do-nothing, Blue reward:0.85\n", + "step: 3740, Red action: do-nothing, Blue reward:0.85\n", + "step: 3741, Red action: do-nothing, Blue reward:0.85\n", + "step: 3742, Red action: do-nothing, Blue reward:0.85\n", + "step: 3743, Red action: do-nothing, Blue reward:0.85\n", + "step: 3744, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3745, Red action: do-nothing, Blue reward:0.85\n", + "step: 3746, Red action: do-nothing, Blue reward:0.85\n", + "step: 3747, Red action: do-nothing, Blue reward:0.85\n", + "step: 3748, Red action: do-nothing, Blue reward:0.85\n", + "step: 3749, Red action: do-nothing, Blue reward:0.85\n", + "step: 3750, Red action: do-nothing, Blue reward:0.85\n", + "step: 3751, Red action: do-nothing, Blue reward:0.85\n", + "step: 3752, Red action: do-nothing, Blue reward:0.85\n", + "step: 3753, Red action: do-nothing, Blue reward:0.85\n", + "step: 3754, Red action: do-nothing, Blue reward:0.85\n", + "step: 3755, Red action: do-nothing, Blue reward:0.85\n", + "step: 3756, Red action: do-nothing, Blue reward:0.85\n", + "step: 3757, Red action: do-nothing, Blue reward:0.85\n", + "step: 3758, Red action: do-nothing, Blue reward:0.85\n", + "step: 3759, Red action: do-nothing, Blue reward:0.85\n", + "step: 3760, Red action: do-nothing, Blue reward:0.85\n", + "step: 3761, Red action: do-nothing, Blue reward:0.85\n", + "step: 3762, Red action: do-nothing, Blue reward:0.85\n", + "step: 3763, Red action: do-nothing, Blue reward:0.85\n", + "step: 3764, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3765, Red action: do-nothing, Blue reward:0.85\n", + "step: 3766, Red action: do-nothing, Blue reward:0.85\n", + "step: 3767, Red action: do-nothing, Blue reward:0.85\n", + "step: 3768, Red action: do-nothing, Blue reward:0.85\n", + "step: 3769, Red action: do-nothing, Blue reward:0.85\n", + "step: 3770, Red action: do-nothing, Blue reward:0.85\n", + "step: 3771, Red action: do-nothing, Blue reward:0.85\n", + "step: 3772, Red action: do-nothing, Blue reward:0.85\n", + "step: 3773, Red action: do-nothing, Blue reward:0.85\n", + "step: 3774, Red action: do-nothing, Blue reward:0.85\n", + "step: 3775, Red action: do-nothing, Blue reward:0.85\n", + "step: 3776, Red action: do-nothing, Blue reward:0.85\n", + "step: 3777, Red action: do-nothing, Blue reward:0.85\n", + "step: 3778, Red action: do-nothing, Blue reward:0.85\n", + "step: 3779, Red action: do-nothing, Blue reward:0.85\n", + "step: 3780, Red action: do-nothing, Blue reward:0.85\n", + "step: 3781, Red action: do-nothing, Blue reward:0.85\n", + "step: 3782, Red action: do-nothing, Blue reward:0.85\n", + "step: 3783, Red action: do-nothing, Blue reward:0.85\n", + "step: 3784, Red action: node-application-execute, Blue reward:0.85\n", + "step: 3785, Red action: do-nothing, Blue reward:0.85\n", + "step: 3786, Red action: do-nothing, Blue reward:0.85\n", + "step: 3787, Red action: do-nothing, Blue reward:0.85\n", + "step: 3788, Red action: do-nothing, Blue reward:0.85\n", + "step: 3789, Red action: do-nothing, Blue reward:0.85\n", + "step: 3790, Red action: do-nothing, Blue reward:0.85\n", + "step: 3791, Red action: do-nothing, Blue reward:0.85\n", + "step: 3792, Red action: do-nothing, Blue reward:0.85\n", + "step: 3793, Red action: do-nothing, Blue reward:0.85\n", + "step: 3794, Red action: do-nothing, Blue reward:0.85\n", + "step: 3795, Red action: do-nothing, Blue reward:0.85\n", + "step: 3796, Red action: do-nothing, Blue reward:0.85\n", + "step: 3797, Red action: do-nothing, Blue reward:0.85\n", + "step: 3798, Red action: do-nothing, Blue reward:0.85\n", + "step: 3799, Red action: do-nothing, Blue reward:0.85\n", + "step: 3800, Red action: do-nothing, Blue reward:0.85\n", + "step: 3801, Red action: do-nothing, Blue reward:0.85\n", + "step: 3802, Red action: do-nothing, Blue reward:0.85\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[11], line 11\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstep: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00menv\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mstep_counter\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Red action: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00minfo[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124magent_actions\u001b[39m\u001b[38;5;124m'\u001b[39m][\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata_manipulation_attacker\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39maction\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Blue reward:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mreward\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m )\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28mabs\u001b[39m(reward \u001b[38;5;241m-\u001b[39m \u001b[38;5;241m0.8\u001b[39m) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m1e-5\u001b[39m:\n\u001b[0;32m---> 11\u001b[0m obs, reward, terminated, truncated, info \u001b[38;5;241m=\u001b[39m \u001b[43menv\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstep\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# do nothing\u001b[39;00m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstep: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00menv\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mstep_counter\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Red action: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00minfo[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124magent_actions\u001b[39m\u001b[38;5;124m'\u001b[39m][\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata_manipulation_attacker\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39maction\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Blue reward:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mreward\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m )\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m env\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mstep_counter \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m10000\u001b[39m:\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/session/environment.py:111\u001b[0m, in \u001b[0;36mPrimaiteGymEnv.step\u001b[0;34m(self, action)\u001b[0m\n\u001b[1;32m 109\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mapply_agent_actions()\n\u001b[1;32m 110\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39madvance_timestep()\n\u001b[0;32m--> 111\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgame\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_sim_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 112\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mupdate_agents(state)\n\u001b[1;32m 114\u001b[0m next_obs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_obs() \u001b[38;5;66;03m# this doesn't update observation, just gets the current observation\u001b[39;00m\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/game/game.py:151\u001b[0m, in \u001b[0;36mPrimaiteGame.get_sim_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 149\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_sim_state\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Dict:\n\u001b[1;32m 150\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get the current state of the simulation.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 151\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msimulation\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/sim_container.py:57\u001b[0m, in \u001b[0;36mSimulation.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 46\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 47\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of this object.\u001b[39;00m\n\u001b[1;32m 48\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 52\u001b[0m \u001b[38;5;124;03m:rtype: Dict\u001b[39;00m\n\u001b[1;32m 53\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 54\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 55\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 56\u001b[0m {\n\u001b[0;32m---> 57\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnetwork\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnetwork\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m 58\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdomain\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdomain\u001b[38;5;241m.\u001b[39mdescribe_state(),\n\u001b[1;32m 59\u001b[0m }\n\u001b[1;32m 60\u001b[0m )\n\u001b[1;32m 61\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/container.py:260\u001b[0m, in \u001b[0;36mNetwork.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 252\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 253\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of the Network.\u001b[39;00m\n\u001b[1;32m 254\u001b[0m \n\u001b[1;32m 255\u001b[0m \u001b[38;5;124;03m:return: A dictionary capturing the current state of the Network and its child objects.\u001b[39;00m\n\u001b[1;32m 256\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 257\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 258\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 259\u001b[0m {\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnodes\u001b[39m\u001b[38;5;124m\"\u001b[39m: {node\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname: node\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m node \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnodes\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 261\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinks\u001b[39m\u001b[38;5;124m\"\u001b[39m: {},\n\u001b[1;32m 262\u001b[0m }\n\u001b[1;32m 263\u001b[0m )\n\u001b[1;32m 264\u001b[0m \u001b[38;5;66;03m# Update the links one-by-one. The key is a 4-tuple of `hostname_a, port_a, hostname_b, port_b`\u001b[39;00m\n\u001b[1;32m 265\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m _, link \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlinks\u001b[38;5;241m.\u001b[39mitems():\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/container.py:260\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 252\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 253\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of the Network.\u001b[39;00m\n\u001b[1;32m 254\u001b[0m \n\u001b[1;32m 255\u001b[0m \u001b[38;5;124;03m:return: A dictionary capturing the current state of the Network and its child objects.\u001b[39;00m\n\u001b[1;32m 256\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 257\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 258\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 259\u001b[0m {\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnodes\u001b[39m\u001b[38;5;124m\"\u001b[39m: {node\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname: \u001b[43mnode\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m node \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnodes\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 261\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinks\u001b[39m\u001b[38;5;124m\"\u001b[39m: {},\n\u001b[1;32m 262\u001b[0m }\n\u001b[1;32m 263\u001b[0m )\n\u001b[1;32m 264\u001b[0m \u001b[38;5;66;03m# Update the links one-by-one. The key is a 4-tuple of `hostname_a, port_a, hostname_b, port_b`\u001b[39;00m\n\u001b[1;32m 265\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m _, link \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlinks\u001b[38;5;241m.\u001b[39mitems():\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/hardware/base.py:1900\u001b[0m, in \u001b[0;36mNode.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1881\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1882\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of this object.\u001b[39;00m\n\u001b[1;32m 1883\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1887\u001b[0m \u001b[38;5;124;03m:rtype: Dict\u001b[39;00m\n\u001b[1;32m 1888\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1889\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1890\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 1891\u001b[0m {\n\u001b[1;32m 1892\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhostname\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname,\n\u001b[1;32m 1893\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moperating_state\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39moperating_state\u001b[38;5;241m.\u001b[39mvalue,\n\u001b[1;32m 1894\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNICs\u001b[39m\u001b[38;5;124m\"\u001b[39m: {\n\u001b[1;32m 1895\u001b[0m eth_num: network_interface\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1896\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m eth_num, network_interface \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnetwork_interface\u001b[38;5;241m.\u001b[39mitems()\n\u001b[1;32m 1897\u001b[0m },\n\u001b[1;32m 1898\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfile_system\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfile_system\u001b[38;5;241m.\u001b[39mdescribe_state(),\n\u001b[1;32m 1899\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapplications\u001b[39m\u001b[38;5;124m\"\u001b[39m: {app\u001b[38;5;241m.\u001b[39mname: app\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m app \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapplications\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[0;32m-> 1900\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mservices\u001b[39m\u001b[38;5;124m\"\u001b[39m: {svc\u001b[38;5;241m.\u001b[39mname: svc\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m svc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mservices\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1901\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprocess\u001b[39m\u001b[38;5;124m\"\u001b[39m: {proc\u001b[38;5;241m.\u001b[39mname: proc\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m proc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprocesses\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1902\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrevealed_to_red\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mrevealed_to_red,\n\u001b[1;32m 1903\u001b[0m }\n\u001b[1;32m 1904\u001b[0m )\n\u001b[1;32m 1905\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/hardware/base.py:1900\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 1881\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1882\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of this object.\u001b[39;00m\n\u001b[1;32m 1883\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1887\u001b[0m \u001b[38;5;124;03m:rtype: Dict\u001b[39;00m\n\u001b[1;32m 1888\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1889\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1890\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 1891\u001b[0m {\n\u001b[1;32m 1892\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhostname\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname,\n\u001b[1;32m 1893\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moperating_state\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39moperating_state\u001b[38;5;241m.\u001b[39mvalue,\n\u001b[1;32m 1894\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNICs\u001b[39m\u001b[38;5;124m\"\u001b[39m: {\n\u001b[1;32m 1895\u001b[0m eth_num: network_interface\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1896\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m eth_num, network_interface \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnetwork_interface\u001b[38;5;241m.\u001b[39mitems()\n\u001b[1;32m 1897\u001b[0m },\n\u001b[1;32m 1898\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfile_system\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfile_system\u001b[38;5;241m.\u001b[39mdescribe_state(),\n\u001b[1;32m 1899\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapplications\u001b[39m\u001b[38;5;124m\"\u001b[39m: {app\u001b[38;5;241m.\u001b[39mname: app\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m app \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapplications\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[0;32m-> 1900\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mservices\u001b[39m\u001b[38;5;124m\"\u001b[39m: {svc\u001b[38;5;241m.\u001b[39mname: \u001b[43msvc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m svc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mservices\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1901\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprocess\u001b[39m\u001b[38;5;124m\"\u001b[39m: {proc\u001b[38;5;241m.\u001b[39mname: proc\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m proc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprocesses\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1902\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrevealed_to_red\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mrevealed_to_red,\n\u001b[1;32m 1903\u001b[0m }\n\u001b[1;32m 1904\u001b[0m )\n\u001b[1;32m 1905\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", + "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/system/services/ntp/ntp_client.py:61\u001b[0m, in \u001b[0;36mNTPClient.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 50\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdescribe_state\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Dict:\n\u001b[1;32m 51\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 52\u001b[0m \u001b[38;5;124;03m Describes the current state of the software.\u001b[39;00m\n\u001b[1;32m 53\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[38;5;124;03m :rtype: Dict\u001b[39;00m\n\u001b[1;32m 60\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m---> 61\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 62\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], "source": [ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", @@ -600,7 +5091,7 @@ "while abs(reward - 0.8) > 1e-5:\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", - " if env.game.step_counter > 10000:\n", + " if env.game.step_counter > 2000:\n", " break # make sure there's no infinite loop if something went wrong" ] }, From 7dafec85176130dc3c9ce66bf4dbe8ec3b2faa7e Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 17 Feb 2025 15:01:02 +0000 Subject: [PATCH 206/224] #3075: Test fix --- .../game_layer/actions/test_terminal_actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/game_layer/actions/test_terminal_actions.py b/tests/integration_tests/game_layer/actions/test_terminal_actions.py index c39d8263..af9c7b74 100644 --- a/tests/integration_tests/game_layer/actions/test_terminal_actions.py +++ b/tests/integration_tests/game_layer/actions/test_terminal_actions.py @@ -106,7 +106,7 @@ def test_remote_login_change_password(game_and_agent_fixture: Tuple[PrimaiteGame "username": "user123", "current_password": "password", "new_password": "different_password", - "remote_ip": str(server_1.network_interface[1].ip_address), + # "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) @@ -146,7 +146,7 @@ def test_change_password_logs_out_user(game_and_agent_fixture: Tuple[PrimaiteGam "username": "user123", "current_password": "password", "new_password": "different_password", - "remote_ip": str(server_1.network_interface[1].ip_address), + # "remote_ip": str(server_1.network_interface[1].ip_address), }, ) agent.store_action(action) From 5f076ba225bf239a61377478949e8b474b7504d7 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 17 Feb 2025 15:12:12 +0000 Subject: [PATCH 207/224] #3075: Fix pre-commit errors --- .../Data-Manipulation-E2E-Demonstration.ipynb | 4527 +---------------- 1 file changed, 18 insertions(+), 4509 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index f3e3fb98..31e1f9d3 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -373,29 +373,16 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2025-02-17 11:15:30,959: Performing the PrimAITE first-time setup...\n", - "2025-02-17 11:15:30,960: Building the PrimAITE app directories...\n", - "2025-02-17 11:15:30,960: Building primaite_config.yaml...\n", - "2025-02-17 11:15:30,960: Rebuilding the demo notebooks...\n", - "2025-02-17 11:15:30,967: Rebuilding the example notebooks...\n", - "2025-02-17 11:15:30,970: PrimAITE setup complete!\n" - ] - } - ], + "outputs": [], "source": [ "!primaite setup" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, @@ -407,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "tags": [] }, @@ -433,254 +420,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2025-02-17 11:15:35,108: PrimaiteGymEnv RNG seed = None\n", - "2025-02-17 11:15:35,109: Resetting environment, episode 0, avg. reward: 0.0\n", - "2025-02-17 11:15:35,111: Saving agent action log to /home/nick/primaite/4.0.0a1-dev/sessions/2025-02-17/11-15-31/agent_actions/episode_0.json\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "env created successfully\n", - "{'ICS': 0,\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 0}},\n", - " 2: {'PROTOCOLS': {'ALL': 0}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 0}},\n", - " 5: {'PROTOCOLS': {'ALL': 0}},\n", - " 6: {'PROTOCOLS': {'ALL': 0}},\n", - " 7: {'PROTOCOLS': {'ALL': 0}},\n", - " 8: {'PROTOCOLS': {'ALL': 0}},\n", - " 9: {'PROTOCOLS': {'ALL': 0}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'NODES': {'HOST0': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST1': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 1}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST2': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST3': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST4': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST5': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST6': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0,\n", - " 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0,\n", - " 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'ROUTER0': {'ACL': {1: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 0,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 2: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 1,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 3: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 2,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 4: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 3,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 5: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 4,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 6: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 5,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 7: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 6,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 8: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 7,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 9: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 8,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 10: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 9,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}}}\n" - ] - } - ], + "outputs": [], "source": [ "# create the env\n", "with open(data_manipulation_config_path(), 'r') as f:\n", @@ -708,7 +450,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -726,51 +468,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 1, Red action: DO NOTHING, Blue reward:0.65\n", - "step: 2, Red action: DO NOTHING, Blue reward:0.65\n", - "step: 3, Red action: DO NOTHING, Blue reward:0.65\n", - "step: 4, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 5, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 6, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 7, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 8, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 9, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 10, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 11, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 12, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 13, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 14, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 15, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 16, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 17, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 18, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 19, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 20, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 21, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 22, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 23, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 24, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 25, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 26, Red action: ATTACK from client 2, Blue reward:0.15\n", - "step: 27, Red action: DO NOTHING, Blue reward:-0.35\n", - "step: 28, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 29, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 30, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 31, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 32, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 33, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 34, Red action: DO NOTHING, Blue reward:-0.85\n", - "step: 35, Red action: DO NOTHING, Blue reward:-0.85\n" - ] - } - ], + "outputs": [], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -786,198 +486,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'HOST0': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST1': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST2': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST3': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST4': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST5': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST6': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'ROUTER0': {'ACL': {1: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 0,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 2: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 1,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 3: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 2,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 4: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 3,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 5: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 4,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 6: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 5,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 7: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 6,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 8: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 7,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 9: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 8,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 10: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 9,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}}\n" - ] - } - ], + "outputs": [], "source": [ "pprint(obs['NODES'])" ] @@ -991,198 +502,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'HOST0': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST1': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST2': {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST3': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST4': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST5': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'HOST6': {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'TRAFFIC': {'icmp': {'inbound': 0, 'outbound': 0},\n", - " 'tcp': {53: {'inbound': 0, 'outbound': 0}}},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1,\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}},\n", - " 'ROUTER0': {'ACL': {1: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 0,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 2: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 1,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 3: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 2,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 4: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 3,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 5: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 4,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 6: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 5,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 7: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 6,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 8: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 7,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 9: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 8,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0},\n", - " 10: {'dest_ip_id': 0,\n", - " 'dest_port_id': 0,\n", - " 'dest_wildcard_id': 0,\n", - " 'permission': 0,\n", - " 'position': 9,\n", - " 'protocol_id': 0,\n", - " 'source_ip_id': 0,\n", - " 'source_port_id': 0,\n", - " 'source_wildcard_id': 0}},\n", - " 'users': {'local_login': 0, 'remote_sessions': 0}}}\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -1214,21 +536,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 38\n", - "Red action: do-nothing\n", - "Green action: do-nothing\n", - "Green action: node-application-execute\n", - "Blue reward:-0.8500000000000001\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -1251,21 +561,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 39\n", - "Red action: do-nothing\n", - "Green action: timestep=38 action='node-application-execute' parameters={'node_name': 'client_2', 'application_name': 'web-browser'} request=['network', 'node', 'client_2', 'application', 'web-browser', 'execute'] response=RequestResponse(status='failure', data={}) reward=-0.2 reward_info={'connection_attempt_status': 'n/a'}\n", - "Green action: timestep=38 action='node-application-execute' parameters={'node_name': 'client_1', 'application_name': 'web-browser'} request=['network', 'node', 'client_1', 'application', 'web-browser', 'execute'] response=RequestResponse(status='failure', data={}) reward=-0.25 reward_info={'connection_attempt_status': 'n/a'}\n", - "Blue reward:-0.05\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -1288,3796 +586,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 40, Red action: do-nothing, Blue reward:-0.05\n", - "step: 41, Red action: do-nothing, Blue reward:-0.05\n", - "step: 42, Red action: do-nothing, Blue reward:-0.05\n", - "step: 43, Red action: do-nothing, Blue reward:0.95\n", - "step: 44, Red action: do-nothing, Blue reward:0.95\n", - "step: 45, Red action: do-nothing, Blue reward:0.95\n", - "step: 46, Red action: do-nothing, Blue reward:0.95\n", - "step: 47, Red action: do-nothing, Blue reward:0.95\n", - "step: 48, Red action: do-nothing, Blue reward:0.95\n", - "step: 49, Red action: do-nothing, Blue reward:0.95\n", - "step: 50, Red action: do-nothing, Blue reward:0.95\n", - "step: 51, Red action: node-application-execute, Blue reward:0.95\n", - "step: 52, Red action: do-nothing, Blue reward:0.95\n", - "step: 53, Red action: do-nothing, Blue reward:0.85\n", - "step: 54, Red action: do-nothing, Blue reward:0.85\n", - "step: 55, Red action: do-nothing, Blue reward:0.85\n", - "step: 56, Red action: do-nothing, Blue reward:0.85\n", - "step: 57, Red action: do-nothing, Blue reward:0.85\n", - "step: 58, Red action: do-nothing, Blue reward:0.85\n", - "step: 59, Red action: do-nothing, Blue reward:0.85\n", - "step: 60, Red action: do-nothing, Blue reward:0.85\n", - "step: 61, Red action: do-nothing, Blue reward:0.85\n", - "step: 62, Red action: do-nothing, Blue reward:0.85\n", - "step: 63, Red action: do-nothing, Blue reward:0.85\n", - "step: 64, Red action: do-nothing, Blue reward:0.85\n", - "step: 65, Red action: do-nothing, Blue reward:0.85\n", - "step: 66, Red action: do-nothing, Blue reward:0.85\n", - "step: 67, Red action: do-nothing, Blue reward:0.85\n", - "step: 68, Red action: do-nothing, Blue reward:0.85\n", - "step: 69, Red action: do-nothing, Blue reward:0.85\n", - "step: 70, Red action: do-nothing, Blue reward:0.85\n", - "step: 71, Red action: do-nothing, Blue reward:0.85\n", - "step: 72, Red action: do-nothing, Blue reward:0.85\n", - "step: 73, Red action: do-nothing, Blue reward:0.85\n", - "step: 74, Red action: do-nothing, Blue reward:0.85\n", - "step: 75, Red action: node-application-execute, Blue reward:0.85\n", - "step: 76, Red action: do-nothing, Blue reward:0.85\n", - "step: 77, Red action: do-nothing, Blue reward:0.85\n", - "step: 78, Red action: do-nothing, Blue reward:0.85\n", - "step: 79, Red action: do-nothing, Blue reward:0.85\n", - "step: 80, Red action: do-nothing, Blue reward:0.85\n", - "step: 81, Red action: do-nothing, Blue reward:0.85\n", - "step: 82, Red action: do-nothing, Blue reward:0.85\n", - "step: 83, Red action: do-nothing, Blue reward:0.85\n", - "step: 84, Red action: do-nothing, Blue reward:0.85\n", - "step: 85, Red action: do-nothing, Blue reward:0.85\n", - "step: 86, Red action: do-nothing, Blue reward:0.85\n", - "step: 87, Red action: do-nothing, Blue reward:0.85\n", - "step: 88, Red action: do-nothing, Blue reward:0.85\n", - "step: 89, Red action: do-nothing, Blue reward:0.85\n", - "step: 90, Red action: do-nothing, Blue reward:0.85\n", - "step: 91, Red action: do-nothing, Blue reward:0.85\n", - "step: 92, Red action: do-nothing, Blue reward:0.85\n", - "step: 93, Red action: do-nothing, Blue reward:0.85\n", - "step: 94, Red action: do-nothing, Blue reward:0.85\n", - "step: 95, Red action: do-nothing, Blue reward:0.85\n", - "step: 96, Red action: do-nothing, Blue reward:0.85\n", - "step: 97, Red action: node-application-execute, Blue reward:0.85\n", - "step: 98, Red action: do-nothing, Blue reward:0.85\n", - "step: 99, Red action: do-nothing, Blue reward:0.85\n", - "step: 100, Red action: do-nothing, Blue reward:0.85\n", - "step: 101, Red action: do-nothing, Blue reward:0.85\n", - "step: 102, Red action: do-nothing, Blue reward:0.85\n", - "step: 103, Red action: do-nothing, Blue reward:0.85\n", - "step: 104, Red action: do-nothing, Blue reward:0.85\n", - "step: 105, Red action: do-nothing, Blue reward:0.85\n", - "step: 106, Red action: do-nothing, Blue reward:0.85\n", - "step: 107, Red action: do-nothing, Blue reward:0.85\n", - "step: 108, Red action: do-nothing, Blue reward:0.85\n", - "step: 109, Red action: do-nothing, Blue reward:0.85\n", - "step: 110, Red action: do-nothing, Blue reward:0.85\n", - "step: 111, Red action: do-nothing, Blue reward:0.85\n", - "step: 112, Red action: do-nothing, Blue reward:0.85\n", - "step: 113, Red action: node-application-execute, Blue reward:0.85\n", - "step: 114, Red action: do-nothing, Blue reward:0.85\n", - "step: 115, Red action: do-nothing, Blue reward:0.85\n", - "step: 116, Red action: do-nothing, Blue reward:0.85\n", - "step: 117, Red action: do-nothing, Blue reward:0.85\n", - "step: 118, Red action: do-nothing, Blue reward:0.85\n", - "step: 119, Red action: do-nothing, Blue reward:0.85\n", - "step: 120, Red action: do-nothing, Blue reward:0.85\n", - "step: 121, Red action: do-nothing, Blue reward:0.85\n", - "step: 122, Red action: do-nothing, Blue reward:0.85\n", - "step: 123, Red action: do-nothing, Blue reward:0.85\n", - "step: 124, Red action: do-nothing, Blue reward:0.85\n", - "step: 125, Red action: do-nothing, Blue reward:0.85\n", - "step: 126, Red action: do-nothing, Blue reward:0.85\n", - "step: 127, Red action: do-nothing, Blue reward:0.85\n", - "step: 128, Red action: do-nothing, Blue reward:0.85\n", - "step: 129, Red action: do-nothing, Blue reward:0.85\n", - "step: 130, Red action: do-nothing, Blue reward:0.85\n", - "step: 131, Red action: do-nothing, Blue reward:0.85\n", - "step: 132, Red action: node-application-execute, Blue reward:0.85\n", - "step: 133, Red action: do-nothing, Blue reward:0.85\n", - "step: 134, Red action: do-nothing, Blue reward:0.85\n", - "step: 135, Red action: do-nothing, Blue reward:0.85\n", - "step: 136, Red action: do-nothing, Blue reward:0.85\n", - "step: 137, Red action: do-nothing, Blue reward:0.85\n", - "step: 138, Red action: do-nothing, Blue reward:0.85\n", - "step: 139, Red action: do-nothing, Blue reward:0.85\n", - "step: 140, Red action: do-nothing, Blue reward:0.85\n", - "step: 141, Red action: do-nothing, Blue reward:0.85\n", - "step: 142, Red action: do-nothing, Blue reward:0.85\n", - "step: 143, Red action: do-nothing, Blue reward:0.85\n", - "step: 144, Red action: do-nothing, Blue reward:0.85\n", - "step: 145, Red action: do-nothing, Blue reward:0.85\n", - "step: 146, Red action: do-nothing, Blue reward:0.85\n", - "step: 147, Red action: do-nothing, Blue reward:0.85\n", - "step: 148, Red action: do-nothing, Blue reward:0.85\n", - "step: 149, Red action: do-nothing, Blue reward:0.85\n", - "step: 150, Red action: do-nothing, Blue reward:0.85\n", - "step: 151, Red action: do-nothing, Blue reward:0.85\n", - "step: 152, Red action: do-nothing, Blue reward:0.85\n", - "step: 153, Red action: do-nothing, Blue reward:0.85\n", - "step: 154, Red action: do-nothing, Blue reward:0.85\n", - "step: 155, Red action: do-nothing, Blue reward:0.85\n", - "step: 156, Red action: do-nothing, Blue reward:0.85\n", - "step: 157, Red action: node-application-execute, Blue reward:0.85\n", - "step: 158, Red action: do-nothing, Blue reward:0.85\n", - "step: 159, Red action: do-nothing, Blue reward:0.85\n", - "step: 160, Red action: do-nothing, Blue reward:0.85\n", - "step: 161, Red action: do-nothing, Blue reward:0.85\n", - "step: 162, Red action: do-nothing, Blue reward:0.85\n", - "step: 163, Red action: do-nothing, Blue reward:0.85\n", - "step: 164, Red action: do-nothing, Blue reward:0.85\n", - "step: 165, Red action: do-nothing, Blue reward:0.85\n", - "step: 166, Red action: do-nothing, Blue reward:0.85\n", - "step: 167, Red action: do-nothing, Blue reward:0.85\n", - "step: 168, Red action: do-nothing, Blue reward:0.85\n", - "step: 169, Red action: do-nothing, Blue reward:0.85\n", - "step: 170, Red action: do-nothing, Blue reward:0.85\n", - "step: 171, Red action: do-nothing, Blue reward:0.85\n", - "step: 172, Red action: do-nothing, Blue reward:0.85\n", - "step: 173, Red action: do-nothing, Blue reward:0.85\n", - "step: 174, Red action: do-nothing, Blue reward:0.85\n", - "step: 175, Red action: do-nothing, Blue reward:0.85\n", - "step: 176, Red action: do-nothing, Blue reward:0.85\n", - "step: 177, Red action: do-nothing, Blue reward:0.85\n", - "step: 178, Red action: node-application-execute, Blue reward:0.85\n", - "step: 179, Red action: do-nothing, Blue reward:0.85\n", - "step: 180, Red action: do-nothing, Blue reward:0.85\n", - "step: 181, Red action: do-nothing, Blue reward:0.85\n", - "step: 182, Red action: do-nothing, Blue reward:0.85\n", - "step: 183, Red action: do-nothing, Blue reward:0.85\n", - "step: 184, Red action: do-nothing, Blue reward:0.85\n", - "step: 185, Red action: do-nothing, Blue reward:0.85\n", - "step: 186, Red action: do-nothing, Blue reward:0.85\n", - "step: 187, Red action: do-nothing, Blue reward:0.85\n", - "step: 188, Red action: do-nothing, Blue reward:0.85\n", - "step: 189, Red action: do-nothing, Blue reward:0.85\n", - "step: 190, Red action: do-nothing, Blue reward:0.85\n", - "step: 191, Red action: do-nothing, Blue reward:0.85\n", - "step: 192, Red action: do-nothing, Blue reward:0.85\n", - "step: 193, Red action: do-nothing, Blue reward:0.85\n", - "step: 194, Red action: do-nothing, Blue reward:0.85\n", - "step: 195, Red action: do-nothing, Blue reward:0.85\n", - "step: 196, Red action: do-nothing, Blue reward:0.85\n", - "step: 197, Red action: do-nothing, Blue reward:0.85\n", - "step: 198, Red action: node-application-execute, Blue reward:0.85\n", - "step: 199, Red action: do-nothing, Blue reward:0.85\n", - "step: 200, Red action: do-nothing, Blue reward:0.85\n", - "step: 201, Red action: do-nothing, Blue reward:0.85\n", - "step: 202, Red action: do-nothing, Blue reward:0.85\n", - "step: 203, Red action: do-nothing, Blue reward:0.85\n", - "step: 204, Red action: do-nothing, Blue reward:0.85\n", - "step: 205, Red action: do-nothing, Blue reward:0.85\n", - "step: 206, Red action: do-nothing, Blue reward:0.85\n", - "step: 207, Red action: do-nothing, Blue reward:0.85\n", - "step: 208, Red action: do-nothing, Blue reward:0.85\n", - "step: 209, Red action: do-nothing, Blue reward:0.85\n", - "step: 210, Red action: do-nothing, Blue reward:0.85\n", - "step: 211, Red action: do-nothing, Blue reward:0.85\n", - "step: 212, Red action: do-nothing, Blue reward:0.85\n", - "step: 213, Red action: do-nothing, Blue reward:0.85\n", - "step: 214, Red action: do-nothing, Blue reward:0.85\n", - "step: 215, Red action: do-nothing, Blue reward:0.85\n", - "step: 216, Red action: do-nothing, Blue reward:0.85\n", - "step: 217, Red action: do-nothing, Blue reward:0.85\n", - "step: 218, Red action: node-application-execute, Blue reward:0.85\n", - "step: 219, Red action: do-nothing, Blue reward:0.85\n", - "step: 220, Red action: do-nothing, Blue reward:0.85\n", - "step: 221, Red action: do-nothing, Blue reward:0.85\n", - "step: 222, Red action: do-nothing, Blue reward:0.85\n", - "step: 223, Red action: do-nothing, Blue reward:0.85\n", - "step: 224, Red action: do-nothing, Blue reward:0.85\n", - "step: 225, Red action: do-nothing, Blue reward:0.85\n", - "step: 226, Red action: do-nothing, Blue reward:0.85\n", - "step: 227, Red action: do-nothing, Blue reward:0.85\n", - "step: 228, Red action: do-nothing, Blue reward:0.85\n", - "step: 229, Red action: do-nothing, Blue reward:0.85\n", - "step: 230, Red action: do-nothing, Blue reward:0.85\n", - "step: 231, Red action: do-nothing, Blue reward:0.85\n", - "step: 232, Red action: do-nothing, Blue reward:0.85\n", - "step: 233, Red action: do-nothing, Blue reward:0.85\n", - "step: 234, Red action: do-nothing, Blue reward:0.85\n", - "step: 235, Red action: do-nothing, Blue reward:0.85\n", - "step: 236, Red action: do-nothing, Blue reward:0.85\n", - "step: 237, Red action: do-nothing, Blue reward:0.85\n", - "step: 238, Red action: do-nothing, Blue reward:0.85\n", - "step: 239, Red action: do-nothing, Blue reward:0.85\n", - "step: 240, Red action: do-nothing, Blue reward:0.85\n", - "step: 241, Red action: node-application-execute, Blue reward:0.85\n", - "step: 242, Red action: do-nothing, Blue reward:0.85\n", - "step: 243, Red action: do-nothing, Blue reward:0.85\n", - "step: 244, Red action: do-nothing, Blue reward:0.85\n", - "step: 245, Red action: do-nothing, Blue reward:0.85\n", - "step: 246, Red action: do-nothing, Blue reward:0.85\n", - "step: 247, Red action: do-nothing, Blue reward:0.85\n", - "step: 248, Red action: do-nothing, Blue reward:0.85\n", - "step: 249, Red action: do-nothing, Blue reward:0.85\n", - "step: 250, Red action: do-nothing, Blue reward:0.85\n", - "step: 251, Red action: do-nothing, Blue reward:0.85\n", - "step: 252, Red action: do-nothing, Blue reward:0.85\n", - "step: 253, Red action: do-nothing, Blue reward:0.85\n", - "step: 254, Red action: do-nothing, Blue reward:0.85\n", - "step: 255, Red action: do-nothing, Blue reward:0.85\n", - "step: 256, Red action: do-nothing, Blue reward:0.85\n", - "step: 257, Red action: do-nothing, Blue reward:0.85\n", - "step: 258, Red action: do-nothing, Blue reward:0.85\n", - "step: 259, Red action: do-nothing, Blue reward:0.85\n", - "step: 260, Red action: do-nothing, Blue reward:0.85\n", - "step: 261, Red action: node-application-execute, Blue reward:0.85\n", - "step: 262, Red action: do-nothing, Blue reward:0.85\n", - "step: 263, Red action: do-nothing, Blue reward:0.85\n", - "step: 264, Red action: do-nothing, Blue reward:0.85\n", - "step: 265, Red action: do-nothing, Blue reward:0.85\n", - "step: 266, Red action: do-nothing, Blue reward:0.85\n", - "step: 267, Red action: do-nothing, Blue reward:0.85\n", - "step: 268, Red action: do-nothing, Blue reward:0.85\n", - "step: 269, Red action: do-nothing, Blue reward:0.85\n", - "step: 270, Red action: do-nothing, Blue reward:0.85\n", - "step: 271, Red action: do-nothing, Blue reward:0.85\n", - "step: 272, Red action: do-nothing, Blue reward:0.85\n", - "step: 273, Red action: do-nothing, Blue reward:0.85\n", - "step: 274, Red action: do-nothing, Blue reward:0.85\n", - "step: 275, Red action: do-nothing, Blue reward:0.85\n", - "step: 276, Red action: do-nothing, Blue reward:0.85\n", - "step: 277, Red action: do-nothing, Blue reward:0.85\n", - "step: 278, Red action: do-nothing, Blue reward:0.85\n", - "step: 279, Red action: do-nothing, Blue reward:0.85\n", - "step: 280, Red action: do-nothing, Blue reward:0.85\n", - "step: 281, Red action: node-application-execute, Blue reward:0.85\n", - "step: 282, Red action: do-nothing, Blue reward:0.85\n", - "step: 283, Red action: do-nothing, Blue reward:0.85\n", - "step: 284, Red action: do-nothing, Blue reward:0.85\n", - "step: 285, Red action: do-nothing, Blue reward:0.85\n", - "step: 286, Red action: do-nothing, Blue reward:0.85\n", - "step: 287, Red action: do-nothing, Blue reward:0.85\n", - "step: 288, Red action: do-nothing, Blue reward:0.85\n", - "step: 289, Red action: do-nothing, Blue reward:0.85\n", - "step: 290, Red action: do-nothing, Blue reward:0.85\n", - "step: 291, Red action: do-nothing, Blue reward:0.85\n", - "step: 292, Red action: do-nothing, Blue reward:0.85\n", - "step: 293, Red action: do-nothing, Blue reward:0.85\n", - "step: 294, Red action: do-nothing, Blue reward:0.85\n", - "step: 295, Red action: do-nothing, Blue reward:0.85\n", - "step: 296, Red action: do-nothing, Blue reward:0.85\n", - "step: 297, Red action: do-nothing, Blue reward:0.85\n", - "step: 298, Red action: do-nothing, Blue reward:0.85\n", - "step: 299, Red action: do-nothing, Blue reward:0.85\n", - "step: 300, Red action: do-nothing, Blue reward:0.85\n", - "step: 301, Red action: do-nothing, Blue reward:0.85\n", - "step: 302, Red action: do-nothing, Blue reward:0.85\n", - "step: 303, Red action: do-nothing, Blue reward:0.85\n", - "step: 304, Red action: node-application-execute, Blue reward:0.85\n", - "step: 305, Red action: do-nothing, Blue reward:0.85\n", - "step: 306, Red action: do-nothing, Blue reward:0.85\n", - "step: 307, Red action: do-nothing, Blue reward:0.85\n", - "step: 308, Red action: do-nothing, Blue reward:0.85\n", - "step: 309, Red action: do-nothing, Blue reward:0.85\n", - "step: 310, Red action: do-nothing, Blue reward:0.85\n", - "step: 311, Red action: do-nothing, Blue reward:0.85\n", - "step: 312, Red action: do-nothing, Blue reward:0.85\n", - "step: 313, Red action: do-nothing, Blue reward:0.85\n", - "step: 314, Red action: do-nothing, Blue reward:0.85\n", - "step: 315, Red action: do-nothing, Blue reward:0.85\n", - "step: 316, Red action: do-nothing, Blue reward:0.85\n", - "step: 317, Red action: do-nothing, Blue reward:0.85\n", - "step: 318, Red action: do-nothing, Blue reward:0.85\n", - "step: 319, Red action: do-nothing, Blue reward:0.85\n", - "step: 320, Red action: do-nothing, Blue reward:0.85\n", - "step: 321, Red action: do-nothing, Blue reward:0.85\n", - "step: 322, Red action: do-nothing, Blue reward:0.85\n", - "step: 323, Red action: do-nothing, Blue reward:0.85\n", - "step: 324, Red action: do-nothing, Blue reward:0.85\n", - "step: 325, Red action: node-application-execute, Blue reward:0.85\n", - "step: 326, Red action: do-nothing, Blue reward:0.85\n", - "step: 327, Red action: do-nothing, Blue reward:0.85\n", - "step: 328, Red action: do-nothing, Blue reward:0.85\n", - "step: 329, Red action: do-nothing, Blue reward:0.85\n", - "step: 330, Red action: do-nothing, Blue reward:0.85\n", - "step: 331, Red action: do-nothing, Blue reward:0.85\n", - "step: 332, Red action: do-nothing, Blue reward:0.85\n", - "step: 333, Red action: do-nothing, Blue reward:0.85\n", - "step: 334, Red action: do-nothing, Blue reward:0.85\n", - "step: 335, Red action: do-nothing, Blue reward:0.85\n", - "step: 336, Red action: do-nothing, Blue reward:0.85\n", - "step: 337, Red action: do-nothing, Blue reward:0.85\n", - "step: 338, Red action: do-nothing, Blue reward:0.85\n", - "step: 339, Red action: do-nothing, Blue reward:0.85\n", - "step: 340, Red action: do-nothing, Blue reward:0.85\n", - "step: 341, Red action: do-nothing, Blue reward:0.85\n", - "step: 342, Red action: do-nothing, Blue reward:0.85\n", - "step: 343, Red action: do-nothing, Blue reward:0.85\n", - "step: 344, Red action: do-nothing, Blue reward:0.85\n", - "step: 345, Red action: do-nothing, Blue reward:0.85\n", - "step: 346, Red action: do-nothing, Blue reward:0.85\n", - "step: 347, Red action: do-nothing, Blue reward:0.85\n", - "step: 348, Red action: do-nothing, Blue reward:0.85\n", - "step: 349, Red action: node-application-execute, Blue reward:0.85\n", - "step: 350, Red action: do-nothing, Blue reward:0.85\n", - "step: 351, Red action: do-nothing, Blue reward:0.85\n", - "step: 352, Red action: do-nothing, Blue reward:0.85\n", - "step: 353, Red action: do-nothing, Blue reward:0.85\n", - "step: 354, Red action: do-nothing, Blue reward:0.85\n", - "step: 355, Red action: do-nothing, Blue reward:0.85\n", - "step: 356, Red action: do-nothing, Blue reward:0.85\n", - "step: 357, Red action: do-nothing, Blue reward:0.85\n", - "step: 358, Red action: do-nothing, Blue reward:0.85\n", - "step: 359, Red action: do-nothing, Blue reward:0.85\n", - "step: 360, Red action: do-nothing, Blue reward:0.85\n", - "step: 361, Red action: do-nothing, Blue reward:0.85\n", - "step: 362, Red action: do-nothing, Blue reward:0.85\n", - "step: 363, Red action: do-nothing, Blue reward:0.85\n", - "step: 364, Red action: do-nothing, Blue reward:0.85\n", - "step: 365, Red action: do-nothing, Blue reward:0.85\n", - "step: 366, Red action: do-nothing, Blue reward:0.85\n", - "step: 367, Red action: do-nothing, Blue reward:0.85\n", - "step: 368, Red action: do-nothing, Blue reward:0.85\n", - "step: 369, Red action: node-application-execute, Blue reward:0.85\n", - "step: 370, Red action: do-nothing, Blue reward:0.85\n", - "step: 371, Red action: do-nothing, Blue reward:0.85\n", - "step: 372, Red action: do-nothing, Blue reward:0.85\n", - "step: 373, Red action: do-nothing, Blue reward:0.85\n", - "step: 374, Red action: do-nothing, Blue reward:0.85\n", - "step: 375, Red action: do-nothing, Blue reward:0.85\n", - "step: 376, Red action: do-nothing, Blue reward:0.85\n", - "step: 377, Red action: do-nothing, Blue reward:0.85\n", - "step: 378, Red action: do-nothing, Blue reward:0.85\n", - "step: 379, Red action: do-nothing, Blue reward:0.85\n", - "step: 380, Red action: do-nothing, Blue reward:0.85\n", - "step: 381, Red action: do-nothing, Blue reward:0.85\n", - "step: 382, Red action: do-nothing, Blue reward:0.85\n", - "step: 383, Red action: do-nothing, Blue reward:0.85\n", - "step: 384, Red action: do-nothing, Blue reward:0.85\n", - "step: 385, Red action: do-nothing, Blue reward:0.85\n", - "step: 386, Red action: do-nothing, Blue reward:0.85\n", - "step: 387, Red action: do-nothing, Blue reward:0.85\n", - "step: 388, Red action: do-nothing, Blue reward:0.85\n", - "step: 389, Red action: do-nothing, Blue reward:0.85\n", - "step: 390, Red action: do-nothing, Blue reward:0.85\n", - "step: 391, Red action: do-nothing, Blue reward:0.85\n", - "step: 392, Red action: do-nothing, Blue reward:0.85\n", - "step: 393, Red action: do-nothing, Blue reward:0.85\n", - "step: 394, Red action: node-application-execute, Blue reward:0.85\n", - "step: 395, Red action: do-nothing, Blue reward:0.85\n", - "step: 396, Red action: do-nothing, Blue reward:0.85\n", - "step: 397, Red action: do-nothing, Blue reward:0.85\n", - "step: 398, Red action: do-nothing, Blue reward:0.85\n", - "step: 399, Red action: do-nothing, Blue reward:0.85\n", - "step: 400, Red action: do-nothing, Blue reward:0.85\n", - "step: 401, Red action: do-nothing, Blue reward:0.85\n", - "step: 402, Red action: do-nothing, Blue reward:0.85\n", - "step: 403, Red action: do-nothing, Blue reward:0.85\n", - "step: 404, Red action: do-nothing, Blue reward:0.85\n", - "step: 405, Red action: do-nothing, Blue reward:0.85\n", - "step: 406, Red action: do-nothing, Blue reward:0.85\n", - "step: 407, Red action: do-nothing, Blue reward:0.85\n", - "step: 408, Red action: do-nothing, Blue reward:0.85\n", - "step: 409, Red action: do-nothing, Blue reward:0.85\n", - "step: 410, Red action: do-nothing, Blue reward:0.85\n", - "step: 411, Red action: do-nothing, Blue reward:0.85\n", - "step: 412, Red action: do-nothing, Blue reward:0.85\n", - "step: 413, Red action: do-nothing, Blue reward:0.85\n", - "step: 414, Red action: do-nothing, Blue reward:0.85\n", - "step: 415, Red action: do-nothing, Blue reward:0.85\n", - "step: 416, Red action: do-nothing, Blue reward:0.85\n", - "step: 417, Red action: do-nothing, Blue reward:0.85\n", - "step: 418, Red action: node-application-execute, Blue reward:0.85\n", - "step: 419, Red action: do-nothing, Blue reward:0.85\n", - "step: 420, Red action: do-nothing, Blue reward:0.85\n", - "step: 421, Red action: do-nothing, Blue reward:0.85\n", - "step: 422, Red action: do-nothing, Blue reward:0.85\n", - "step: 423, Red action: do-nothing, Blue reward:0.85\n", - "step: 424, Red action: do-nothing, Blue reward:0.85\n", - "step: 425, Red action: do-nothing, Blue reward:0.85\n", - "step: 426, Red action: do-nothing, Blue reward:0.85\n", - "step: 427, Red action: do-nothing, Blue reward:0.85\n", - "step: 428, Red action: do-nothing, Blue reward:0.85\n", - "step: 429, Red action: do-nothing, Blue reward:0.85\n", - "step: 430, Red action: do-nothing, Blue reward:0.85\n", - "step: 431, Red action: do-nothing, Blue reward:0.85\n", - "step: 432, Red action: do-nothing, Blue reward:0.85\n", - "step: 433, Red action: do-nothing, Blue reward:0.85\n", - "step: 434, Red action: do-nothing, Blue reward:0.85\n", - "step: 435, Red action: do-nothing, Blue reward:0.85\n", - "step: 436, Red action: do-nothing, Blue reward:0.85\n", - "step: 437, Red action: do-nothing, Blue reward:0.85\n", - "step: 438, Red action: do-nothing, Blue reward:0.85\n", - "step: 439, Red action: do-nothing, Blue reward:0.85\n", - "step: 440, Red action: do-nothing, Blue reward:0.85\n", - "step: 441, Red action: do-nothing, Blue reward:0.85\n", - "step: 442, Red action: do-nothing, Blue reward:0.85\n", - "step: 443, Red action: node-application-execute, Blue reward:0.85\n", - "step: 444, Red action: do-nothing, Blue reward:0.85\n", - "step: 445, Red action: do-nothing, Blue reward:0.85\n", - "step: 446, Red action: do-nothing, Blue reward:0.85\n", - "step: 447, Red action: do-nothing, Blue reward:0.85\n", - "step: 448, Red action: do-nothing, Blue reward:0.85\n", - "step: 449, Red action: do-nothing, Blue reward:0.85\n", - "step: 450, Red action: do-nothing, Blue reward:0.85\n", - "step: 451, Red action: do-nothing, Blue reward:0.85\n", - "step: 452, Red action: do-nothing, Blue reward:0.85\n", - "step: 453, Red action: do-nothing, Blue reward:0.85\n", - "step: 454, Red action: do-nothing, Blue reward:0.85\n", - "step: 455, Red action: do-nothing, Blue reward:0.85\n", - "step: 456, Red action: do-nothing, Blue reward:0.85\n", - "step: 457, Red action: do-nothing, Blue reward:0.85\n", - "step: 458, Red action: do-nothing, Blue reward:0.85\n", - "step: 459, Red action: do-nothing, Blue reward:0.85\n", - "step: 460, Red action: do-nothing, Blue reward:0.85\n", - "step: 461, Red action: node-application-execute, Blue reward:0.85\n", - "step: 462, Red action: do-nothing, Blue reward:0.85\n", - "step: 463, Red action: do-nothing, Blue reward:0.85\n", - "step: 464, Red action: do-nothing, Blue reward:0.85\n", - "step: 465, Red action: do-nothing, Blue reward:0.85\n", - "step: 466, Red action: do-nothing, Blue reward:0.85\n", - "step: 467, Red action: do-nothing, Blue reward:0.85\n", - "step: 468, Red action: do-nothing, Blue reward:0.85\n", - "step: 469, Red action: do-nothing, Blue reward:0.85\n", - "step: 470, Red action: do-nothing, Blue reward:0.85\n", - "step: 471, Red action: do-nothing, Blue reward:0.85\n", - "step: 472, Red action: do-nothing, Blue reward:0.85\n", - "step: 473, Red action: do-nothing, Blue reward:0.85\n", - "step: 474, Red action: do-nothing, Blue reward:0.85\n", - "step: 475, Red action: do-nothing, Blue reward:0.85\n", - "step: 476, Red action: do-nothing, Blue reward:0.85\n", - "step: 477, Red action: do-nothing, Blue reward:0.85\n", - "step: 478, Red action: do-nothing, Blue reward:0.85\n", - "step: 479, Red action: do-nothing, Blue reward:0.85\n", - "step: 480, Red action: node-application-execute, Blue reward:0.85\n", - "step: 481, Red action: do-nothing, Blue reward:0.85\n", - "step: 482, Red action: do-nothing, Blue reward:0.85\n", - "step: 483, Red action: do-nothing, Blue reward:0.85\n", - "step: 484, Red action: do-nothing, Blue reward:0.85\n", - "step: 485, Red action: do-nothing, Blue reward:0.85\n", - "step: 486, Red action: do-nothing, Blue reward:0.85\n", - "step: 487, Red action: do-nothing, Blue reward:0.85\n", - "step: 488, Red action: do-nothing, Blue reward:0.85\n", - "step: 489, Red action: do-nothing, Blue reward:0.85\n", - "step: 490, Red action: do-nothing, Blue reward:0.85\n", - "step: 491, Red action: do-nothing, Blue reward:0.85\n", - "step: 492, Red action: do-nothing, Blue reward:0.85\n", - "step: 493, Red action: do-nothing, Blue reward:0.85\n", - "step: 494, Red action: do-nothing, Blue reward:0.85\n", - "step: 495, Red action: do-nothing, Blue reward:0.85\n", - "step: 496, Red action: do-nothing, Blue reward:0.85\n", - "step: 497, Red action: do-nothing, Blue reward:0.85\n", - "step: 498, Red action: do-nothing, Blue reward:0.85\n", - "step: 499, Red action: do-nothing, Blue reward:0.85\n", - "step: 500, Red action: do-nothing, Blue reward:0.85\n", - "step: 501, Red action: do-nothing, Blue reward:0.85\n", - "step: 502, Red action: do-nothing, Blue reward:0.85\n", - "step: 503, Red action: do-nothing, Blue reward:0.85\n", - "step: 504, Red action: node-application-execute, Blue reward:0.85\n", - "step: 505, Red action: do-nothing, Blue reward:0.85\n", - "step: 506, Red action: do-nothing, Blue reward:0.85\n", - "step: 507, Red action: do-nothing, Blue reward:0.85\n", - "step: 508, Red action: do-nothing, Blue reward:0.85\n", - "step: 509, Red action: do-nothing, Blue reward:0.85\n", - "step: 510, Red action: do-nothing, Blue reward:0.85\n", - "step: 511, Red action: do-nothing, Blue reward:0.85\n", - "step: 512, Red action: do-nothing, Blue reward:0.85\n", - "step: 513, Red action: do-nothing, Blue reward:0.85\n", - "step: 514, Red action: do-nothing, Blue reward:0.85\n", - "step: 515, Red action: do-nothing, Blue reward:0.85\n", - "step: 516, Red action: do-nothing, Blue reward:0.85\n", - "step: 517, Red action: do-nothing, Blue reward:0.85\n", - "step: 518, Red action: do-nothing, Blue reward:0.85\n", - "step: 519, Red action: do-nothing, Blue reward:0.85\n", - "step: 520, Red action: do-nothing, Blue reward:0.85\n", - "step: 521, Red action: do-nothing, Blue reward:0.85\n", - "step: 522, Red action: do-nothing, Blue reward:0.85\n", - "step: 523, Red action: do-nothing, Blue reward:0.85\n", - "step: 524, Red action: do-nothing, Blue reward:0.85\n", - "step: 525, Red action: do-nothing, Blue reward:0.85\n", - "step: 526, Red action: do-nothing, Blue reward:0.85\n", - "step: 527, Red action: node-application-execute, Blue reward:0.85\n", - "step: 528, Red action: do-nothing, Blue reward:0.85\n", - "step: 529, Red action: do-nothing, Blue reward:0.85\n", - "step: 530, Red action: do-nothing, Blue reward:0.85\n", - "step: 531, Red action: do-nothing, Blue reward:0.85\n", - "step: 532, Red action: do-nothing, Blue reward:0.85\n", - "step: 533, Red action: do-nothing, Blue reward:0.85\n", - "step: 534, Red action: do-nothing, Blue reward:0.85\n", - "step: 535, Red action: do-nothing, Blue reward:0.85\n", - "step: 536, Red action: do-nothing, Blue reward:0.85\n", - "step: 537, Red action: do-nothing, Blue reward:0.85\n", - "step: 538, Red action: do-nothing, Blue reward:0.85\n", - "step: 539, Red action: do-nothing, Blue reward:0.85\n", - "step: 540, Red action: do-nothing, Blue reward:0.85\n", - "step: 541, Red action: do-nothing, Blue reward:0.85\n", - "step: 542, Red action: do-nothing, Blue reward:0.85\n", - "step: 543, Red action: do-nothing, Blue reward:0.85\n", - "step: 544, Red action: do-nothing, Blue reward:0.85\n", - "step: 545, Red action: do-nothing, Blue reward:0.85\n", - "step: 546, Red action: do-nothing, Blue reward:0.85\n", - "step: 547, Red action: do-nothing, Blue reward:0.85\n", - "step: 548, Red action: do-nothing, Blue reward:0.85\n", - "step: 549, Red action: node-application-execute, Blue reward:0.85\n", - "step: 550, Red action: do-nothing, Blue reward:0.85\n", - "step: 551, Red action: do-nothing, Blue reward:0.85\n", - "step: 552, Red action: do-nothing, Blue reward:0.85\n", - "step: 553, Red action: do-nothing, Blue reward:0.85\n", - "step: 554, Red action: do-nothing, Blue reward:0.85\n", - "step: 555, Red action: do-nothing, Blue reward:0.85\n", - "step: 556, Red action: do-nothing, Blue reward:0.85\n", - "step: 557, Red action: do-nothing, Blue reward:0.85\n", - "step: 558, Red action: do-nothing, Blue reward:0.85\n", - "step: 559, Red action: do-nothing, Blue reward:0.85\n", - "step: 560, Red action: do-nothing, Blue reward:0.85\n", - "step: 561, Red action: do-nothing, Blue reward:0.85\n", - "step: 562, Red action: do-nothing, Blue reward:0.85\n", - "step: 563, Red action: do-nothing, Blue reward:0.85\n", - "step: 564, Red action: do-nothing, Blue reward:0.85\n", - "step: 565, Red action: node-application-execute, Blue reward:0.85\n", - "step: 566, Red action: do-nothing, Blue reward:0.85\n", - "step: 567, Red action: do-nothing, Blue reward:0.85\n", - "step: 568, Red action: do-nothing, Blue reward:0.85\n", - "step: 569, Red action: do-nothing, Blue reward:0.85\n", - "step: 570, Red action: do-nothing, Blue reward:0.85\n", - "step: 571, Red action: do-nothing, Blue reward:0.85\n", - "step: 572, Red action: do-nothing, Blue reward:0.85\n", - "step: 573, Red action: do-nothing, Blue reward:0.85\n", - "step: 574, Red action: do-nothing, Blue reward:0.85\n", - "step: 575, Red action: do-nothing, Blue reward:0.85\n", - "step: 576, Red action: do-nothing, Blue reward:0.85\n", - "step: 577, Red action: do-nothing, Blue reward:0.85\n", - "step: 578, Red action: do-nothing, Blue reward:0.85\n", - "step: 579, Red action: do-nothing, Blue reward:0.85\n", - "step: 580, Red action: do-nothing, Blue reward:0.85\n", - "step: 581, Red action: do-nothing, Blue reward:0.85\n", - "step: 582, Red action: do-nothing, Blue reward:0.85\n", - "step: 583, Red action: do-nothing, Blue reward:0.85\n", - "step: 584, Red action: do-nothing, Blue reward:0.85\n", - "step: 585, Red action: do-nothing, Blue reward:0.85\n", - "step: 586, Red action: do-nothing, Blue reward:0.85\n", - "step: 587, Red action: do-nothing, Blue reward:0.85\n", - "step: 588, Red action: do-nothing, Blue reward:0.85\n", - "step: 589, Red action: node-application-execute, Blue reward:0.85\n", - "step: 590, Red action: do-nothing, Blue reward:0.85\n", - "step: 591, Red action: do-nothing, Blue reward:0.85\n", - "step: 592, Red action: do-nothing, Blue reward:0.85\n", - "step: 593, Red action: do-nothing, Blue reward:0.85\n", - "step: 594, Red action: do-nothing, Blue reward:0.85\n", - "step: 595, Red action: do-nothing, Blue reward:0.85\n", - "step: 596, Red action: do-nothing, Blue reward:0.85\n", - "step: 597, Red action: do-nothing, Blue reward:0.85\n", - "step: 598, Red action: do-nothing, Blue reward:0.85\n", - "step: 599, Red action: do-nothing, Blue reward:0.85\n", - "step: 600, Red action: do-nothing, Blue reward:0.85\n", - "step: 601, Red action: do-nothing, Blue reward:0.85\n", - "step: 602, Red action: do-nothing, Blue reward:0.85\n", - "step: 603, Red action: do-nothing, Blue reward:0.85\n", - "step: 604, Red action: do-nothing, Blue reward:0.85\n", - "step: 605, Red action: do-nothing, Blue reward:0.85\n", - "step: 606, Red action: do-nothing, Blue reward:0.85\n", - "step: 607, Red action: do-nothing, Blue reward:0.85\n", - "step: 608, Red action: do-nothing, Blue reward:0.85\n", - "step: 609, Red action: do-nothing, Blue reward:0.85\n", - "step: 610, Red action: node-application-execute, Blue reward:0.85\n", - "step: 611, Red action: do-nothing, Blue reward:0.85\n", - "step: 612, Red action: do-nothing, Blue reward:0.85\n", - "step: 613, Red action: do-nothing, Blue reward:0.85\n", - "step: 614, Red action: do-nothing, Blue reward:0.85\n", - "step: 615, Red action: do-nothing, Blue reward:0.85\n", - "step: 616, Red action: do-nothing, Blue reward:0.85\n", - "step: 617, Red action: do-nothing, Blue reward:0.85\n", - "step: 618, Red action: do-nothing, Blue reward:0.85\n", - "step: 619, Red action: do-nothing, Blue reward:0.85\n", - "step: 620, Red action: do-nothing, Blue reward:0.85\n", - "step: 621, Red action: do-nothing, Blue reward:0.85\n", - "step: 622, Red action: do-nothing, Blue reward:0.85\n", - "step: 623, Red action: do-nothing, Blue reward:0.85\n", - "step: 624, Red action: do-nothing, Blue reward:0.85\n", - "step: 625, Red action: do-nothing, Blue reward:0.85\n", - "step: 626, Red action: node-application-execute, Blue reward:0.85\n", - "step: 627, Red action: do-nothing, Blue reward:0.85\n", - "step: 628, Red action: do-nothing, Blue reward:0.85\n", - "step: 629, Red action: do-nothing, Blue reward:0.85\n", - "step: 630, Red action: do-nothing, Blue reward:0.85\n", - "step: 631, Red action: do-nothing, Blue reward:0.85\n", - "step: 632, Red action: do-nothing, Blue reward:0.85\n", - "step: 633, Red action: do-nothing, Blue reward:0.85\n", - "step: 634, Red action: do-nothing, Blue reward:0.85\n", - "step: 635, Red action: do-nothing, Blue reward:0.85\n", - "step: 636, Red action: do-nothing, Blue reward:0.85\n", - "step: 637, Red action: do-nothing, Blue reward:0.85\n", - "step: 638, Red action: do-nothing, Blue reward:0.85\n", - "step: 639, Red action: do-nothing, Blue reward:0.85\n", - "step: 640, Red action: do-nothing, Blue reward:0.85\n", - "step: 641, Red action: do-nothing, Blue reward:0.85\n", - "step: 642, Red action: do-nothing, Blue reward:0.85\n", - "step: 643, Red action: do-nothing, Blue reward:0.85\n", - "step: 644, Red action: node-application-execute, Blue reward:0.85\n", - "step: 645, Red action: do-nothing, Blue reward:0.85\n", - "step: 646, Red action: do-nothing, Blue reward:0.85\n", - "step: 647, Red action: do-nothing, Blue reward:0.85\n", - "step: 648, Red action: do-nothing, Blue reward:0.85\n", - "step: 649, Red action: do-nothing, Blue reward:0.85\n", - "step: 650, Red action: do-nothing, Blue reward:0.85\n", - "step: 651, Red action: do-nothing, Blue reward:0.85\n", - "step: 652, Red action: do-nothing, Blue reward:0.85\n", - "step: 653, Red action: do-nothing, Blue reward:0.85\n", - "step: 654, Red action: do-nothing, Blue reward:0.85\n", - "step: 655, Red action: do-nothing, Blue reward:0.85\n", - "step: 656, Red action: do-nothing, Blue reward:0.85\n", - "step: 657, Red action: do-nothing, Blue reward:0.85\n", - "step: 658, Red action: do-nothing, Blue reward:0.85\n", - "step: 659, Red action: do-nothing, Blue reward:0.85\n", - "step: 660, Red action: do-nothing, Blue reward:0.85\n", - "step: 661, Red action: do-nothing, Blue reward:0.85\n", - "step: 662, Red action: do-nothing, Blue reward:0.85\n", - "step: 663, Red action: do-nothing, Blue reward:0.85\n", - "step: 664, Red action: do-nothing, Blue reward:0.85\n", - "step: 665, Red action: do-nothing, Blue reward:0.85\n", - "step: 666, Red action: do-nothing, Blue reward:0.85\n", - "step: 667, Red action: node-application-execute, Blue reward:0.85\n", - "step: 668, Red action: do-nothing, Blue reward:0.85\n", - "step: 669, Red action: do-nothing, Blue reward:0.85\n", - "step: 670, Red action: do-nothing, Blue reward:0.85\n", - "step: 671, Red action: do-nothing, Blue reward:0.85\n", - "step: 672, Red action: do-nothing, Blue reward:0.85\n", - "step: 673, Red action: do-nothing, Blue reward:0.85\n", - "step: 674, Red action: do-nothing, Blue reward:0.85\n", - "step: 675, Red action: do-nothing, Blue reward:0.85\n", - "step: 676, Red action: do-nothing, Blue reward:0.85\n", - "step: 677, Red action: do-nothing, Blue reward:0.85\n", - "step: 678, Red action: do-nothing, Blue reward:0.85\n", - "step: 679, Red action: do-nothing, Blue reward:0.85\n", - "step: 680, Red action: do-nothing, Blue reward:0.85\n", - "step: 681, Red action: do-nothing, Blue reward:0.85\n", - "step: 682, Red action: do-nothing, Blue reward:0.85\n", - "step: 683, Red action: do-nothing, Blue reward:0.85\n", - "step: 684, Red action: do-nothing, Blue reward:0.85\n", - "step: 685, Red action: node-application-execute, Blue reward:0.85\n", - "step: 686, Red action: do-nothing, Blue reward:0.85\n", - "step: 687, Red action: do-nothing, Blue reward:0.85\n", - "step: 688, Red action: do-nothing, Blue reward:0.85\n", - "step: 689, Red action: do-nothing, Blue reward:0.85\n", - "step: 690, Red action: do-nothing, Blue reward:0.85\n", - "step: 691, Red action: do-nothing, Blue reward:0.85\n", - "step: 692, Red action: do-nothing, Blue reward:0.85\n", - "step: 693, Red action: do-nothing, Blue reward:0.85\n", - "step: 694, Red action: do-nothing, Blue reward:0.85\n", - "step: 695, Red action: do-nothing, Blue reward:0.85\n", - "step: 696, Red action: do-nothing, Blue reward:0.85\n", - "step: 697, Red action: do-nothing, Blue reward:0.85\n", - "step: 698, Red action: do-nothing, Blue reward:0.85\n", - "step: 699, Red action: do-nothing, Blue reward:0.85\n", - "step: 700, Red action: do-nothing, Blue reward:0.85\n", - "step: 701, Red action: node-application-execute, Blue reward:0.85\n", - "step: 702, Red action: do-nothing, Blue reward:0.85\n", - "step: 703, Red action: do-nothing, Blue reward:0.85\n", - "step: 704, Red action: do-nothing, Blue reward:0.85\n", - "step: 705, Red action: do-nothing, Blue reward:0.85\n", - "step: 706, Red action: do-nothing, Blue reward:0.85\n", - "step: 707, Red action: do-nothing, Blue reward:0.85\n", - "step: 708, Red action: do-nothing, Blue reward:0.85\n", - "step: 709, Red action: do-nothing, Blue reward:0.85\n", - "step: 710, Red action: do-nothing, Blue reward:0.85\n", - "step: 711, Red action: do-nothing, Blue reward:0.85\n", - "step: 712, Red action: do-nothing, Blue reward:0.85\n", - "step: 713, Red action: do-nothing, Blue reward:0.85\n", - "step: 714, Red action: do-nothing, Blue reward:0.85\n", - "step: 715, Red action: do-nothing, Blue reward:0.85\n", - "step: 716, Red action: do-nothing, Blue reward:0.85\n", - "step: 717, Red action: do-nothing, Blue reward:0.85\n", - "step: 718, Red action: node-application-execute, Blue reward:0.85\n", - "step: 719, Red action: do-nothing, Blue reward:0.85\n", - "step: 720, Red action: do-nothing, Blue reward:0.85\n", - "step: 721, Red action: do-nothing, Blue reward:0.85\n", - "step: 722, Red action: do-nothing, Blue reward:0.85\n", - "step: 723, Red action: do-nothing, Blue reward:0.85\n", - "step: 724, Red action: do-nothing, Blue reward:0.85\n", - "step: 725, Red action: do-nothing, Blue reward:0.85\n", - "step: 726, Red action: do-nothing, Blue reward:0.85\n", - "step: 727, Red action: do-nothing, Blue reward:0.85\n", - "step: 728, Red action: do-nothing, Blue reward:0.85\n", - "step: 729, Red action: do-nothing, Blue reward:0.85\n", - "step: 730, Red action: do-nothing, Blue reward:0.85\n", - "step: 731, Red action: do-nothing, Blue reward:0.85\n", - "step: 732, Red action: do-nothing, Blue reward:0.85\n", - "step: 733, Red action: do-nothing, Blue reward:0.85\n", - "step: 734, Red action: do-nothing, Blue reward:0.85\n", - "step: 735, Red action: do-nothing, Blue reward:0.85\n", - "step: 736, Red action: do-nothing, Blue reward:0.85\n", - "step: 737, Red action: do-nothing, Blue reward:0.85\n", - "step: 738, Red action: do-nothing, Blue reward:0.85\n", - "step: 739, Red action: do-nothing, Blue reward:0.85\n", - "step: 740, Red action: do-nothing, Blue reward:0.85\n", - "step: 741, Red action: do-nothing, Blue reward:0.85\n", - "step: 742, Red action: do-nothing, Blue reward:0.85\n", - "step: 743, Red action: node-application-execute, Blue reward:0.85\n", - "step: 744, Red action: do-nothing, Blue reward:0.85\n", - "step: 745, Red action: do-nothing, Blue reward:0.85\n", - "step: 746, Red action: do-nothing, Blue reward:0.85\n", - "step: 747, Red action: do-nothing, Blue reward:0.85\n", - "step: 748, Red action: do-nothing, Blue reward:0.85\n", - "step: 749, Red action: do-nothing, Blue reward:0.85\n", - "step: 750, Red action: do-nothing, Blue reward:0.85\n", - "step: 751, Red action: do-nothing, Blue reward:0.85\n", - "step: 752, Red action: do-nothing, Blue reward:0.85\n", - "step: 753, Red action: do-nothing, Blue reward:0.85\n", - "step: 754, Red action: do-nothing, Blue reward:0.85\n", - "step: 755, Red action: do-nothing, Blue reward:0.85\n", - "step: 756, Red action: do-nothing, Blue reward:0.85\n", - "step: 757, Red action: do-nothing, Blue reward:0.85\n", - "step: 758, Red action: do-nothing, Blue reward:0.85\n", - "step: 759, Red action: do-nothing, Blue reward:0.85\n", - "step: 760, Red action: node-application-execute, Blue reward:0.85\n", - "step: 761, Red action: do-nothing, Blue reward:0.85\n", - "step: 762, Red action: do-nothing, Blue reward:0.85\n", - "step: 763, Red action: do-nothing, Blue reward:0.85\n", - "step: 764, Red action: do-nothing, Blue reward:0.85\n", - "step: 765, Red action: do-nothing, Blue reward:0.85\n", - "step: 766, Red action: do-nothing, Blue reward:0.85\n", - "step: 767, Red action: do-nothing, Blue reward:0.85\n", - "step: 768, Red action: do-nothing, Blue reward:0.85\n", - "step: 769, Red action: do-nothing, Blue reward:0.85\n", - "step: 770, Red action: do-nothing, Blue reward:0.85\n", - "step: 771, Red action: do-nothing, Blue reward:0.85\n", - "step: 772, Red action: do-nothing, Blue reward:0.85\n", - "step: 773, Red action: do-nothing, Blue reward:0.85\n", - "step: 774, Red action: do-nothing, Blue reward:0.85\n", - "step: 775, Red action: do-nothing, Blue reward:0.85\n", - "step: 776, Red action: do-nothing, Blue reward:0.85\n", - "step: 777, Red action: do-nothing, Blue reward:0.85\n", - "step: 778, Red action: do-nothing, Blue reward:0.85\n", - "step: 779, Red action: do-nothing, Blue reward:0.85\n", - "step: 780, Red action: do-nothing, Blue reward:0.85\n", - "step: 781, Red action: do-nothing, Blue reward:0.85\n", - "step: 782, Red action: do-nothing, Blue reward:0.85\n", - "step: 783, Red action: node-application-execute, Blue reward:0.85\n", - "step: 784, Red action: do-nothing, Blue reward:0.85\n", - "step: 785, Red action: do-nothing, Blue reward:0.85\n", - "step: 786, Red action: do-nothing, Blue reward:0.85\n", - "step: 787, Red action: do-nothing, Blue reward:0.85\n", - "step: 788, Red action: do-nothing, Blue reward:0.85\n", - "step: 789, Red action: do-nothing, Blue reward:0.85\n", - "step: 790, Red action: do-nothing, Blue reward:0.85\n", - "step: 791, Red action: do-nothing, Blue reward:0.85\n", - "step: 792, Red action: do-nothing, Blue reward:0.85\n", - "step: 793, Red action: do-nothing, Blue reward:0.85\n", - "step: 794, Red action: do-nothing, Blue reward:0.85\n", - "step: 795, Red action: do-nothing, Blue reward:0.85\n", - "step: 796, Red action: do-nothing, Blue reward:0.85\n", - "step: 797, Red action: do-nothing, Blue reward:0.85\n", - "step: 798, Red action: do-nothing, Blue reward:0.85\n", - "step: 799, Red action: do-nothing, Blue reward:0.85\n", - "step: 800, Red action: do-nothing, Blue reward:0.85\n", - "step: 801, Red action: do-nothing, Blue reward:0.85\n", - "step: 802, Red action: do-nothing, Blue reward:0.85\n", - "step: 803, Red action: node-application-execute, Blue reward:0.85\n", - "step: 804, Red action: do-nothing, Blue reward:0.85\n", - "step: 805, Red action: do-nothing, Blue reward:0.85\n", - "step: 806, Red action: do-nothing, Blue reward:0.85\n", - "step: 807, Red action: do-nothing, Blue reward:0.85\n", - "step: 808, Red action: do-nothing, Blue reward:0.85\n", - "step: 809, Red action: do-nothing, Blue reward:0.85\n", - "step: 810, Red action: do-nothing, Blue reward:0.85\n", - "step: 811, Red action: do-nothing, Blue reward:0.85\n", - "step: 812, Red action: do-nothing, Blue reward:0.85\n", - "step: 813, Red action: do-nothing, Blue reward:0.85\n", - "step: 814, Red action: do-nothing, Blue reward:0.85\n", - "step: 815, Red action: do-nothing, Blue reward:0.85\n", - "step: 816, Red action: do-nothing, Blue reward:0.85\n", - "step: 817, Red action: do-nothing, Blue reward:0.85\n", - "step: 818, Red action: do-nothing, Blue reward:0.85\n", - "step: 819, Red action: do-nothing, Blue reward:0.85\n", - "step: 820, Red action: do-nothing, Blue reward:0.85\n", - "step: 821, Red action: do-nothing, Blue reward:0.85\n", - "step: 822, Red action: do-nothing, Blue reward:0.85\n", - "step: 823, Red action: do-nothing, Blue reward:0.85\n", - "step: 824, Red action: do-nothing, Blue reward:0.85\n", - "step: 825, Red action: do-nothing, Blue reward:0.85\n", - "step: 826, Red action: do-nothing, Blue reward:0.85\n", - "step: 827, Red action: node-application-execute, Blue reward:0.85\n", - "step: 828, Red action: do-nothing, Blue reward:0.85\n", - "step: 829, Red action: do-nothing, Blue reward:0.85\n", - "step: 830, Red action: do-nothing, Blue reward:0.85\n", - "step: 831, Red action: do-nothing, Blue reward:0.85\n", - "step: 832, Red action: do-nothing, Blue reward:0.85\n", - "step: 833, Red action: do-nothing, Blue reward:0.85\n", - "step: 834, Red action: do-nothing, Blue reward:0.85\n", - "step: 835, Red action: do-nothing, Blue reward:0.85\n", - "step: 836, Red action: do-nothing, Blue reward:0.85\n", - "step: 837, Red action: do-nothing, Blue reward:0.85\n", - "step: 838, Red action: do-nothing, Blue reward:0.85\n", - "step: 839, Red action: do-nothing, Blue reward:0.85\n", - "step: 840, Red action: do-nothing, Blue reward:0.85\n", - "step: 841, Red action: do-nothing, Blue reward:0.85\n", - "step: 842, Red action: do-nothing, Blue reward:0.85\n", - "step: 843, Red action: do-nothing, Blue reward:0.85\n", - "step: 844, Red action: do-nothing, Blue reward:0.85\n", - "step: 845, Red action: do-nothing, Blue reward:0.85\n", - "step: 846, Red action: do-nothing, Blue reward:0.85\n", - "step: 847, Red action: node-application-execute, Blue reward:0.85\n", - "step: 848, Red action: do-nothing, Blue reward:0.85\n", - "step: 849, Red action: do-nothing, Blue reward:0.85\n", - "step: 850, Red action: do-nothing, Blue reward:0.85\n", - "step: 851, Red action: do-nothing, Blue reward:0.85\n", - "step: 852, Red action: do-nothing, Blue reward:0.85\n", - "step: 853, Red action: do-nothing, Blue reward:0.85\n", - "step: 854, Red action: do-nothing, Blue reward:0.85\n", - "step: 855, Red action: do-nothing, Blue reward:0.85\n", - "step: 856, Red action: do-nothing, Blue reward:0.85\n", - "step: 857, Red action: do-nothing, Blue reward:0.85\n", - "step: 858, Red action: do-nothing, Blue reward:0.85\n", - "step: 859, Red action: do-nothing, Blue reward:0.85\n", - "step: 860, Red action: do-nothing, Blue reward:0.85\n", - "step: 861, Red action: do-nothing, Blue reward:0.85\n", - "step: 862, Red action: do-nothing, Blue reward:0.85\n", - "step: 863, Red action: do-nothing, Blue reward:0.85\n", - "step: 864, Red action: do-nothing, Blue reward:0.85\n", - "step: 865, Red action: do-nothing, Blue reward:0.85\n", - "step: 866, Red action: do-nothing, Blue reward:0.85\n", - "step: 867, Red action: do-nothing, Blue reward:0.85\n", - "step: 868, Red action: do-nothing, Blue reward:0.85\n", - "step: 869, Red action: do-nothing, Blue reward:0.85\n", - "step: 870, Red action: do-nothing, Blue reward:0.85\n", - "step: 871, Red action: node-application-execute, Blue reward:0.85\n", - "step: 872, Red action: do-nothing, Blue reward:0.85\n", - "step: 873, Red action: do-nothing, Blue reward:0.85\n", - "step: 874, Red action: do-nothing, Blue reward:0.85\n", - "step: 875, Red action: do-nothing, Blue reward:0.85\n", - "step: 876, Red action: do-nothing, Blue reward:0.85\n", - "step: 877, Red action: do-nothing, Blue reward:0.85\n", - "step: 878, Red action: do-nothing, Blue reward:0.85\n", - "step: 879, Red action: do-nothing, Blue reward:0.85\n", - "step: 880, Red action: do-nothing, Blue reward:0.85\n", - "step: 881, Red action: do-nothing, Blue reward:0.85\n", - "step: 882, Red action: do-nothing, Blue reward:0.85\n", - "step: 883, Red action: do-nothing, Blue reward:0.85\n", - "step: 884, Red action: do-nothing, Blue reward:0.85\n", - "step: 885, Red action: do-nothing, Blue reward:0.85\n", - "step: 886, Red action: do-nothing, Blue reward:0.85\n", - "step: 887, Red action: do-nothing, Blue reward:0.85\n", - "step: 888, Red action: do-nothing, Blue reward:0.85\n", - "step: 889, Red action: do-nothing, Blue reward:0.85\n", - "step: 890, Red action: do-nothing, Blue reward:0.85\n", - "step: 891, Red action: node-application-execute, Blue reward:0.85\n", - "step: 892, Red action: do-nothing, Blue reward:0.85\n", - "step: 893, Red action: do-nothing, Blue reward:0.85\n", - "step: 894, Red action: do-nothing, Blue reward:0.85\n", - "step: 895, Red action: do-nothing, Blue reward:0.85\n", - "step: 896, Red action: do-nothing, Blue reward:0.85\n", - "step: 897, Red action: do-nothing, Blue reward:0.85\n", - "step: 898, Red action: do-nothing, Blue reward:0.85\n", - "step: 899, Red action: do-nothing, Blue reward:0.85\n", - "step: 900, Red action: do-nothing, Blue reward:0.85\n", - "step: 901, Red action: do-nothing, Blue reward:0.85\n", - "step: 902, Red action: do-nothing, Blue reward:0.85\n", - "step: 903, Red action: do-nothing, Blue reward:0.85\n", - "step: 904, Red action: do-nothing, Blue reward:0.85\n", - "step: 905, Red action: do-nothing, Blue reward:0.85\n", - "step: 906, Red action: node-application-execute, Blue reward:0.85\n", - "step: 907, Red action: do-nothing, Blue reward:0.85\n", - "step: 908, Red action: do-nothing, Blue reward:0.85\n", - "step: 909, Red action: do-nothing, Blue reward:0.85\n", - "step: 910, Red action: do-nothing, Blue reward:0.85\n", - "step: 911, Red action: do-nothing, Blue reward:0.85\n", - "step: 912, Red action: do-nothing, Blue reward:0.85\n", - "step: 913, Red action: do-nothing, Blue reward:0.85\n", - "step: 914, Red action: do-nothing, Blue reward:0.85\n", - "step: 915, Red action: do-nothing, Blue reward:0.85\n", - "step: 916, Red action: do-nothing, Blue reward:0.85\n", - "step: 917, Red action: do-nothing, Blue reward:0.85\n", - "step: 918, Red action: do-nothing, Blue reward:0.85\n", - "step: 919, Red action: do-nothing, Blue reward:0.85\n", - "step: 920, Red action: do-nothing, Blue reward:0.85\n", - "step: 921, Red action: do-nothing, Blue reward:0.85\n", - "step: 922, Red action: do-nothing, Blue reward:0.85\n", - "step: 923, Red action: do-nothing, Blue reward:0.85\n", - "step: 924, Red action: do-nothing, Blue reward:0.85\n", - "step: 925, Red action: do-nothing, Blue reward:0.85\n", - "step: 926, Red action: do-nothing, Blue reward:0.85\n", - "step: 927, Red action: do-nothing, Blue reward:0.85\n", - "step: 928, Red action: do-nothing, Blue reward:0.85\n", - "step: 929, Red action: do-nothing, Blue reward:0.85\n", - "step: 930, Red action: node-application-execute, Blue reward:0.85\n", - "step: 931, Red action: do-nothing, Blue reward:0.85\n", - "step: 932, Red action: do-nothing, Blue reward:0.85\n", - "step: 933, Red action: do-nothing, Blue reward:0.85\n", - "step: 934, Red action: do-nothing, Blue reward:0.85\n", - "step: 935, Red action: do-nothing, Blue reward:0.85\n", - "step: 936, Red action: do-nothing, Blue reward:0.85\n", - "step: 937, Red action: do-nothing, Blue reward:0.85\n", - "step: 938, Red action: do-nothing, Blue reward:0.85\n", - "step: 939, Red action: do-nothing, Blue reward:0.85\n", - "step: 940, Red action: do-nothing, Blue reward:0.85\n", - "step: 941, Red action: do-nothing, Blue reward:0.85\n", - "step: 942, Red action: do-nothing, Blue reward:0.85\n", - "step: 943, Red action: do-nothing, Blue reward:0.85\n", - "step: 944, Red action: do-nothing, Blue reward:0.85\n", - "step: 945, Red action: do-nothing, Blue reward:0.85\n", - "step: 946, Red action: do-nothing, Blue reward:0.85\n", - "step: 947, Red action: node-application-execute, Blue reward:0.85\n", - "step: 948, Red action: do-nothing, Blue reward:0.85\n", - "step: 949, Red action: do-nothing, Blue reward:0.85\n", - "step: 950, Red action: do-nothing, Blue reward:0.85\n", - "step: 951, Red action: do-nothing, Blue reward:0.85\n", - "step: 952, Red action: do-nothing, Blue reward:0.85\n", - "step: 953, Red action: do-nothing, Blue reward:0.85\n", - "step: 954, Red action: do-nothing, Blue reward:0.85\n", - "step: 955, Red action: do-nothing, Blue reward:0.85\n", - "step: 956, Red action: do-nothing, Blue reward:0.85\n", - "step: 957, Red action: do-nothing, Blue reward:0.85\n", - "step: 958, Red action: do-nothing, Blue reward:0.85\n", - "step: 959, Red action: do-nothing, Blue reward:0.85\n", - "step: 960, Red action: do-nothing, Blue reward:0.85\n", - "step: 961, Red action: do-nothing, Blue reward:0.85\n", - "step: 962, Red action: do-nothing, Blue reward:0.85\n", - "step: 963, Red action: node-application-execute, Blue reward:0.85\n", - "step: 964, Red action: do-nothing, Blue reward:0.85\n", - "step: 965, Red action: do-nothing, Blue reward:0.85\n", - "step: 966, Red action: do-nothing, Blue reward:0.85\n", - "step: 967, Red action: do-nothing, Blue reward:0.85\n", - "step: 968, Red action: do-nothing, Blue reward:0.85\n", - "step: 969, Red action: do-nothing, Blue reward:0.85\n", - "step: 970, Red action: do-nothing, Blue reward:0.85\n", - "step: 971, Red action: do-nothing, Blue reward:0.85\n", - "step: 972, Red action: do-nothing, Blue reward:0.85\n", - "step: 973, Red action: do-nothing, Blue reward:0.85\n", - "step: 974, Red action: do-nothing, Blue reward:0.85\n", - "step: 975, Red action: do-nothing, Blue reward:0.85\n", - "step: 976, Red action: do-nothing, Blue reward:0.85\n", - "step: 977, Red action: do-nothing, Blue reward:0.85\n", - "step: 978, Red action: do-nothing, Blue reward:0.85\n", - "step: 979, Red action: do-nothing, Blue reward:0.85\n", - "step: 980, Red action: do-nothing, Blue reward:0.85\n", - "step: 981, Red action: do-nothing, Blue reward:0.85\n", - "step: 982, Red action: do-nothing, Blue reward:0.85\n", - "step: 983, Red action: do-nothing, Blue reward:0.85\n", - "step: 984, Red action: do-nothing, Blue reward:0.85\n", - "step: 985, Red action: do-nothing, Blue reward:0.85\n", - "step: 986, Red action: do-nothing, Blue reward:0.85\n", - "step: 987, Red action: do-nothing, Blue reward:0.85\n", - "step: 988, Red action: node-application-execute, Blue reward:0.85\n", - "step: 989, Red action: do-nothing, Blue reward:0.85\n", - "step: 990, Red action: do-nothing, Blue reward:0.85\n", - "step: 991, Red action: do-nothing, Blue reward:0.85\n", - "step: 992, Red action: do-nothing, Blue reward:0.85\n", - "step: 993, Red action: do-nothing, Blue reward:0.85\n", - "step: 994, Red action: do-nothing, Blue reward:0.85\n", - "step: 995, Red action: do-nothing, Blue reward:0.85\n", - "step: 996, Red action: do-nothing, Blue reward:0.85\n", - "step: 997, Red action: do-nothing, Blue reward:0.85\n", - "step: 998, Red action: do-nothing, Blue reward:0.85\n", - "step: 999, Red action: do-nothing, Blue reward:0.85\n", - "step: 1000, Red action: do-nothing, Blue reward:0.85\n", - "step: 1001, Red action: do-nothing, Blue reward:0.85\n", - "step: 1002, Red action: do-nothing, Blue reward:0.85\n", - "step: 1003, Red action: do-nothing, Blue reward:0.85\n", - "step: 1004, Red action: do-nothing, Blue reward:0.85\n", - "step: 1005, Red action: do-nothing, Blue reward:0.85\n", - "step: 1006, Red action: do-nothing, Blue reward:0.85\n", - "step: 1007, Red action: do-nothing, Blue reward:0.85\n", - "step: 1008, Red action: do-nothing, Blue reward:0.85\n", - "step: 1009, Red action: do-nothing, Blue reward:0.85\n", - "step: 1010, Red action: do-nothing, Blue reward:0.85\n", - "step: 1011, Red action: do-nothing, Blue reward:0.85\n", - "step: 1012, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1013, Red action: do-nothing, Blue reward:0.85\n", - "step: 1014, Red action: do-nothing, Blue reward:0.85\n", - "step: 1015, Red action: do-nothing, Blue reward:0.85\n", - "step: 1016, Red action: do-nothing, Blue reward:0.85\n", - "step: 1017, Red action: do-nothing, Blue reward:0.85\n", - "step: 1018, Red action: do-nothing, Blue reward:0.85\n", - "step: 1019, Red action: do-nothing, Blue reward:0.85\n", - "step: 1020, Red action: do-nothing, Blue reward:0.85\n", - "step: 1021, Red action: do-nothing, Blue reward:0.85\n", - "step: 1022, Red action: do-nothing, Blue reward:0.85\n", - "step: 1023, Red action: do-nothing, Blue reward:0.85\n", - "step: 1024, Red action: do-nothing, Blue reward:0.85\n", - "step: 1025, Red action: do-nothing, Blue reward:0.85\n", - "step: 1026, Red action: do-nothing, Blue reward:0.85\n", - "step: 1027, Red action: do-nothing, Blue reward:0.85\n", - "step: 1028, Red action: do-nothing, Blue reward:0.85\n", - "step: 1029, Red action: do-nothing, Blue reward:0.85\n", - "step: 1030, Red action: do-nothing, Blue reward:0.85\n", - "step: 1031, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1032, Red action: do-nothing, Blue reward:0.85\n", - "step: 1033, Red action: do-nothing, Blue reward:0.85\n", - "step: 1034, Red action: do-nothing, Blue reward:0.85\n", - "step: 1035, Red action: do-nothing, Blue reward:0.85\n", - "step: 1036, Red action: do-nothing, Blue reward:0.85\n", - "step: 1037, Red action: do-nothing, Blue reward:0.85\n", - "step: 1038, Red action: do-nothing, Blue reward:0.85\n", - "step: 1039, Red action: do-nothing, Blue reward:0.85\n", - "step: 1040, Red action: do-nothing, Blue reward:0.85\n", - "step: 1041, Red action: do-nothing, Blue reward:0.85\n", - "step: 1042, Red action: do-nothing, Blue reward:0.85\n", - "step: 1043, Red action: do-nothing, Blue reward:0.85\n", - "step: 1044, Red action: do-nothing, Blue reward:0.85\n", - "step: 1045, Red action: do-nothing, Blue reward:0.85\n", - "step: 1046, Red action: do-nothing, Blue reward:0.85\n", - "step: 1047, Red action: do-nothing, Blue reward:0.85\n", - "step: 1048, Red action: do-nothing, Blue reward:0.85\n", - "step: 1049, Red action: do-nothing, Blue reward:0.85\n", - "step: 1050, Red action: do-nothing, Blue reward:0.85\n", - "step: 1051, Red action: do-nothing, Blue reward:0.85\n", - "step: 1052, Red action: do-nothing, Blue reward:0.85\n", - "step: 1053, Red action: do-nothing, Blue reward:0.85\n", - "step: 1054, Red action: do-nothing, Blue reward:0.85\n", - "step: 1055, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1056, Red action: do-nothing, Blue reward:0.85\n", - "step: 1057, Red action: do-nothing, Blue reward:0.85\n", - "step: 1058, Red action: do-nothing, Blue reward:0.85\n", - "step: 1059, Red action: do-nothing, Blue reward:0.85\n", - "step: 1060, Red action: do-nothing, Blue reward:0.85\n", - "step: 1061, Red action: do-nothing, Blue reward:0.85\n", - "step: 1062, Red action: do-nothing, Blue reward:0.85\n", - "step: 1063, Red action: do-nothing, Blue reward:0.85\n", - "step: 1064, Red action: do-nothing, Blue reward:0.85\n", - "step: 1065, Red action: do-nothing, Blue reward:0.85\n", - "step: 1066, Red action: do-nothing, Blue reward:0.85\n", - "step: 1067, Red action: do-nothing, Blue reward:0.85\n", - "step: 1068, Red action: do-nothing, Blue reward:0.85\n", - "step: 1069, Red action: do-nothing, Blue reward:0.85\n", - "step: 1070, Red action: do-nothing, Blue reward:0.85\n", - "step: 1071, Red action: do-nothing, Blue reward:0.85\n", - "step: 1072, Red action: do-nothing, Blue reward:0.85\n", - "step: 1073, Red action: do-nothing, Blue reward:0.85\n", - "step: 1074, Red action: do-nothing, Blue reward:0.85\n", - "step: 1075, Red action: do-nothing, Blue reward:0.85\n", - "step: 1076, Red action: do-nothing, Blue reward:0.85\n", - "step: 1077, Red action: do-nothing, Blue reward:0.85\n", - "step: 1078, Red action: do-nothing, Blue reward:0.85\n", - "step: 1079, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1080, Red action: do-nothing, Blue reward:0.85\n", - "step: 1081, Red action: do-nothing, Blue reward:0.85\n", - "step: 1082, Red action: do-nothing, Blue reward:0.85\n", - "step: 1083, Red action: do-nothing, Blue reward:0.85\n", - "step: 1084, Red action: do-nothing, Blue reward:0.85\n", - "step: 1085, Red action: do-nothing, Blue reward:0.85\n", - "step: 1086, Red action: do-nothing, Blue reward:0.85\n", - "step: 1087, Red action: do-nothing, Blue reward:0.85\n", - "step: 1088, Red action: do-nothing, Blue reward:0.85\n", - "step: 1089, Red action: do-nothing, Blue reward:0.85\n", - "step: 1090, Red action: do-nothing, Blue reward:0.85\n", - "step: 1091, Red action: do-nothing, Blue reward:0.85\n", - "step: 1092, Red action: do-nothing, Blue reward:0.85\n", - "step: 1093, Red action: do-nothing, Blue reward:0.85\n", - "step: 1094, Red action: do-nothing, Blue reward:0.85\n", - "step: 1095, Red action: do-nothing, Blue reward:0.85\n", - "step: 1096, Red action: do-nothing, Blue reward:0.85\n", - "step: 1097, Red action: do-nothing, Blue reward:0.85\n", - "step: 1098, Red action: do-nothing, Blue reward:0.85\n", - "step: 1099, Red action: do-nothing, Blue reward:0.85\n", - "step: 1100, Red action: do-nothing, Blue reward:0.85\n", - "step: 1101, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1102, Red action: do-nothing, Blue reward:0.85\n", - "step: 1103, Red action: do-nothing, Blue reward:0.85\n", - "step: 1104, Red action: do-nothing, Blue reward:0.85\n", - "step: 1105, Red action: do-nothing, Blue reward:0.85\n", - "step: 1106, Red action: do-nothing, Blue reward:0.85\n", - "step: 1107, Red action: do-nothing, Blue reward:0.85\n", - "step: 1108, Red action: do-nothing, Blue reward:0.85\n", - "step: 1109, Red action: do-nothing, Blue reward:0.85\n", - "step: 1110, Red action: do-nothing, Blue reward:0.85\n", - "step: 1111, Red action: do-nothing, Blue reward:0.85\n", - "step: 1112, Red action: do-nothing, Blue reward:0.85\n", - "step: 1113, Red action: do-nothing, Blue reward:0.85\n", - "step: 1114, Red action: do-nothing, Blue reward:0.85\n", - "step: 1115, Red action: do-nothing, Blue reward:0.85\n", - "step: 1116, Red action: do-nothing, Blue reward:0.85\n", - "step: 1117, Red action: do-nothing, Blue reward:0.85\n", - "step: 1118, Red action: do-nothing, Blue reward:0.85\n", - "step: 1119, Red action: do-nothing, Blue reward:0.85\n", - "step: 1120, Red action: do-nothing, Blue reward:0.85\n", - "step: 1121, Red action: do-nothing, Blue reward:0.85\n", - "step: 1122, Red action: do-nothing, Blue reward:0.85\n", - "step: 1123, Red action: do-nothing, Blue reward:0.85\n", - "step: 1124, Red action: do-nothing, Blue reward:0.85\n", - "step: 1125, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1126, Red action: do-nothing, Blue reward:0.85\n", - "step: 1127, Red action: do-nothing, Blue reward:0.85\n", - "step: 1128, Red action: do-nothing, Blue reward:0.85\n", - "step: 1129, Red action: do-nothing, Blue reward:0.85\n", - "step: 1130, Red action: do-nothing, Blue reward:0.85\n", - "step: 1131, Red action: do-nothing, Blue reward:0.85\n", - "step: 1132, Red action: do-nothing, Blue reward:0.85\n", - "step: 1133, Red action: do-nothing, Blue reward:0.85\n", - "step: 1134, Red action: do-nothing, Blue reward:0.85\n", - "step: 1135, Red action: do-nothing, Blue reward:0.85\n", - "step: 1136, Red action: do-nothing, Blue reward:0.85\n", - "step: 1137, Red action: do-nothing, Blue reward:0.85\n", - "step: 1138, Red action: do-nothing, Blue reward:0.85\n", - "step: 1139, Red action: do-nothing, Blue reward:0.85\n", - "step: 1140, Red action: do-nothing, Blue reward:0.85\n", - "step: 1141, Red action: do-nothing, Blue reward:0.85\n", - "step: 1142, Red action: do-nothing, Blue reward:0.85\n", - "step: 1143, Red action: do-nothing, Blue reward:0.85\n", - "step: 1144, Red action: do-nothing, Blue reward:0.85\n", - "step: 1145, Red action: do-nothing, Blue reward:0.85\n", - "step: 1146, Red action: do-nothing, Blue reward:0.85\n", - "step: 1147, Red action: do-nothing, Blue reward:0.85\n", - "step: 1148, Red action: do-nothing, Blue reward:0.85\n", - "step: 1149, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1150, Red action: do-nothing, Blue reward:0.85\n", - "step: 1151, Red action: do-nothing, Blue reward:0.85\n", - "step: 1152, Red action: do-nothing, Blue reward:0.85\n", - "step: 1153, Red action: do-nothing, Blue reward:0.85\n", - "step: 1154, Red action: do-nothing, Blue reward:0.85\n", - "step: 1155, Red action: do-nothing, Blue reward:0.85\n", - "step: 1156, Red action: do-nothing, Blue reward:0.85\n", - "step: 1157, Red action: do-nothing, Blue reward:0.85\n", - "step: 1158, Red action: do-nothing, Blue reward:0.85\n", - "step: 1159, Red action: do-nothing, Blue reward:0.85\n", - "step: 1160, Red action: do-nothing, Blue reward:0.85\n", - "step: 1161, Red action: do-nothing, Blue reward:0.85\n", - "step: 1162, Red action: do-nothing, Blue reward:0.85\n", - "step: 1163, Red action: do-nothing, Blue reward:0.85\n", - "step: 1164, Red action: do-nothing, Blue reward:0.85\n", - "step: 1165, Red action: do-nothing, Blue reward:0.85\n", - "step: 1166, Red action: do-nothing, Blue reward:0.85\n", - "step: 1167, Red action: do-nothing, Blue reward:0.85\n", - "step: 1168, Red action: do-nothing, Blue reward:0.85\n", - "step: 1169, Red action: do-nothing, Blue reward:0.85\n", - "step: 1170, Red action: do-nothing, Blue reward:0.85\n", - "step: 1171, Red action: do-nothing, Blue reward:0.85\n", - "step: 1172, Red action: do-nothing, Blue reward:0.85\n", - "step: 1173, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1174, Red action: do-nothing, Blue reward:0.85\n", - "step: 1175, Red action: do-nothing, Blue reward:0.85\n", - "step: 1176, Red action: do-nothing, Blue reward:0.85\n", - "step: 1177, Red action: do-nothing, Blue reward:0.85\n", - "step: 1178, Red action: do-nothing, Blue reward:0.85\n", - "step: 1179, Red action: do-nothing, Blue reward:0.85\n", - "step: 1180, Red action: do-nothing, Blue reward:0.85\n", - "step: 1181, Red action: do-nothing, Blue reward:0.85\n", - "step: 1182, Red action: do-nothing, Blue reward:0.85\n", - "step: 1183, Red action: do-nothing, Blue reward:0.85\n", - "step: 1184, Red action: do-nothing, Blue reward:0.85\n", - "step: 1185, Red action: do-nothing, Blue reward:0.85\n", - "step: 1186, Red action: do-nothing, Blue reward:0.85\n", - "step: 1187, Red action: do-nothing, Blue reward:0.85\n", - "step: 1188, Red action: do-nothing, Blue reward:0.85\n", - "step: 1189, Red action: do-nothing, Blue reward:0.85\n", - "step: 1190, Red action: do-nothing, Blue reward:0.85\n", - "step: 1191, Red action: do-nothing, Blue reward:0.85\n", - "step: 1192, Red action: do-nothing, Blue reward:0.85\n", - "step: 1193, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1194, Red action: do-nothing, Blue reward:0.85\n", - "step: 1195, Red action: do-nothing, Blue reward:0.85\n", - "step: 1196, Red action: do-nothing, Blue reward:0.85\n", - "step: 1197, Red action: do-nothing, Blue reward:0.85\n", - "step: 1198, Red action: do-nothing, Blue reward:0.85\n", - "step: 1199, Red action: do-nothing, Blue reward:0.85\n", - "step: 1200, Red action: do-nothing, Blue reward:0.85\n", - "step: 1201, Red action: do-nothing, Blue reward:0.85\n", - "step: 1202, Red action: do-nothing, Blue reward:0.85\n", - "step: 1203, Red action: do-nothing, Blue reward:0.85\n", - "step: 1204, Red action: do-nothing, Blue reward:0.85\n", - "step: 1205, Red action: do-nothing, Blue reward:0.85\n", - "step: 1206, Red action: do-nothing, Blue reward:0.85\n", - "step: 1207, Red action: do-nothing, Blue reward:0.85\n", - "step: 1208, Red action: do-nothing, Blue reward:0.85\n", - "step: 1209, Red action: do-nothing, Blue reward:0.85\n", - "step: 1210, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1211, Red action: do-nothing, Blue reward:0.85\n", - "step: 1212, Red action: do-nothing, Blue reward:0.85\n", - "step: 1213, Red action: do-nothing, Blue reward:0.85\n", - "step: 1214, Red action: do-nothing, Blue reward:0.85\n", - "step: 1215, Red action: do-nothing, Blue reward:0.85\n", - "step: 1216, Red action: do-nothing, Blue reward:0.85\n", - "step: 1217, Red action: do-nothing, Blue reward:0.85\n", - "step: 1218, Red action: do-nothing, Blue reward:0.85\n", - "step: 1219, Red action: do-nothing, Blue reward:0.85\n", - "step: 1220, Red action: do-nothing, Blue reward:0.85\n", - "step: 1221, Red action: do-nothing, Blue reward:0.85\n", - "step: 1222, Red action: do-nothing, Blue reward:0.85\n", - "step: 1223, Red action: do-nothing, Blue reward:0.85\n", - "step: 1224, Red action: do-nothing, Blue reward:0.85\n", - "step: 1225, Red action: do-nothing, Blue reward:0.85\n", - "step: 1226, Red action: do-nothing, Blue reward:0.85\n", - "step: 1227, Red action: do-nothing, Blue reward:0.85\n", - "step: 1228, Red action: do-nothing, Blue reward:0.85\n", - "step: 1229, Red action: do-nothing, Blue reward:0.85\n", - "step: 1230, Red action: do-nothing, Blue reward:0.85\n", - "step: 1231, Red action: do-nothing, Blue reward:0.85\n", - "step: 1232, Red action: do-nothing, Blue reward:0.85\n", - "step: 1233, Red action: do-nothing, Blue reward:0.85\n", - "step: 1234, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1235, Red action: do-nothing, Blue reward:0.85\n", - "step: 1236, Red action: do-nothing, Blue reward:0.85\n", - "step: 1237, Red action: do-nothing, Blue reward:0.85\n", - "step: 1238, Red action: do-nothing, Blue reward:0.85\n", - "step: 1239, Red action: do-nothing, Blue reward:0.85\n", - "step: 1240, Red action: do-nothing, Blue reward:0.85\n", - "step: 1241, Red action: do-nothing, Blue reward:0.85\n", - "step: 1242, Red action: do-nothing, Blue reward:0.85\n", - "step: 1243, Red action: do-nothing, Blue reward:0.85\n", - "step: 1244, Red action: do-nothing, Blue reward:0.85\n", - "step: 1245, Red action: do-nothing, Blue reward:0.85\n", - "step: 1246, Red action: do-nothing, Blue reward:0.85\n", - "step: 1247, Red action: do-nothing, Blue reward:0.85\n", - "step: 1248, Red action: do-nothing, Blue reward:0.85\n", - "step: 1249, Red action: do-nothing, Blue reward:0.85\n", - "step: 1250, Red action: do-nothing, Blue reward:0.85\n", - "step: 1251, Red action: do-nothing, Blue reward:0.85\n", - "step: 1252, Red action: do-nothing, Blue reward:0.85\n", - "step: 1253, Red action: do-nothing, Blue reward:0.85\n", - "step: 1254, Red action: do-nothing, Blue reward:0.85\n", - "step: 1255, Red action: do-nothing, Blue reward:0.85\n", - "step: 1256, Red action: do-nothing, Blue reward:0.85\n", - "step: 1257, Red action: do-nothing, Blue reward:0.85\n", - "step: 1258, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1259, Red action: do-nothing, Blue reward:0.85\n", - "step: 1260, Red action: do-nothing, Blue reward:0.85\n", - "step: 1261, Red action: do-nothing, Blue reward:0.85\n", - "step: 1262, Red action: do-nothing, Blue reward:0.85\n", - "step: 1263, Red action: do-nothing, Blue reward:0.85\n", - "step: 1264, Red action: do-nothing, Blue reward:0.85\n", - "step: 1265, Red action: do-nothing, Blue reward:0.85\n", - "step: 1266, Red action: do-nothing, Blue reward:0.85\n", - "step: 1267, Red action: do-nothing, Blue reward:0.85\n", - "step: 1268, Red action: do-nothing, Blue reward:0.85\n", - "step: 1269, Red action: do-nothing, Blue reward:0.85\n", - "step: 1270, Red action: do-nothing, Blue reward:0.85\n", - "step: 1271, Red action: do-nothing, Blue reward:0.85\n", - "step: 1272, Red action: do-nothing, Blue reward:0.85\n", - "step: 1273, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1274, Red action: do-nothing, Blue reward:0.85\n", - "step: 1275, Red action: do-nothing, Blue reward:0.85\n", - "step: 1276, Red action: do-nothing, Blue reward:0.85\n", - "step: 1277, Red action: do-nothing, Blue reward:0.85\n", - "step: 1278, Red action: do-nothing, Blue reward:0.85\n", - "step: 1279, Red action: do-nothing, Blue reward:0.85\n", - "step: 1280, Red action: do-nothing, Blue reward:0.85\n", - "step: 1281, Red action: do-nothing, Blue reward:0.85\n", - "step: 1282, Red action: do-nothing, Blue reward:0.85\n", - "step: 1283, Red action: do-nothing, Blue reward:0.85\n", - "step: 1284, Red action: do-nothing, Blue reward:0.85\n", - "step: 1285, Red action: do-nothing, Blue reward:0.85\n", - "step: 1286, Red action: do-nothing, Blue reward:0.85\n", - "step: 1287, Red action: do-nothing, Blue reward:0.85\n", - "step: 1288, Red action: do-nothing, Blue reward:0.85\n", - "step: 1289, Red action: do-nothing, Blue reward:0.85\n", - "step: 1290, Red action: do-nothing, Blue reward:0.85\n", - "step: 1291, Red action: do-nothing, Blue reward:0.85\n", - "step: 1292, Red action: do-nothing, Blue reward:0.85\n", - "step: 1293, Red action: do-nothing, Blue reward:0.85\n", - "step: 1294, Red action: do-nothing, Blue reward:0.85\n", - "step: 1295, Red action: do-nothing, Blue reward:0.85\n", - "step: 1296, Red action: do-nothing, Blue reward:0.85\n", - "step: 1297, Red action: do-nothing, Blue reward:0.85\n", - "step: 1298, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1299, Red action: do-nothing, Blue reward:0.85\n", - "step: 1300, Red action: do-nothing, Blue reward:0.85\n", - "step: 1301, Red action: do-nothing, Blue reward:0.85\n", - "step: 1302, Red action: do-nothing, Blue reward:0.85\n", - "step: 1303, Red action: do-nothing, Blue reward:0.85\n", - "step: 1304, Red action: do-nothing, Blue reward:0.85\n", - "step: 1305, Red action: do-nothing, Blue reward:0.85\n", - "step: 1306, Red action: do-nothing, Blue reward:0.85\n", - "step: 1307, Red action: do-nothing, Blue reward:0.85\n", - "step: 1308, Red action: do-nothing, Blue reward:0.85\n", - "step: 1309, Red action: do-nothing, Blue reward:0.85\n", - "step: 1310, Red action: do-nothing, Blue reward:0.85\n", - "step: 1311, Red action: do-nothing, Blue reward:0.85\n", - "step: 1312, Red action: do-nothing, Blue reward:0.85\n", - "step: 1313, Red action: do-nothing, Blue reward:0.85\n", - "step: 1314, Red action: do-nothing, Blue reward:0.85\n", - "step: 1315, Red action: do-nothing, Blue reward:0.85\n", - "step: 1316, Red action: do-nothing, Blue reward:0.85\n", - "step: 1317, Red action: do-nothing, Blue reward:0.85\n", - "step: 1318, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1319, Red action: do-nothing, Blue reward:0.85\n", - "step: 1320, Red action: do-nothing, Blue reward:0.85\n", - "step: 1321, Red action: do-nothing, Blue reward:0.85\n", - "step: 1322, Red action: do-nothing, Blue reward:0.85\n", - "step: 1323, Red action: do-nothing, Blue reward:0.85\n", - "step: 1324, Red action: do-nothing, Blue reward:0.85\n", - "step: 1325, Red action: do-nothing, Blue reward:0.85\n", - "step: 1326, Red action: do-nothing, Blue reward:0.85\n", - "step: 1327, Red action: do-nothing, Blue reward:0.85\n", - "step: 1328, Red action: do-nothing, Blue reward:0.85\n", - "step: 1329, Red action: do-nothing, Blue reward:0.85\n", - "step: 1330, Red action: do-nothing, Blue reward:0.85\n", - "step: 1331, Red action: do-nothing, Blue reward:0.85\n", - "step: 1332, Red action: do-nothing, Blue reward:0.85\n", - "step: 1333, Red action: do-nothing, Blue reward:0.85\n", - "step: 1334, Red action: do-nothing, Blue reward:0.85\n", - "step: 1335, Red action: do-nothing, Blue reward:0.85\n", - "step: 1336, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1337, Red action: do-nothing, Blue reward:0.85\n", - "step: 1338, Red action: do-nothing, Blue reward:0.85\n", - "step: 1339, Red action: do-nothing, Blue reward:0.85\n", - "step: 1340, Red action: do-nothing, Blue reward:0.85\n", - "step: 1341, Red action: do-nothing, Blue reward:0.85\n", - "step: 1342, Red action: do-nothing, Blue reward:0.85\n", - "step: 1343, Red action: do-nothing, Blue reward:0.85\n", - "step: 1344, Red action: do-nothing, Blue reward:0.85\n", - "step: 1345, Red action: do-nothing, Blue reward:0.85\n", - "step: 1346, Red action: do-nothing, Blue reward:0.85\n", - "step: 1347, Red action: do-nothing, Blue reward:0.85\n", - "step: 1348, Red action: do-nothing, Blue reward:0.85\n", - "step: 1349, Red action: do-nothing, Blue reward:0.85\n", - "step: 1350, Red action: do-nothing, Blue reward:0.85\n", - "step: 1351, Red action: do-nothing, Blue reward:0.85\n", - "step: 1352, Red action: do-nothing, Blue reward:0.85\n", - "step: 1353, Red action: do-nothing, Blue reward:0.85\n", - "step: 1354, Red action: do-nothing, Blue reward:0.85\n", - "step: 1355, Red action: do-nothing, Blue reward:0.85\n", - "step: 1356, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1357, Red action: do-nothing, Blue reward:0.85\n", - "step: 1358, Red action: do-nothing, Blue reward:0.85\n", - "step: 1359, Red action: do-nothing, Blue reward:0.85\n", - "step: 1360, Red action: do-nothing, Blue reward:0.85\n", - "step: 1361, Red action: do-nothing, Blue reward:0.85\n", - "step: 1362, Red action: do-nothing, Blue reward:0.85\n", - "step: 1363, Red action: do-nothing, Blue reward:0.85\n", - "step: 1364, Red action: do-nothing, Blue reward:0.85\n", - "step: 1365, Red action: do-nothing, Blue reward:0.85\n", - "step: 1366, Red action: do-nothing, Blue reward:0.85\n", - "step: 1367, Red action: do-nothing, Blue reward:0.85\n", - "step: 1368, Red action: do-nothing, Blue reward:0.85\n", - "step: 1369, Red action: do-nothing, Blue reward:0.85\n", - "step: 1370, Red action: do-nothing, Blue reward:0.85\n", - "step: 1371, Red action: do-nothing, Blue reward:0.85\n", - "step: 1372, Red action: do-nothing, Blue reward:0.85\n", - "step: 1373, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1374, Red action: do-nothing, Blue reward:0.85\n", - "step: 1375, Red action: do-nothing, Blue reward:0.85\n", - "step: 1376, Red action: do-nothing, Blue reward:0.85\n", - "step: 1377, Red action: do-nothing, Blue reward:0.85\n", - "step: 1378, Red action: do-nothing, Blue reward:0.85\n", - "step: 1379, Red action: do-nothing, Blue reward:0.85\n", - "step: 1380, Red action: do-nothing, Blue reward:0.85\n", - "step: 1381, Red action: do-nothing, Blue reward:0.85\n", - "step: 1382, Red action: do-nothing, Blue reward:0.85\n", - "step: 1383, Red action: do-nothing, Blue reward:0.85\n", - "step: 1384, Red action: do-nothing, Blue reward:0.85\n", - "step: 1385, Red action: do-nothing, Blue reward:0.85\n", - "step: 1386, Red action: do-nothing, Blue reward:0.85\n", - "step: 1387, Red action: do-nothing, Blue reward:0.85\n", - "step: 1388, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1389, Red action: do-nothing, Blue reward:0.85\n", - "step: 1390, Red action: do-nothing, Blue reward:0.85\n", - "step: 1391, Red action: do-nothing, Blue reward:0.85\n", - "step: 1392, Red action: do-nothing, Blue reward:0.85\n", - "step: 1393, Red action: do-nothing, Blue reward:0.85\n", - "step: 1394, Red action: do-nothing, Blue reward:0.85\n", - "step: 1395, Red action: do-nothing, Blue reward:0.85\n", - "step: 1396, Red action: do-nothing, Blue reward:0.85\n", - "step: 1397, Red action: do-nothing, Blue reward:0.85\n", - "step: 1398, Red action: do-nothing, Blue reward:0.85\n", - "step: 1399, Red action: do-nothing, Blue reward:0.85\n", - "step: 1400, Red action: do-nothing, Blue reward:0.85\n", - "step: 1401, Red action: do-nothing, Blue reward:0.85\n", - "step: 1402, Red action: do-nothing, Blue reward:0.85\n", - "step: 1403, Red action: do-nothing, Blue reward:0.85\n", - "step: 1404, Red action: do-nothing, Blue reward:0.85\n", - "step: 1405, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1406, Red action: do-nothing, Blue reward:0.85\n", - "step: 1407, Red action: do-nothing, Blue reward:0.85\n", - "step: 1408, Red action: do-nothing, Blue reward:0.85\n", - "step: 1409, Red action: do-nothing, Blue reward:0.85\n", - "step: 1410, Red action: do-nothing, Blue reward:0.85\n", - "step: 1411, Red action: do-nothing, Blue reward:0.85\n", - "step: 1412, Red action: do-nothing, Blue reward:0.85\n", - "step: 1413, Red action: do-nothing, Blue reward:0.85\n", - "step: 1414, Red action: do-nothing, Blue reward:0.85\n", - "step: 1415, Red action: do-nothing, Blue reward:0.85\n", - "step: 1416, Red action: do-nothing, Blue reward:0.85\n", - "step: 1417, Red action: do-nothing, Blue reward:0.85\n", - "step: 1418, Red action: do-nothing, Blue reward:0.85\n", - "step: 1419, Red action: do-nothing, Blue reward:0.85\n", - "step: 1420, Red action: do-nothing, Blue reward:0.85\n", - "step: 1421, Red action: do-nothing, Blue reward:0.85\n", - "step: 1422, Red action: do-nothing, Blue reward:0.85\n", - "step: 1423, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1424, Red action: do-nothing, Blue reward:0.85\n", - "step: 1425, Red action: do-nothing, Blue reward:0.85\n", - "step: 1426, Red action: do-nothing, Blue reward:0.85\n", - "step: 1427, Red action: do-nothing, Blue reward:0.85\n", - "step: 1428, Red action: do-nothing, Blue reward:0.85\n", - "step: 1429, Red action: do-nothing, Blue reward:0.85\n", - "step: 1430, Red action: do-nothing, Blue reward:0.85\n", - "step: 1431, Red action: do-nothing, Blue reward:0.85\n", - "step: 1432, Red action: do-nothing, Blue reward:0.85\n", - "step: 1433, Red action: do-nothing, Blue reward:0.85\n", - "step: 1434, Red action: do-nothing, Blue reward:0.85\n", - "step: 1435, Red action: do-nothing, Blue reward:0.85\n", - "step: 1436, Red action: do-nothing, Blue reward:0.85\n", - "step: 1437, Red action: do-nothing, Blue reward:0.85\n", - "step: 1438, Red action: do-nothing, Blue reward:0.85\n", - "step: 1439, Red action: do-nothing, Blue reward:0.85\n", - "step: 1440, Red action: do-nothing, Blue reward:0.85\n", - "step: 1441, Red action: do-nothing, Blue reward:0.85\n", - "step: 1442, Red action: do-nothing, Blue reward:0.85\n", - "step: 1443, Red action: do-nothing, Blue reward:0.85\n", - "step: 1444, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1445, Red action: do-nothing, Blue reward:0.85\n", - "step: 1446, Red action: do-nothing, Blue reward:0.85\n", - "step: 1447, Red action: do-nothing, Blue reward:0.85\n", - "step: 1448, Red action: do-nothing, Blue reward:0.85\n", - "step: 1449, Red action: do-nothing, Blue reward:0.85\n", - "step: 1450, Red action: do-nothing, Blue reward:0.85\n", - "step: 1451, Red action: do-nothing, Blue reward:0.85\n", - "step: 1452, Red action: do-nothing, Blue reward:0.85\n", - "step: 1453, Red action: do-nothing, Blue reward:0.85\n", - "step: 1454, Red action: do-nothing, Blue reward:0.85\n", - "step: 1455, Red action: do-nothing, Blue reward:0.85\n", - "step: 1456, Red action: do-nothing, Blue reward:0.85\n", - "step: 1457, Red action: do-nothing, Blue reward:0.85\n", - "step: 1458, Red action: do-nothing, Blue reward:0.85\n", - "step: 1459, Red action: do-nothing, Blue reward:0.85\n", - "step: 1460, Red action: do-nothing, Blue reward:0.85\n", - "step: 1461, Red action: do-nothing, Blue reward:0.85\n", - "step: 1462, Red action: do-nothing, Blue reward:0.85\n", - "step: 1463, Red action: do-nothing, Blue reward:0.85\n", - "step: 1464, Red action: do-nothing, Blue reward:0.85\n", - "step: 1465, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1466, Red action: do-nothing, Blue reward:0.85\n", - "step: 1467, Red action: do-nothing, Blue reward:0.85\n", - "step: 1468, Red action: do-nothing, Blue reward:0.85\n", - "step: 1469, Red action: do-nothing, Blue reward:0.85\n", - "step: 1470, Red action: do-nothing, Blue reward:0.85\n", - "step: 1471, Red action: do-nothing, Blue reward:0.85\n", - "step: 1472, Red action: do-nothing, Blue reward:0.85\n", - "step: 1473, Red action: do-nothing, Blue reward:0.85\n", - "step: 1474, Red action: do-nothing, Blue reward:0.85\n", - "step: 1475, Red action: do-nothing, Blue reward:0.85\n", - "step: 1476, Red action: do-nothing, Blue reward:0.85\n", - "step: 1477, Red action: do-nothing, Blue reward:0.85\n", - "step: 1478, Red action: do-nothing, Blue reward:0.85\n", - "step: 1479, Red action: do-nothing, Blue reward:0.85\n", - "step: 1480, Red action: do-nothing, Blue reward:0.85\n", - "step: 1481, Red action: do-nothing, Blue reward:0.85\n", - "step: 1482, Red action: do-nothing, Blue reward:0.85\n", - "step: 1483, Red action: do-nothing, Blue reward:0.85\n", - "step: 1484, Red action: do-nothing, Blue reward:0.85\n", - "step: 1485, Red action: do-nothing, Blue reward:0.85\n", - "step: 1486, Red action: do-nothing, Blue reward:0.85\n", - "step: 1487, Red action: do-nothing, Blue reward:0.85\n", - "step: 1488, Red action: do-nothing, Blue reward:0.85\n", - "step: 1489, Red action: do-nothing, Blue reward:0.85\n", - "step: 1490, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1491, Red action: do-nothing, Blue reward:0.85\n", - "step: 1492, Red action: do-nothing, Blue reward:0.85\n", - "step: 1493, Red action: do-nothing, Blue reward:0.85\n", - "step: 1494, Red action: do-nothing, Blue reward:0.85\n", - "step: 1495, Red action: do-nothing, Blue reward:0.85\n", - "step: 1496, Red action: do-nothing, Blue reward:0.85\n", - "step: 1497, Red action: do-nothing, Blue reward:0.85\n", - "step: 1498, Red action: do-nothing, Blue reward:0.85\n", - "step: 1499, Red action: do-nothing, Blue reward:0.85\n", - "step: 1500, Red action: do-nothing, Blue reward:0.85\n", - "step: 1501, Red action: do-nothing, Blue reward:0.85\n", - "step: 1502, Red action: do-nothing, Blue reward:0.85\n", - "step: 1503, Red action: do-nothing, Blue reward:0.85\n", - "step: 1504, Red action: do-nothing, Blue reward:0.85\n", - "step: 1505, Red action: do-nothing, Blue reward:0.85\n", - "step: 1506, Red action: do-nothing, Blue reward:0.85\n", - "step: 1507, Red action: do-nothing, Blue reward:0.85\n", - "step: 1508, Red action: do-nothing, Blue reward:0.85\n", - "step: 1509, Red action: do-nothing, Blue reward:0.85\n", - "step: 1510, Red action: do-nothing, Blue reward:0.85\n", - "step: 1511, Red action: do-nothing, Blue reward:0.85\n", - "step: 1512, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1513, Red action: do-nothing, Blue reward:0.85\n", - "step: 1514, Red action: do-nothing, Blue reward:0.85\n", - "step: 1515, Red action: do-nothing, Blue reward:0.85\n", - "step: 1516, Red action: do-nothing, Blue reward:0.85\n", - "step: 1517, Red action: do-nothing, Blue reward:0.85\n", - "step: 1518, Red action: do-nothing, Blue reward:0.85\n", - "step: 1519, Red action: do-nothing, Blue reward:0.85\n", - "step: 1520, Red action: do-nothing, Blue reward:0.85\n", - "step: 1521, Red action: do-nothing, Blue reward:0.85\n", - "step: 1522, Red action: do-nothing, Blue reward:0.85\n", - "step: 1523, Red action: do-nothing, Blue reward:0.85\n", - "step: 1524, Red action: do-nothing, Blue reward:0.85\n", - "step: 1525, Red action: do-nothing, Blue reward:0.85\n", - "step: 1526, Red action: do-nothing, Blue reward:0.85\n", - "step: 1527, Red action: do-nothing, Blue reward:0.85\n", - "step: 1528, Red action: do-nothing, Blue reward:0.85\n", - "step: 1529, Red action: do-nothing, Blue reward:0.85\n", - "step: 1530, Red action: do-nothing, Blue reward:0.85\n", - "step: 1531, Red action: do-nothing, Blue reward:0.85\n", - "step: 1532, Red action: do-nothing, Blue reward:0.85\n", - "step: 1533, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1534, Red action: do-nothing, Blue reward:0.85\n", - "step: 1535, Red action: do-nothing, Blue reward:0.85\n", - "step: 1536, Red action: do-nothing, Blue reward:0.85\n", - "step: 1537, Red action: do-nothing, Blue reward:0.85\n", - "step: 1538, Red action: do-nothing, Blue reward:0.85\n", - "step: 1539, Red action: do-nothing, Blue reward:0.85\n", - "step: 1540, Red action: do-nothing, Blue reward:0.85\n", - "step: 1541, Red action: do-nothing, Blue reward:0.85\n", - "step: 1542, Red action: do-nothing, Blue reward:0.85\n", - "step: 1543, Red action: do-nothing, Blue reward:0.85\n", - "step: 1544, Red action: do-nothing, Blue reward:0.85\n", - "step: 1545, Red action: do-nothing, Blue reward:0.85\n", - "step: 1546, Red action: do-nothing, Blue reward:0.85\n", - "step: 1547, Red action: do-nothing, Blue reward:0.85\n", - "step: 1548, Red action: do-nothing, Blue reward:0.85\n", - "step: 1549, Red action: do-nothing, Blue reward:0.85\n", - "step: 1550, Red action: do-nothing, Blue reward:0.85\n", - "step: 1551, Red action: do-nothing, Blue reward:0.85\n", - "step: 1552, Red action: do-nothing, Blue reward:0.85\n", - "step: 1553, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1554, Red action: do-nothing, Blue reward:0.85\n", - "step: 1555, Red action: do-nothing, Blue reward:0.85\n", - "step: 1556, Red action: do-nothing, Blue reward:0.85\n", - "step: 1557, Red action: do-nothing, Blue reward:0.85\n", - "step: 1558, Red action: do-nothing, Blue reward:0.85\n", - "step: 1559, Red action: do-nothing, Blue reward:0.85\n", - "step: 1560, Red action: do-nothing, Blue reward:0.85\n", - "step: 1561, Red action: do-nothing, Blue reward:0.85\n", - "step: 1562, Red action: do-nothing, Blue reward:0.85\n", - "step: 1563, Red action: do-nothing, Blue reward:0.85\n", - "step: 1564, Red action: do-nothing, Blue reward:0.85\n", - "step: 1565, Red action: do-nothing, Blue reward:0.85\n", - "step: 1566, Red action: do-nothing, Blue reward:0.85\n", - "step: 1567, Red action: do-nothing, Blue reward:0.85\n", - "step: 1568, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1569, Red action: do-nothing, Blue reward:0.85\n", - "step: 1570, Red action: do-nothing, Blue reward:0.85\n", - "step: 1571, Red action: do-nothing, Blue reward:0.85\n", - "step: 1572, Red action: do-nothing, Blue reward:0.85\n", - "step: 1573, Red action: do-nothing, Blue reward:0.85\n", - "step: 1574, Red action: do-nothing, Blue reward:0.85\n", - "step: 1575, Red action: do-nothing, Blue reward:0.85\n", - "step: 1576, Red action: do-nothing, Blue reward:0.85\n", - "step: 1577, Red action: do-nothing, Blue reward:0.85\n", - "step: 1578, Red action: do-nothing, Blue reward:0.85\n", - "step: 1579, Red action: do-nothing, Blue reward:0.85\n", - "step: 1580, Red action: do-nothing, Blue reward:0.85\n", - "step: 1581, Red action: do-nothing, Blue reward:0.85\n", - "step: 1582, Red action: do-nothing, Blue reward:0.85\n", - "step: 1583, Red action: do-nothing, Blue reward:0.85\n", - "step: 1584, Red action: do-nothing, Blue reward:0.85\n", - "step: 1585, Red action: do-nothing, Blue reward:0.85\n", - "step: 1586, Red action: do-nothing, Blue reward:0.85\n", - "step: 1587, Red action: do-nothing, Blue reward:0.85\n", - "step: 1588, Red action: do-nothing, Blue reward:0.85\n", - "step: 1589, Red action: do-nothing, Blue reward:0.85\n", - "step: 1590, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1591, Red action: do-nothing, Blue reward:0.85\n", - "step: 1592, Red action: do-nothing, Blue reward:0.85\n", - "step: 1593, Red action: do-nothing, Blue reward:0.85\n", - "step: 1594, Red action: do-nothing, Blue reward:0.85\n", - "step: 1595, Red action: do-nothing, Blue reward:0.85\n", - "step: 1596, Red action: do-nothing, Blue reward:0.85\n", - "step: 1597, Red action: do-nothing, Blue reward:0.85\n", - "step: 1598, Red action: do-nothing, Blue reward:0.85\n", - "step: 1599, Red action: do-nothing, Blue reward:0.85\n", - "step: 1600, Red action: do-nothing, Blue reward:0.85\n", - "step: 1601, Red action: do-nothing, Blue reward:0.85\n", - "step: 1602, Red action: do-nothing, Blue reward:0.85\n", - "step: 1603, Red action: do-nothing, Blue reward:0.85\n", - "step: 1604, Red action: do-nothing, Blue reward:0.85\n", - "step: 1605, Red action: do-nothing, Blue reward:0.85\n", - "step: 1606, Red action: do-nothing, Blue reward:0.85\n", - "step: 1607, Red action: do-nothing, Blue reward:0.85\n", - "step: 1608, Red action: do-nothing, Blue reward:0.85\n", - "step: 1609, Red action: do-nothing, Blue reward:0.85\n", - "step: 1610, Red action: do-nothing, Blue reward:0.85\n", - "step: 1611, Red action: do-nothing, Blue reward:0.85\n", - "step: 1612, Red action: do-nothing, Blue reward:0.85\n", - "step: 1613, Red action: do-nothing, Blue reward:0.85\n", - "step: 1614, Red action: do-nothing, Blue reward:0.85\n", - "step: 1615, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1616, Red action: do-nothing, Blue reward:0.85\n", - "step: 1617, Red action: do-nothing, Blue reward:0.85\n", - "step: 1618, Red action: do-nothing, Blue reward:0.85\n", - "step: 1619, Red action: do-nothing, Blue reward:0.85\n", - "step: 1620, Red action: do-nothing, Blue reward:0.85\n", - "step: 1621, Red action: do-nothing, Blue reward:0.85\n", - "step: 1622, Red action: do-nothing, Blue reward:0.85\n", - "step: 1623, Red action: do-nothing, Blue reward:0.85\n", - "step: 1624, Red action: do-nothing, Blue reward:0.85\n", - "step: 1625, Red action: do-nothing, Blue reward:0.85\n", - "step: 1626, Red action: do-nothing, Blue reward:0.85\n", - "step: 1627, Red action: do-nothing, Blue reward:0.85\n", - "step: 1628, Red action: do-nothing, Blue reward:0.85\n", - "step: 1629, Red action: do-nothing, Blue reward:0.85\n", - "step: 1630, Red action: do-nothing, Blue reward:0.85\n", - "step: 1631, Red action: do-nothing, Blue reward:0.85\n", - "step: 1632, Red action: do-nothing, Blue reward:0.85\n", - "step: 1633, Red action: do-nothing, Blue reward:0.85\n", - "step: 1634, Red action: do-nothing, Blue reward:0.85\n", - "step: 1635, Red action: do-nothing, Blue reward:0.85\n", - "step: 1636, Red action: do-nothing, Blue reward:0.85\n", - "step: 1637, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1638, Red action: do-nothing, Blue reward:0.85\n", - "step: 1639, Red action: do-nothing, Blue reward:0.85\n", - "step: 1640, Red action: do-nothing, Blue reward:0.85\n", - "step: 1641, Red action: do-nothing, Blue reward:0.85\n", - "step: 1642, Red action: do-nothing, Blue reward:0.85\n", - "step: 1643, Red action: do-nothing, Blue reward:0.85\n", - "step: 1644, Red action: do-nothing, Blue reward:0.85\n", - "step: 1645, Red action: do-nothing, Blue reward:0.85\n", - "step: 1646, Red action: do-nothing, Blue reward:0.85\n", - "step: 1647, Red action: do-nothing, Blue reward:0.85\n", - "step: 1648, Red action: do-nothing, Blue reward:0.85\n", - "step: 1649, Red action: do-nothing, Blue reward:0.85\n", - "step: 1650, Red action: do-nothing, Blue reward:0.85\n", - "step: 1651, Red action: do-nothing, Blue reward:0.85\n", - "step: 1652, Red action: do-nothing, Blue reward:0.85\n", - "step: 1653, Red action: do-nothing, Blue reward:0.85\n", - "step: 1654, Red action: do-nothing, Blue reward:0.85\n", - "step: 1655, Red action: do-nothing, Blue reward:0.85\n", - "step: 1656, Red action: do-nothing, Blue reward:0.85\n", - "step: 1657, Red action: do-nothing, Blue reward:0.85\n", - "step: 1658, Red action: do-nothing, Blue reward:0.85\n", - "step: 1659, Red action: do-nothing, Blue reward:0.85\n", - "step: 1660, Red action: do-nothing, Blue reward:0.85\n", - "step: 1661, Red action: do-nothing, Blue reward:0.85\n", - "step: 1662, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1663, Red action: do-nothing, Blue reward:0.85\n", - "step: 1664, Red action: do-nothing, Blue reward:0.85\n", - "step: 1665, Red action: do-nothing, Blue reward:0.85\n", - "step: 1666, Red action: do-nothing, Blue reward:0.85\n", - "step: 1667, Red action: do-nothing, Blue reward:0.85\n", - "step: 1668, Red action: do-nothing, Blue reward:0.85\n", - "step: 1669, Red action: do-nothing, Blue reward:0.85\n", - "step: 1670, Red action: do-nothing, Blue reward:0.85\n", - "step: 1671, Red action: do-nothing, Blue reward:0.85\n", - "step: 1672, Red action: do-nothing, Blue reward:0.85\n", - "step: 1673, Red action: do-nothing, Blue reward:0.85\n", - "step: 1674, Red action: do-nothing, Blue reward:0.85\n", - "step: 1675, Red action: do-nothing, Blue reward:0.85\n", - "step: 1676, Red action: do-nothing, Blue reward:0.85\n", - "step: 1677, Red action: do-nothing, Blue reward:0.85\n", - "step: 1678, Red action: do-nothing, Blue reward:0.85\n", - "step: 1679, Red action: do-nothing, Blue reward:0.85\n", - "step: 1680, Red action: do-nothing, Blue reward:0.85\n", - "step: 1681, Red action: do-nothing, Blue reward:0.85\n", - "step: 1682, Red action: do-nothing, Blue reward:0.85\n", - "step: 1683, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1684, Red action: do-nothing, Blue reward:0.85\n", - "step: 1685, Red action: do-nothing, Blue reward:0.85\n", - "step: 1686, Red action: do-nothing, Blue reward:0.85\n", - "step: 1687, Red action: do-nothing, Blue reward:0.85\n", - "step: 1688, Red action: do-nothing, Blue reward:0.85\n", - "step: 1689, Red action: do-nothing, Blue reward:0.85\n", - "step: 1690, Red action: do-nothing, Blue reward:0.85\n", - "step: 1691, Red action: do-nothing, Blue reward:0.85\n", - "step: 1692, Red action: do-nothing, Blue reward:0.85\n", - "step: 1693, Red action: do-nothing, Blue reward:0.85\n", - "step: 1694, Red action: do-nothing, Blue reward:0.85\n", - "step: 1695, Red action: do-nothing, Blue reward:0.85\n", - "step: 1696, Red action: do-nothing, Blue reward:0.85\n", - "step: 1697, Red action: do-nothing, Blue reward:0.85\n", - "step: 1698, Red action: do-nothing, Blue reward:0.85\n", - "step: 1699, Red action: do-nothing, Blue reward:0.85\n", - "step: 1700, Red action: do-nothing, Blue reward:0.85\n", - "step: 1701, Red action: do-nothing, Blue reward:0.85\n", - "step: 1702, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1703, Red action: do-nothing, Blue reward:0.85\n", - "step: 1704, Red action: do-nothing, Blue reward:0.85\n", - "step: 1705, Red action: do-nothing, Blue reward:0.85\n", - "step: 1706, Red action: do-nothing, Blue reward:0.85\n", - "step: 1707, Red action: do-nothing, Blue reward:0.85\n", - "step: 1708, Red action: do-nothing, Blue reward:0.85\n", - "step: 1709, Red action: do-nothing, Blue reward:0.85\n", - "step: 1710, Red action: do-nothing, Blue reward:0.85\n", - "step: 1711, Red action: do-nothing, Blue reward:0.85\n", - "step: 1712, Red action: do-nothing, Blue reward:0.85\n", - "step: 1713, Red action: do-nothing, Blue reward:0.85\n", - "step: 1714, Red action: do-nothing, Blue reward:0.85\n", - "step: 1715, Red action: do-nothing, Blue reward:0.85\n", - "step: 1716, Red action: do-nothing, Blue reward:0.85\n", - "step: 1717, Red action: do-nothing, Blue reward:0.85\n", - "step: 1718, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1719, Red action: do-nothing, Blue reward:0.85\n", - "step: 1720, Red action: do-nothing, Blue reward:0.85\n", - "step: 1721, Red action: do-nothing, Blue reward:0.85\n", - "step: 1722, Red action: do-nothing, Blue reward:0.85\n", - "step: 1723, Red action: do-nothing, Blue reward:0.85\n", - "step: 1724, Red action: do-nothing, Blue reward:0.85\n", - "step: 1725, Red action: do-nothing, Blue reward:0.85\n", - "step: 1726, Red action: do-nothing, Blue reward:0.85\n", - "step: 1727, Red action: do-nothing, Blue reward:0.85\n", - "step: 1728, Red action: do-nothing, Blue reward:0.85\n", - "step: 1729, Red action: do-nothing, Blue reward:0.85\n", - "step: 1730, Red action: do-nothing, Blue reward:0.85\n", - "step: 1731, Red action: do-nothing, Blue reward:0.85\n", - "step: 1732, Red action: do-nothing, Blue reward:0.85\n", - "step: 1733, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1734, Red action: do-nothing, Blue reward:0.85\n", - "step: 1735, Red action: do-nothing, Blue reward:0.85\n", - "step: 1736, Red action: do-nothing, Blue reward:0.85\n", - "step: 1737, Red action: do-nothing, Blue reward:0.85\n", - "step: 1738, Red action: do-nothing, Blue reward:0.85\n", - "step: 1739, Red action: do-nothing, Blue reward:0.85\n", - "step: 1740, Red action: do-nothing, Blue reward:0.85\n", - "step: 1741, Red action: do-nothing, Blue reward:0.85\n", - "step: 1742, Red action: do-nothing, Blue reward:0.85\n", - "step: 1743, Red action: do-nothing, Blue reward:0.85\n", - "step: 1744, Red action: do-nothing, Blue reward:0.85\n", - "step: 1745, Red action: do-nothing, Blue reward:0.85\n", - "step: 1746, Red action: do-nothing, Blue reward:0.85\n", - "step: 1747, Red action: do-nothing, Blue reward:0.85\n", - "step: 1748, Red action: do-nothing, Blue reward:0.85\n", - "step: 1749, Red action: do-nothing, Blue reward:0.85\n", - "step: 1750, Red action: do-nothing, Blue reward:0.85\n", - "step: 1751, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1752, Red action: do-nothing, Blue reward:0.85\n", - "step: 1753, Red action: do-nothing, Blue reward:0.85\n", - "step: 1754, Red action: do-nothing, Blue reward:0.85\n", - "step: 1755, Red action: do-nothing, Blue reward:0.85\n", - "step: 1756, Red action: do-nothing, Blue reward:0.85\n", - "step: 1757, Red action: do-nothing, Blue reward:0.85\n", - "step: 1758, Red action: do-nothing, Blue reward:0.85\n", - "step: 1759, Red action: do-nothing, Blue reward:0.85\n", - "step: 1760, Red action: do-nothing, Blue reward:0.85\n", - "step: 1761, Red action: do-nothing, Blue reward:0.85\n", - "step: 1762, Red action: do-nothing, Blue reward:0.85\n", - "step: 1763, Red action: do-nothing, Blue reward:0.85\n", - "step: 1764, Red action: do-nothing, Blue reward:0.85\n", - "step: 1765, Red action: do-nothing, Blue reward:0.85\n", - "step: 1766, Red action: do-nothing, Blue reward:0.85\n", - "step: 1767, Red action: do-nothing, Blue reward:0.85\n", - "step: 1768, Red action: do-nothing, Blue reward:0.85\n", - "step: 1769, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1770, Red action: do-nothing, Blue reward:0.85\n", - "step: 1771, Red action: do-nothing, Blue reward:0.85\n", - "step: 1772, Red action: do-nothing, Blue reward:0.85\n", - "step: 1773, Red action: do-nothing, Blue reward:0.85\n", - "step: 1774, Red action: do-nothing, Blue reward:0.85\n", - "step: 1775, Red action: do-nothing, Blue reward:0.85\n", - "step: 1776, Red action: do-nothing, Blue reward:0.85\n", - "step: 1777, Red action: do-nothing, Blue reward:0.85\n", - "step: 1778, Red action: do-nothing, Blue reward:0.85\n", - "step: 1779, Red action: do-nothing, Blue reward:0.85\n", - "step: 1780, Red action: do-nothing, Blue reward:0.85\n", - "step: 1781, Red action: do-nothing, Blue reward:0.85\n", - "step: 1782, Red action: do-nothing, Blue reward:0.85\n", - "step: 1783, Red action: do-nothing, Blue reward:0.85\n", - "step: 1784, Red action: do-nothing, Blue reward:0.85\n", - "step: 1785, Red action: do-nothing, Blue reward:0.85\n", - "step: 1786, Red action: do-nothing, Blue reward:0.85\n", - "step: 1787, Red action: do-nothing, Blue reward:0.85\n", - "step: 1788, Red action: do-nothing, Blue reward:0.85\n", - "step: 1789, Red action: do-nothing, Blue reward:0.85\n", - "step: 1790, Red action: do-nothing, Blue reward:0.85\n", - "step: 1791, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1792, Red action: do-nothing, Blue reward:0.85\n", - "step: 1793, Red action: do-nothing, Blue reward:0.85\n", - "step: 1794, Red action: do-nothing, Blue reward:0.85\n", - "step: 1795, Red action: do-nothing, Blue reward:0.85\n", - "step: 1796, Red action: do-nothing, Blue reward:0.85\n", - "step: 1797, Red action: do-nothing, Blue reward:0.85\n", - "step: 1798, Red action: do-nothing, Blue reward:0.85\n", - "step: 1799, Red action: do-nothing, Blue reward:0.85\n", - "step: 1800, Red action: do-nothing, Blue reward:0.85\n", - "step: 1801, Red action: do-nothing, Blue reward:0.85\n", - "step: 1802, Red action: do-nothing, Blue reward:0.85\n", - "step: 1803, Red action: do-nothing, Blue reward:0.85\n", - "step: 1804, Red action: do-nothing, Blue reward:0.85\n", - "step: 1805, Red action: do-nothing, Blue reward:0.85\n", - "step: 1806, Red action: do-nothing, Blue reward:0.85\n", - "step: 1807, Red action: do-nothing, Blue reward:0.85\n", - "step: 1808, Red action: do-nothing, Blue reward:0.85\n", - "step: 1809, Red action: do-nothing, Blue reward:0.85\n", - "step: 1810, Red action: do-nothing, Blue reward:0.85\n", - "step: 1811, Red action: do-nothing, Blue reward:0.85\n", - "step: 1812, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1813, Red action: do-nothing, Blue reward:0.85\n", - "step: 1814, Red action: do-nothing, Blue reward:0.85\n", - "step: 1815, Red action: do-nothing, Blue reward:0.85\n", - "step: 1816, Red action: do-nothing, Blue reward:0.85\n", - "step: 1817, Red action: do-nothing, Blue reward:0.85\n", - "step: 1818, Red action: do-nothing, Blue reward:0.85\n", - "step: 1819, Red action: do-nothing, Blue reward:0.85\n", - "step: 1820, Red action: do-nothing, Blue reward:0.85\n", - "step: 1821, Red action: do-nothing, Blue reward:0.85\n", - "step: 1822, Red action: do-nothing, Blue reward:0.85\n", - "step: 1823, Red action: do-nothing, Blue reward:0.85\n", - "step: 1824, Red action: do-nothing, Blue reward:0.85\n", - "step: 1825, Red action: do-nothing, Blue reward:0.85\n", - "step: 1826, Red action: do-nothing, Blue reward:0.85\n", - "step: 1827, Red action: do-nothing, Blue reward:0.85\n", - "step: 1828, Red action: do-nothing, Blue reward:0.85\n", - "step: 1829, Red action: do-nothing, Blue reward:0.85\n", - "step: 1830, Red action: do-nothing, Blue reward:0.85\n", - "step: 1831, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1832, Red action: do-nothing, Blue reward:0.85\n", - "step: 1833, Red action: do-nothing, Blue reward:0.85\n", - "step: 1834, Red action: do-nothing, Blue reward:0.85\n", - "step: 1835, Red action: do-nothing, Blue reward:0.85\n", - "step: 1836, Red action: do-nothing, Blue reward:0.85\n", - "step: 1837, Red action: do-nothing, Blue reward:0.85\n", - "step: 1838, Red action: do-nothing, Blue reward:0.85\n", - "step: 1839, Red action: do-nothing, Blue reward:0.85\n", - "step: 1840, Red action: do-nothing, Blue reward:0.85\n", - "step: 1841, Red action: do-nothing, Blue reward:0.85\n", - "step: 1842, Red action: do-nothing, Blue reward:0.85\n", - "step: 1843, Red action: do-nothing, Blue reward:0.85\n", - "step: 1844, Red action: do-nothing, Blue reward:0.85\n", - "step: 1845, Red action: do-nothing, Blue reward:0.85\n", - "step: 1846, Red action: do-nothing, Blue reward:0.85\n", - "step: 1847, Red action: do-nothing, Blue reward:0.85\n", - "step: 1848, Red action: do-nothing, Blue reward:0.85\n", - "step: 1849, Red action: do-nothing, Blue reward:0.85\n", - "step: 1850, Red action: do-nothing, Blue reward:0.85\n", - "step: 1851, Red action: do-nothing, Blue reward:0.85\n", - "step: 1852, Red action: do-nothing, Blue reward:0.85\n", - "step: 1853, Red action: do-nothing, Blue reward:0.85\n", - "step: 1854, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1855, Red action: do-nothing, Blue reward:0.85\n", - "step: 1856, Red action: do-nothing, Blue reward:0.85\n", - "step: 1857, Red action: do-nothing, Blue reward:0.85\n", - "step: 1858, Red action: do-nothing, Blue reward:0.85\n", - "step: 1859, Red action: do-nothing, Blue reward:0.85\n", - "step: 1860, Red action: do-nothing, Blue reward:0.85\n", - "step: 1861, Red action: do-nothing, Blue reward:0.85\n", - "step: 1862, Red action: do-nothing, Blue reward:0.85\n", - "step: 1863, Red action: do-nothing, Blue reward:0.85\n", - "step: 1864, Red action: do-nothing, Blue reward:0.85\n", - "step: 1865, Red action: do-nothing, Blue reward:0.85\n", - "step: 1866, Red action: do-nothing, Blue reward:0.85\n", - "step: 1867, Red action: do-nothing, Blue reward:0.85\n", - "step: 1868, Red action: do-nothing, Blue reward:0.85\n", - "step: 1869, Red action: do-nothing, Blue reward:0.85\n", - "step: 1870, Red action: do-nothing, Blue reward:0.85\n", - "step: 1871, Red action: do-nothing, Blue reward:0.85\n", - "step: 1872, Red action: do-nothing, Blue reward:0.85\n", - "step: 1873, Red action: do-nothing, Blue reward:0.85\n", - "step: 1874, Red action: do-nothing, Blue reward:0.85\n", - "step: 1875, Red action: do-nothing, Blue reward:0.85\n", - "step: 1876, Red action: do-nothing, Blue reward:0.85\n", - "step: 1877, Red action: do-nothing, Blue reward:0.85\n", - "step: 1878, Red action: do-nothing, Blue reward:0.85\n", - "step: 1879, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1880, Red action: do-nothing, Blue reward:0.85\n", - "step: 1881, Red action: do-nothing, Blue reward:0.85\n", - "step: 1882, Red action: do-nothing, Blue reward:0.85\n", - "step: 1883, Red action: do-nothing, Blue reward:0.85\n", - "step: 1884, Red action: do-nothing, Blue reward:0.85\n", - "step: 1885, Red action: do-nothing, Blue reward:0.85\n", - "step: 1886, Red action: do-nothing, Blue reward:0.85\n", - "step: 1887, Red action: do-nothing, Blue reward:0.85\n", - "step: 1888, Red action: do-nothing, Blue reward:0.85\n", - "step: 1889, Red action: do-nothing, Blue reward:0.85\n", - "step: 1890, Red action: do-nothing, Blue reward:0.85\n", - "step: 1891, Red action: do-nothing, Blue reward:0.85\n", - "step: 1892, Red action: do-nothing, Blue reward:0.85\n", - "step: 1893, Red action: do-nothing, Blue reward:0.85\n", - "step: 1894, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1895, Red action: do-nothing, Blue reward:0.85\n", - "step: 1896, Red action: do-nothing, Blue reward:0.85\n", - "step: 1897, Red action: do-nothing, Blue reward:0.85\n", - "step: 1898, Red action: do-nothing, Blue reward:0.85\n", - "step: 1899, Red action: do-nothing, Blue reward:0.85\n", - "step: 1900, Red action: do-nothing, Blue reward:0.85\n", - "step: 1901, Red action: do-nothing, Blue reward:0.85\n", - "step: 1902, Red action: do-nothing, Blue reward:0.85\n", - "step: 1903, Red action: do-nothing, Blue reward:0.85\n", - "step: 1904, Red action: do-nothing, Blue reward:0.85\n", - "step: 1905, Red action: do-nothing, Blue reward:0.85\n", - "step: 1906, Red action: do-nothing, Blue reward:0.85\n", - "step: 1907, Red action: do-nothing, Blue reward:0.85\n", - "step: 1908, Red action: do-nothing, Blue reward:0.85\n", - "step: 1909, Red action: do-nothing, Blue reward:0.85\n", - "step: 1910, Red action: do-nothing, Blue reward:0.85\n", - "step: 1911, Red action: do-nothing, Blue reward:0.85\n", - "step: 1912, Red action: do-nothing, Blue reward:0.85\n", - "step: 1913, Red action: do-nothing, Blue reward:0.85\n", - "step: 1914, Red action: do-nothing, Blue reward:0.85\n", - "step: 1915, Red action: do-nothing, Blue reward:0.85\n", - "step: 1916, Red action: do-nothing, Blue reward:0.85\n", - "step: 1917, Red action: do-nothing, Blue reward:0.85\n", - "step: 1918, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1919, Red action: do-nothing, Blue reward:0.85\n", - "step: 1920, Red action: do-nothing, Blue reward:0.85\n", - "step: 1921, Red action: do-nothing, Blue reward:0.85\n", - "step: 1922, Red action: do-nothing, Blue reward:0.85\n", - "step: 1923, Red action: do-nothing, Blue reward:0.85\n", - "step: 1924, Red action: do-nothing, Blue reward:0.85\n", - "step: 1925, Red action: do-nothing, Blue reward:0.85\n", - "step: 1926, Red action: do-nothing, Blue reward:0.85\n", - "step: 1927, Red action: do-nothing, Blue reward:0.85\n", - "step: 1928, Red action: do-nothing, Blue reward:0.85\n", - "step: 1929, Red action: do-nothing, Blue reward:0.85\n", - "step: 1930, Red action: do-nothing, Blue reward:0.85\n", - "step: 1931, Red action: do-nothing, Blue reward:0.85\n", - "step: 1932, Red action: do-nothing, Blue reward:0.85\n", - "step: 1933, Red action: do-nothing, Blue reward:0.85\n", - "step: 1934, Red action: do-nothing, Blue reward:0.85\n", - "step: 1935, Red action: do-nothing, Blue reward:0.85\n", - "step: 1936, Red action: do-nothing, Blue reward:0.85\n", - "step: 1937, Red action: do-nothing, Blue reward:0.85\n", - "step: 1938, Red action: do-nothing, Blue reward:0.85\n", - "step: 1939, Red action: do-nothing, Blue reward:0.85\n", - "step: 1940, Red action: do-nothing, Blue reward:0.85\n", - "step: 1941, Red action: do-nothing, Blue reward:0.85\n", - "step: 1942, Red action: do-nothing, Blue reward:0.85\n", - "step: 1943, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1944, Red action: do-nothing, Blue reward:0.85\n", - "step: 1945, Red action: do-nothing, Blue reward:0.85\n", - "step: 1946, Red action: do-nothing, Blue reward:0.85\n", - "step: 1947, Red action: do-nothing, Blue reward:0.85\n", - "step: 1948, Red action: do-nothing, Blue reward:0.85\n", - "step: 1949, Red action: do-nothing, Blue reward:0.85\n", - "step: 1950, Red action: do-nothing, Blue reward:0.85\n", - "step: 1951, Red action: do-nothing, Blue reward:0.85\n", - "step: 1952, Red action: do-nothing, Blue reward:0.85\n", - "step: 1953, Red action: do-nothing, Blue reward:0.85\n", - "step: 1954, Red action: do-nothing, Blue reward:0.85\n", - "step: 1955, Red action: do-nothing, Blue reward:0.85\n", - "step: 1956, Red action: do-nothing, Blue reward:0.85\n", - "step: 1957, Red action: do-nothing, Blue reward:0.85\n", - "step: 1958, Red action: do-nothing, Blue reward:0.85\n", - "step: 1959, Red action: do-nothing, Blue reward:0.85\n", - "step: 1960, Red action: do-nothing, Blue reward:0.85\n", - "step: 1961, Red action: do-nothing, Blue reward:0.85\n", - "step: 1962, Red action: do-nothing, Blue reward:0.85\n", - "step: 1963, Red action: do-nothing, Blue reward:0.85\n", - "step: 1964, Red action: do-nothing, Blue reward:0.85\n", - "step: 1965, Red action: do-nothing, Blue reward:0.85\n", - "step: 1966, Red action: do-nothing, Blue reward:0.85\n", - "step: 1967, Red action: do-nothing, Blue reward:0.85\n", - "step: 1968, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1969, Red action: do-nothing, Blue reward:0.85\n", - "step: 1970, Red action: do-nothing, Blue reward:0.85\n", - "step: 1971, Red action: do-nothing, Blue reward:0.85\n", - "step: 1972, Red action: do-nothing, Blue reward:0.85\n", - "step: 1973, Red action: do-nothing, Blue reward:0.85\n", - "step: 1974, Red action: do-nothing, Blue reward:0.85\n", - "step: 1975, Red action: do-nothing, Blue reward:0.85\n", - "step: 1976, Red action: do-nothing, Blue reward:0.85\n", - "step: 1977, Red action: do-nothing, Blue reward:0.85\n", - "step: 1978, Red action: do-nothing, Blue reward:0.85\n", - "step: 1979, Red action: do-nothing, Blue reward:0.85\n", - "step: 1980, Red action: do-nothing, Blue reward:0.85\n", - "step: 1981, Red action: do-nothing, Blue reward:0.85\n", - "step: 1982, Red action: do-nothing, Blue reward:0.85\n", - "step: 1983, Red action: do-nothing, Blue reward:0.85\n", - "step: 1984, Red action: do-nothing, Blue reward:0.85\n", - "step: 1985, Red action: do-nothing, Blue reward:0.85\n", - "step: 1986, Red action: do-nothing, Blue reward:0.85\n", - "step: 1987, Red action: node-application-execute, Blue reward:0.85\n", - "step: 1988, Red action: do-nothing, Blue reward:0.85\n", - "step: 1989, Red action: do-nothing, Blue reward:0.85\n", - "step: 1990, Red action: do-nothing, Blue reward:0.85\n", - "step: 1991, Red action: do-nothing, Blue reward:0.85\n", - "step: 1992, Red action: do-nothing, Blue reward:0.85\n", - "step: 1993, Red action: do-nothing, Blue reward:0.85\n", - "step: 1994, Red action: do-nothing, Blue reward:0.85\n", - "step: 1995, Red action: do-nothing, Blue reward:0.85\n", - "step: 1996, Red action: do-nothing, Blue reward:0.85\n", - "step: 1997, Red action: do-nothing, Blue reward:0.85\n", - "step: 1998, Red action: do-nothing, Blue reward:0.85\n", - "step: 1999, Red action: do-nothing, Blue reward:0.85\n", - "step: 2000, Red action: do-nothing, Blue reward:0.85\n", - "step: 2001, Red action: do-nothing, Blue reward:0.85\n", - "step: 2002, Red action: do-nothing, Blue reward:0.85\n", - "step: 2003, Red action: do-nothing, Blue reward:0.85\n", - "step: 2004, Red action: do-nothing, Blue reward:0.85\n", - "step: 2005, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2006, Red action: do-nothing, Blue reward:0.85\n", - "step: 2007, Red action: do-nothing, Blue reward:0.85\n", - "step: 2008, Red action: do-nothing, Blue reward:0.85\n", - "step: 2009, Red action: do-nothing, Blue reward:0.85\n", - "step: 2010, Red action: do-nothing, Blue reward:0.85\n", - "step: 2011, Red action: do-nothing, Blue reward:0.85\n", - "step: 2012, Red action: do-nothing, Blue reward:0.85\n", - "step: 2013, Red action: do-nothing, Blue reward:0.85\n", - "step: 2014, Red action: do-nothing, Blue reward:0.85\n", - "step: 2015, Red action: do-nothing, Blue reward:0.85\n", - "step: 2016, Red action: do-nothing, Blue reward:0.85\n", - "step: 2017, Red action: do-nothing, Blue reward:0.85\n", - "step: 2018, Red action: do-nothing, Blue reward:0.85\n", - "step: 2019, Red action: do-nothing, Blue reward:0.85\n", - "step: 2020, Red action: do-nothing, Blue reward:0.85\n", - "step: 2021, Red action: do-nothing, Blue reward:0.85\n", - "step: 2022, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2023, Red action: do-nothing, Blue reward:0.85\n", - "step: 2024, Red action: do-nothing, Blue reward:0.85\n", - "step: 2025, Red action: do-nothing, Blue reward:0.85\n", - "step: 2026, Red action: do-nothing, Blue reward:0.85\n", - "step: 2027, Red action: do-nothing, Blue reward:0.85\n", - "step: 2028, Red action: do-nothing, Blue reward:0.85\n", - "step: 2029, Red action: do-nothing, Blue reward:0.85\n", - "step: 2030, Red action: do-nothing, Blue reward:0.85\n", - "step: 2031, Red action: do-nothing, Blue reward:0.85\n", - "step: 2032, Red action: do-nothing, Blue reward:0.85\n", - "step: 2033, Red action: do-nothing, Blue reward:0.85\n", - "step: 2034, Red action: do-nothing, Blue reward:0.85\n", - "step: 2035, Red action: do-nothing, Blue reward:0.85\n", - "step: 2036, Red action: do-nothing, Blue reward:0.85\n", - "step: 2037, Red action: do-nothing, Blue reward:0.85\n", - "step: 2038, Red action: do-nothing, Blue reward:0.85\n", - "step: 2039, Red action: do-nothing, Blue reward:0.85\n", - "step: 2040, Red action: do-nothing, Blue reward:0.85\n", - "step: 2041, Red action: do-nothing, Blue reward:0.85\n", - "step: 2042, Red action: do-nothing, Blue reward:0.85\n", - "step: 2043, Red action: do-nothing, Blue reward:0.85\n", - "step: 2044, Red action: do-nothing, Blue reward:0.85\n", - "step: 2045, Red action: do-nothing, Blue reward:0.85\n", - "step: 2046, Red action: do-nothing, Blue reward:0.85\n", - "step: 2047, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2048, Red action: do-nothing, Blue reward:0.85\n", - "step: 2049, Red action: do-nothing, Blue reward:0.85\n", - "step: 2050, Red action: do-nothing, Blue reward:0.85\n", - "step: 2051, Red action: do-nothing, Blue reward:0.85\n", - "step: 2052, Red action: do-nothing, Blue reward:0.85\n", - "step: 2053, Red action: do-nothing, Blue reward:0.85\n", - "step: 2054, Red action: do-nothing, Blue reward:0.85\n", - "step: 2055, Red action: do-nothing, Blue reward:0.85\n", - "step: 2056, Red action: do-nothing, Blue reward:0.85\n", - "step: 2057, Red action: do-nothing, Blue reward:0.85\n", - "step: 2058, Red action: do-nothing, Blue reward:0.85\n", - "step: 2059, Red action: do-nothing, Blue reward:0.85\n", - "step: 2060, Red action: do-nothing, Blue reward:0.85\n", - "step: 2061, Red action: do-nothing, Blue reward:0.85\n", - "step: 2062, Red action: do-nothing, Blue reward:0.85\n", - "step: 2063, Red action: do-nothing, Blue reward:0.85\n", - "step: 2064, Red action: do-nothing, Blue reward:0.85\n", - "step: 2065, Red action: do-nothing, Blue reward:0.85\n", - "step: 2066, Red action: do-nothing, Blue reward:0.85\n", - "step: 2067, Red action: do-nothing, Blue reward:0.85\n", - "step: 2068, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2069, Red action: do-nothing, Blue reward:0.85\n", - "step: 2070, Red action: do-nothing, Blue reward:0.85\n", - "step: 2071, Red action: do-nothing, Blue reward:0.85\n", - "step: 2072, Red action: do-nothing, Blue reward:0.85\n", - "step: 2073, Red action: do-nothing, Blue reward:0.85\n", - "step: 2074, Red action: do-nothing, Blue reward:0.85\n", - "step: 2075, Red action: do-nothing, Blue reward:0.85\n", - "step: 2076, Red action: do-nothing, Blue reward:0.85\n", - "step: 2077, Red action: do-nothing, Blue reward:0.85\n", - "step: 2078, Red action: do-nothing, Blue reward:0.85\n", - "step: 2079, Red action: do-nothing, Blue reward:0.85\n", - "step: 2080, Red action: do-nothing, Blue reward:0.85\n", - "step: 2081, Red action: do-nothing, Blue reward:0.85\n", - "step: 2082, Red action: do-nothing, Blue reward:0.85\n", - "step: 2083, Red action: do-nothing, Blue reward:0.85\n", - "step: 2084, Red action: do-nothing, Blue reward:0.85\n", - "step: 2085, Red action: do-nothing, Blue reward:0.85\n", - "step: 2086, Red action: do-nothing, Blue reward:0.85\n", - "step: 2087, Red action: do-nothing, Blue reward:0.85\n", - "step: 2088, Red action: do-nothing, Blue reward:0.85\n", - "step: 2089, Red action: do-nothing, Blue reward:0.85\n", - "step: 2090, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2091, Red action: do-nothing, Blue reward:0.85\n", - "step: 2092, Red action: do-nothing, Blue reward:0.85\n", - "step: 2093, Red action: do-nothing, Blue reward:0.85\n", - "step: 2094, Red action: do-nothing, Blue reward:0.85\n", - "step: 2095, Red action: do-nothing, Blue reward:0.85\n", - "step: 2096, Red action: do-nothing, Blue reward:0.85\n", - "step: 2097, Red action: do-nothing, Blue reward:0.85\n", - "step: 2098, Red action: do-nothing, Blue reward:0.85\n", - "step: 2099, Red action: do-nothing, Blue reward:0.85\n", - "step: 2100, Red action: do-nothing, Blue reward:0.85\n", - "step: 2101, Red action: do-nothing, Blue reward:0.85\n", - "step: 2102, Red action: do-nothing, Blue reward:0.85\n", - "step: 2103, Red action: do-nothing, Blue reward:0.85\n", - "step: 2104, Red action: do-nothing, Blue reward:0.85\n", - "step: 2105, Red action: do-nothing, Blue reward:0.85\n", - "step: 2106, Red action: do-nothing, Blue reward:0.85\n", - "step: 2107, Red action: do-nothing, Blue reward:0.85\n", - "step: 2108, Red action: do-nothing, Blue reward:0.85\n", - "step: 2109, Red action: do-nothing, Blue reward:0.85\n", - "step: 2110, Red action: do-nothing, Blue reward:0.85\n", - "step: 2111, Red action: do-nothing, Blue reward:0.85\n", - "step: 2112, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2113, Red action: do-nothing, Blue reward:0.85\n", - "step: 2114, Red action: do-nothing, Blue reward:0.85\n", - "step: 2115, Red action: do-nothing, Blue reward:0.85\n", - "step: 2116, Red action: do-nothing, Blue reward:0.85\n", - "step: 2117, Red action: do-nothing, Blue reward:0.85\n", - "step: 2118, Red action: do-nothing, Blue reward:0.85\n", - "step: 2119, Red action: do-nothing, Blue reward:0.85\n", - "step: 2120, Red action: do-nothing, Blue reward:0.85\n", - "step: 2121, Red action: do-nothing, Blue reward:0.85\n", - "step: 2122, Red action: do-nothing, Blue reward:0.85\n", - "step: 2123, Red action: do-nothing, Blue reward:0.85\n", - "step: 2124, Red action: do-nothing, Blue reward:0.85\n", - "step: 2125, Red action: do-nothing, Blue reward:0.85\n", - "step: 2126, Red action: do-nothing, Blue reward:0.85\n", - "step: 2127, Red action: do-nothing, Blue reward:0.85\n", - "step: 2128, Red action: do-nothing, Blue reward:0.85\n", - "step: 2129, Red action: do-nothing, Blue reward:0.85\n", - "step: 2130, Red action: do-nothing, Blue reward:0.85\n", - "step: 2131, Red action: do-nothing, Blue reward:0.85\n", - "step: 2132, Red action: do-nothing, Blue reward:0.85\n", - "step: 2133, Red action: do-nothing, Blue reward:0.85\n", - "step: 2134, Red action: do-nothing, Blue reward:0.85\n", - "step: 2135, Red action: do-nothing, Blue reward:0.85\n", - "step: 2136, Red action: do-nothing, Blue reward:0.85\n", - "step: 2137, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2138, Red action: do-nothing, Blue reward:0.85\n", - "step: 2139, Red action: do-nothing, Blue reward:0.85\n", - "step: 2140, Red action: do-nothing, Blue reward:0.85\n", - "step: 2141, Red action: do-nothing, Blue reward:0.85\n", - "step: 2142, Red action: do-nothing, Blue reward:0.85\n", - "step: 2143, Red action: do-nothing, Blue reward:0.85\n", - "step: 2144, Red action: do-nothing, Blue reward:0.85\n", - "step: 2145, Red action: do-nothing, Blue reward:0.85\n", - "step: 2146, Red action: do-nothing, Blue reward:0.85\n", - "step: 2147, Red action: do-nothing, Blue reward:0.85\n", - "step: 2148, Red action: do-nothing, Blue reward:0.85\n", - "step: 2149, Red action: do-nothing, Blue reward:0.85\n", - "step: 2150, Red action: do-nothing, Blue reward:0.85\n", - "step: 2151, Red action: do-nothing, Blue reward:0.85\n", - "step: 2152, Red action: do-nothing, Blue reward:0.85\n", - "step: 2153, Red action: do-nothing, Blue reward:0.85\n", - "step: 2154, Red action: do-nothing, Blue reward:0.85\n", - "step: 2155, Red action: do-nothing, Blue reward:0.85\n", - "step: 2156, Red action: do-nothing, Blue reward:0.85\n", - "step: 2157, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2158, Red action: do-nothing, Blue reward:0.85\n", - "step: 2159, Red action: do-nothing, Blue reward:0.85\n", - "step: 2160, Red action: do-nothing, Blue reward:0.85\n", - "step: 2161, Red action: do-nothing, Blue reward:0.85\n", - "step: 2162, Red action: do-nothing, Blue reward:0.85\n", - "step: 2163, Red action: do-nothing, Blue reward:0.85\n", - "step: 2164, Red action: do-nothing, Blue reward:0.85\n", - "step: 2165, Red action: do-nothing, Blue reward:0.85\n", - "step: 2166, Red action: do-nothing, Blue reward:0.85\n", - "step: 2167, Red action: do-nothing, Blue reward:0.85\n", - "step: 2168, Red action: do-nothing, Blue reward:0.85\n", - "step: 2169, Red action: do-nothing, Blue reward:0.85\n", - "step: 2170, Red action: do-nothing, Blue reward:0.85\n", - "step: 2171, Red action: do-nothing, Blue reward:0.85\n", - "step: 2172, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2173, Red action: do-nothing, Blue reward:0.85\n", - "step: 2174, Red action: do-nothing, Blue reward:0.85\n", - "step: 2175, Red action: do-nothing, Blue reward:0.85\n", - "step: 2176, Red action: do-nothing, Blue reward:0.85\n", - "step: 2177, Red action: do-nothing, Blue reward:0.85\n", - "step: 2178, Red action: do-nothing, Blue reward:0.85\n", - "step: 2179, Red action: do-nothing, Blue reward:0.85\n", - "step: 2180, Red action: do-nothing, Blue reward:0.85\n", - "step: 2181, Red action: do-nothing, Blue reward:0.85\n", - "step: 2182, Red action: do-nothing, Blue reward:0.85\n", - "step: 2183, Red action: do-nothing, Blue reward:0.85\n", - "step: 2184, Red action: do-nothing, Blue reward:0.85\n", - "step: 2185, Red action: do-nothing, Blue reward:0.85\n", - "step: 2186, Red action: do-nothing, Blue reward:0.85\n", - "step: 2187, Red action: do-nothing, Blue reward:0.85\n", - "step: 2188, Red action: do-nothing, Blue reward:0.85\n", - "step: 2189, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2190, Red action: do-nothing, Blue reward:0.85\n", - "step: 2191, Red action: do-nothing, Blue reward:0.85\n", - "step: 2192, Red action: do-nothing, Blue reward:0.85\n", - "step: 2193, Red action: do-nothing, Blue reward:0.85\n", - "step: 2194, Red action: do-nothing, Blue reward:0.85\n", - "step: 2195, Red action: do-nothing, Blue reward:0.85\n", - "step: 2196, Red action: do-nothing, Blue reward:0.85\n", - "step: 2197, Red action: do-nothing, Blue reward:0.85\n", - "step: 2198, Red action: do-nothing, Blue reward:0.85\n", - "step: 2199, Red action: do-nothing, Blue reward:0.85\n", - "step: 2200, Red action: do-nothing, Blue reward:0.85\n", - "step: 2201, Red action: do-nothing, Blue reward:0.85\n", - "step: 2202, Red action: do-nothing, Blue reward:0.85\n", - "step: 2203, Red action: do-nothing, Blue reward:0.85\n", - "step: 2204, Red action: do-nothing, Blue reward:0.85\n", - "step: 2205, Red action: do-nothing, Blue reward:0.85\n", - "step: 2206, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2207, Red action: do-nothing, Blue reward:0.85\n", - "step: 2208, Red action: do-nothing, Blue reward:0.85\n", - "step: 2209, Red action: do-nothing, Blue reward:0.85\n", - "step: 2210, Red action: do-nothing, Blue reward:0.85\n", - "step: 2211, Red action: do-nothing, Blue reward:0.85\n", - "step: 2212, Red action: do-nothing, Blue reward:0.85\n", - "step: 2213, Red action: do-nothing, Blue reward:0.85\n", - "step: 2214, Red action: do-nothing, Blue reward:0.85\n", - "step: 2215, Red action: do-nothing, Blue reward:0.85\n", - "step: 2216, Red action: do-nothing, Blue reward:0.85\n", - "step: 2217, Red action: do-nothing, Blue reward:0.85\n", - "step: 2218, Red action: do-nothing, Blue reward:0.85\n", - "step: 2219, Red action: do-nothing, Blue reward:0.85\n", - "step: 2220, Red action: do-nothing, Blue reward:0.85\n", - "step: 2221, Red action: do-nothing, Blue reward:0.85\n", - "step: 2222, Red action: do-nothing, Blue reward:0.85\n", - "step: 2223, Red action: do-nothing, Blue reward:0.85\n", - "step: 2224, Red action: do-nothing, Blue reward:0.85\n", - "step: 2225, Red action: do-nothing, Blue reward:0.85\n", - "step: 2226, Red action: do-nothing, Blue reward:0.85\n", - "step: 2227, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2228, Red action: do-nothing, Blue reward:0.85\n", - "step: 2229, Red action: do-nothing, Blue reward:0.85\n", - "step: 2230, Red action: do-nothing, Blue reward:0.85\n", - "step: 2231, Red action: do-nothing, Blue reward:0.85\n", - "step: 2232, Red action: do-nothing, Blue reward:0.85\n", - "step: 2233, Red action: do-nothing, Blue reward:0.85\n", - "step: 2234, Red action: do-nothing, Blue reward:0.85\n", - "step: 2235, Red action: do-nothing, Blue reward:0.85\n", - "step: 2236, Red action: do-nothing, Blue reward:0.85\n", - "step: 2237, Red action: do-nothing, Blue reward:0.85\n", - "step: 2238, Red action: do-nothing, Blue reward:0.85\n", - "step: 2239, Red action: do-nothing, Blue reward:0.85\n", - "step: 2240, Red action: do-nothing, Blue reward:0.85\n", - "step: 2241, Red action: do-nothing, Blue reward:0.85\n", - "step: 2242, Red action: do-nothing, Blue reward:0.85\n", - "step: 2243, Red action: do-nothing, Blue reward:0.85\n", - "step: 2244, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2245, Red action: do-nothing, Blue reward:0.85\n", - "step: 2246, Red action: do-nothing, Blue reward:0.85\n", - "step: 2247, Red action: do-nothing, Blue reward:0.85\n", - "step: 2248, Red action: do-nothing, Blue reward:0.85\n", - "step: 2249, Red action: do-nothing, Blue reward:0.85\n", - "step: 2250, Red action: do-nothing, Blue reward:0.85\n", - "step: 2251, Red action: do-nothing, Blue reward:0.85\n", - "step: 2252, Red action: do-nothing, Blue reward:0.85\n", - "step: 2253, Red action: do-nothing, Blue reward:0.85\n", - "step: 2254, Red action: do-nothing, Blue reward:0.85\n", - "step: 2255, Red action: do-nothing, Blue reward:0.85\n", - "step: 2256, Red action: do-nothing, Blue reward:0.85\n", - "step: 2257, Red action: do-nothing, Blue reward:0.85\n", - "step: 2258, Red action: do-nothing, Blue reward:0.85\n", - "step: 2259, Red action: do-nothing, Blue reward:0.85\n", - "step: 2260, Red action: do-nothing, Blue reward:0.85\n", - "step: 2261, Red action: do-nothing, Blue reward:0.85\n", - "step: 2262, Red action: do-nothing, Blue reward:0.85\n", - "step: 2263, Red action: do-nothing, Blue reward:0.85\n", - "step: 2264, Red action: do-nothing, Blue reward:0.85\n", - "step: 2265, Red action: do-nothing, Blue reward:0.85\n", - "step: 2266, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2267, Red action: do-nothing, Blue reward:0.85\n", - "step: 2268, Red action: do-nothing, Blue reward:0.85\n", - "step: 2269, Red action: do-nothing, Blue reward:0.85\n", - "step: 2270, Red action: do-nothing, Blue reward:0.85\n", - "step: 2271, Red action: do-nothing, Blue reward:0.85\n", - "step: 2272, Red action: do-nothing, Blue reward:0.85\n", - "step: 2273, Red action: do-nothing, Blue reward:0.85\n", - "step: 2274, Red action: do-nothing, Blue reward:0.85\n", - "step: 2275, Red action: do-nothing, Blue reward:0.85\n", - "step: 2276, Red action: do-nothing, Blue reward:0.85\n", - "step: 2277, Red action: do-nothing, Blue reward:0.85\n", - "step: 2278, Red action: do-nothing, Blue reward:0.85\n", - "step: 2279, Red action: do-nothing, Blue reward:0.85\n", - "step: 2280, Red action: do-nothing, Blue reward:0.85\n", - "step: 2281, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2282, Red action: do-nothing, Blue reward:0.85\n", - "step: 2283, Red action: do-nothing, Blue reward:0.85\n", - "step: 2284, Red action: do-nothing, Blue reward:0.85\n", - "step: 2285, Red action: do-nothing, Blue reward:0.85\n", - "step: 2286, Red action: do-nothing, Blue reward:0.85\n", - "step: 2287, Red action: do-nothing, Blue reward:0.85\n", - "step: 2288, Red action: do-nothing, Blue reward:0.85\n", - "step: 2289, Red action: do-nothing, Blue reward:0.85\n", - "step: 2290, Red action: do-nothing, Blue reward:0.85\n", - "step: 2291, Red action: do-nothing, Blue reward:0.85\n", - "step: 2292, Red action: do-nothing, Blue reward:0.85\n", - "step: 2293, Red action: do-nothing, Blue reward:0.85\n", - "step: 2294, Red action: do-nothing, Blue reward:0.85\n", - "step: 2295, Red action: do-nothing, Blue reward:0.85\n", - "step: 2296, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2297, Red action: do-nothing, Blue reward:0.85\n", - "step: 2298, Red action: do-nothing, Blue reward:0.85\n", - "step: 2299, Red action: do-nothing, Blue reward:0.85\n", - "step: 2300, Red action: do-nothing, Blue reward:0.85\n", - "step: 2301, Red action: do-nothing, Blue reward:0.85\n", - "step: 2302, Red action: do-nothing, Blue reward:0.85\n", - "step: 2303, Red action: do-nothing, Blue reward:0.85\n", - "step: 2304, Red action: do-nothing, Blue reward:0.85\n", - "step: 2305, Red action: do-nothing, Blue reward:0.85\n", - "step: 2306, Red action: do-nothing, Blue reward:0.85\n", - "step: 2307, Red action: do-nothing, Blue reward:0.85\n", - "step: 2308, Red action: do-nothing, Blue reward:0.85\n", - "step: 2309, Red action: do-nothing, Blue reward:0.85\n", - "step: 2310, Red action: do-nothing, Blue reward:0.85\n", - "step: 2311, Red action: do-nothing, Blue reward:0.85\n", - "step: 2312, Red action: do-nothing, Blue reward:0.85\n", - "step: 2313, Red action: do-nothing, Blue reward:0.85\n", - "step: 2314, Red action: do-nothing, Blue reward:0.85\n", - "step: 2315, Red action: do-nothing, Blue reward:0.85\n", - "step: 2316, Red action: do-nothing, Blue reward:0.85\n", - "step: 2317, Red action: do-nothing, Blue reward:0.85\n", - "step: 2318, Red action: do-nothing, Blue reward:0.85\n", - "step: 2319, Red action: do-nothing, Blue reward:0.85\n", - "step: 2320, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2321, Red action: do-nothing, Blue reward:0.85\n", - "step: 2322, Red action: do-nothing, Blue reward:0.85\n", - "step: 2323, Red action: do-nothing, Blue reward:0.85\n", - "step: 2324, Red action: do-nothing, Blue reward:0.85\n", - "step: 2325, Red action: do-nothing, Blue reward:0.85\n", - "step: 2326, Red action: do-nothing, Blue reward:0.85\n", - "step: 2327, Red action: do-nothing, Blue reward:0.85\n", - "step: 2328, Red action: do-nothing, Blue reward:0.85\n", - "step: 2329, Red action: do-nothing, Blue reward:0.85\n", - "step: 2330, Red action: do-nothing, Blue reward:0.85\n", - "step: 2331, Red action: do-nothing, Blue reward:0.85\n", - "step: 2332, Red action: do-nothing, Blue reward:0.85\n", - "step: 2333, Red action: do-nothing, Blue reward:0.85\n", - "step: 2334, Red action: do-nothing, Blue reward:0.85\n", - "step: 2335, Red action: do-nothing, Blue reward:0.85\n", - "step: 2336, Red action: do-nothing, Blue reward:0.85\n", - "step: 2337, Red action: do-nothing, Blue reward:0.85\n", - "step: 2338, Red action: do-nothing, Blue reward:0.85\n", - "step: 2339, Red action: do-nothing, Blue reward:0.85\n", - "step: 2340, Red action: do-nothing, Blue reward:0.85\n", - "step: 2341, Red action: do-nothing, Blue reward:0.85\n", - "step: 2342, Red action: do-nothing, Blue reward:0.85\n", - "step: 2343, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2344, Red action: do-nothing, Blue reward:0.85\n", - "step: 2345, Red action: do-nothing, Blue reward:0.85\n", - "step: 2346, Red action: do-nothing, Blue reward:0.85\n", - "step: 2347, Red action: do-nothing, Blue reward:0.85\n", - "step: 2348, Red action: do-nothing, Blue reward:0.85\n", - "step: 2349, Red action: do-nothing, Blue reward:0.85\n", - "step: 2350, Red action: do-nothing, Blue reward:0.85\n", - "step: 2351, Red action: do-nothing, Blue reward:0.85\n", - "step: 2352, Red action: do-nothing, Blue reward:0.85\n", - "step: 2353, Red action: do-nothing, Blue reward:0.85\n", - "step: 2354, Red action: do-nothing, Blue reward:0.85\n", - "step: 2355, Red action: do-nothing, Blue reward:0.85\n", - "step: 2356, Red action: do-nothing, Blue reward:0.85\n", - "step: 2357, Red action: do-nothing, Blue reward:0.85\n", - "step: 2358, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2359, Red action: do-nothing, Blue reward:0.85\n", - "step: 2360, Red action: do-nothing, Blue reward:0.85\n", - "step: 2361, Red action: do-nothing, Blue reward:0.85\n", - "step: 2362, Red action: do-nothing, Blue reward:0.85\n", - "step: 2363, Red action: do-nothing, Blue reward:0.85\n", - "step: 2364, Red action: do-nothing, Blue reward:0.85\n", - "step: 2365, Red action: do-nothing, Blue reward:0.85\n", - "step: 2366, Red action: do-nothing, Blue reward:0.85\n", - "step: 2367, Red action: do-nothing, Blue reward:0.85\n", - "step: 2368, Red action: do-nothing, Blue reward:0.85\n", - "step: 2369, Red action: do-nothing, Blue reward:0.85\n", - "step: 2370, Red action: do-nothing, Blue reward:0.85\n", - "step: 2371, Red action: do-nothing, Blue reward:0.85\n", - "step: 2372, Red action: do-nothing, Blue reward:0.85\n", - "step: 2373, Red action: do-nothing, Blue reward:0.85\n", - "step: 2374, Red action: do-nothing, Blue reward:0.85\n", - "step: 2375, Red action: do-nothing, Blue reward:0.85\n", - "step: 2376, Red action: do-nothing, Blue reward:0.85\n", - "step: 2377, Red action: do-nothing, Blue reward:0.85\n", - "step: 2378, Red action: do-nothing, Blue reward:0.85\n", - "step: 2379, Red action: do-nothing, Blue reward:0.85\n", - "step: 2380, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2381, Red action: do-nothing, Blue reward:0.85\n", - "step: 2382, Red action: do-nothing, Blue reward:0.85\n", - "step: 2383, Red action: do-nothing, Blue reward:0.85\n", - "step: 2384, Red action: do-nothing, Blue reward:0.85\n", - "step: 2385, Red action: do-nothing, Blue reward:0.85\n", - "step: 2386, Red action: do-nothing, Blue reward:0.85\n", - "step: 2387, Red action: do-nothing, Blue reward:0.85\n", - "step: 2388, Red action: do-nothing, Blue reward:0.85\n", - "step: 2389, Red action: do-nothing, Blue reward:0.85\n", - "step: 2390, Red action: do-nothing, Blue reward:0.85\n", - "step: 2391, Red action: do-nothing, Blue reward:0.85\n", - "step: 2392, Red action: do-nothing, Blue reward:0.85\n", - "step: 2393, Red action: do-nothing, Blue reward:0.85\n", - "step: 2394, Red action: do-nothing, Blue reward:0.85\n", - "step: 2395, Red action: do-nothing, Blue reward:0.85\n", - "step: 2396, Red action: do-nothing, Blue reward:0.85\n", - "step: 2397, Red action: do-nothing, Blue reward:0.85\n", - "step: 2398, Red action: do-nothing, Blue reward:0.85\n", - "step: 2399, Red action: do-nothing, Blue reward:0.85\n", - "step: 2400, Red action: do-nothing, Blue reward:0.85\n", - "step: 2401, Red action: do-nothing, Blue reward:0.85\n", - "step: 2402, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2403, Red action: do-nothing, Blue reward:0.85\n", - "step: 2404, Red action: do-nothing, Blue reward:0.85\n", - "step: 2405, Red action: do-nothing, Blue reward:0.85\n", - "step: 2406, Red action: do-nothing, Blue reward:0.85\n", - "step: 2407, Red action: do-nothing, Blue reward:0.85\n", - "step: 2408, Red action: do-nothing, Blue reward:0.85\n", - "step: 2409, Red action: do-nothing, Blue reward:0.85\n", - "step: 2410, Red action: do-nothing, Blue reward:0.85\n", - "step: 2411, Red action: do-nothing, Blue reward:0.85\n", - "step: 2412, Red action: do-nothing, Blue reward:0.85\n", - "step: 2413, Red action: do-nothing, Blue reward:0.85\n", - "step: 2414, Red action: do-nothing, Blue reward:0.85\n", - "step: 2415, Red action: do-nothing, Blue reward:0.85\n", - "step: 2416, Red action: do-nothing, Blue reward:0.85\n", - "step: 2417, Red action: do-nothing, Blue reward:0.85\n", - "step: 2418, Red action: do-nothing, Blue reward:0.85\n", - "step: 2419, Red action: do-nothing, Blue reward:0.85\n", - "step: 2420, Red action: do-nothing, Blue reward:0.85\n", - "step: 2421, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2422, Red action: do-nothing, Blue reward:0.85\n", - "step: 2423, Red action: do-nothing, Blue reward:0.85\n", - "step: 2424, Red action: do-nothing, Blue reward:0.85\n", - "step: 2425, Red action: do-nothing, Blue reward:0.85\n", - "step: 2426, Red action: do-nothing, Blue reward:0.85\n", - "step: 2427, Red action: do-nothing, Blue reward:0.85\n", - "step: 2428, Red action: do-nothing, Blue reward:0.85\n", - "step: 2429, Red action: do-nothing, Blue reward:0.85\n", - "step: 2430, Red action: do-nothing, Blue reward:0.85\n", - "step: 2431, Red action: do-nothing, Blue reward:0.85\n", - "step: 2432, Red action: do-nothing, Blue reward:0.85\n", - "step: 2433, Red action: do-nothing, Blue reward:0.85\n", - "step: 2434, Red action: do-nothing, Blue reward:0.85\n", - "step: 2435, Red action: do-nothing, Blue reward:0.85\n", - "step: 2436, Red action: do-nothing, Blue reward:0.85\n", - "step: 2437, Red action: do-nothing, Blue reward:0.85\n", - "step: 2438, Red action: do-nothing, Blue reward:0.85\n", - "step: 2439, Red action: do-nothing, Blue reward:0.85\n", - "step: 2440, Red action: do-nothing, Blue reward:0.85\n", - "step: 2441, Red action: do-nothing, Blue reward:0.85\n", - "step: 2442, Red action: do-nothing, Blue reward:0.85\n", - "step: 2443, Red action: do-nothing, Blue reward:0.85\n", - "step: 2444, Red action: do-nothing, Blue reward:0.85\n", - "step: 2445, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2446, Red action: do-nothing, Blue reward:0.85\n", - "step: 2447, Red action: do-nothing, Blue reward:0.85\n", - "step: 2448, Red action: do-nothing, Blue reward:0.85\n", - "step: 2449, Red action: do-nothing, Blue reward:0.85\n", - "step: 2450, Red action: do-nothing, Blue reward:0.85\n", - "step: 2451, Red action: do-nothing, Blue reward:0.85\n", - "step: 2452, Red action: do-nothing, Blue reward:0.85\n", - "step: 2453, Red action: do-nothing, Blue reward:0.85\n", - "step: 2454, Red action: do-nothing, Blue reward:0.85\n", - "step: 2455, Red action: do-nothing, Blue reward:0.85\n", - "step: 2456, Red action: do-nothing, Blue reward:0.85\n", - "step: 2457, Red action: do-nothing, Blue reward:0.85\n", - "step: 2458, Red action: do-nothing, Blue reward:0.85\n", - "step: 2459, Red action: do-nothing, Blue reward:0.85\n", - "step: 2460, Red action: do-nothing, Blue reward:0.85\n", - "step: 2461, Red action: do-nothing, Blue reward:0.85\n", - "step: 2462, Red action: do-nothing, Blue reward:0.85\n", - "step: 2463, Red action: do-nothing, Blue reward:0.85\n", - "step: 2464, Red action: do-nothing, Blue reward:0.85\n", - "step: 2465, Red action: do-nothing, Blue reward:0.85\n", - "step: 2466, Red action: do-nothing, Blue reward:0.85\n", - "step: 2467, Red action: do-nothing, Blue reward:0.85\n", - "step: 2468, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2469, Red action: do-nothing, Blue reward:0.85\n", - "step: 2470, Red action: do-nothing, Blue reward:0.85\n", - "step: 2471, Red action: do-nothing, Blue reward:0.85\n", - "step: 2472, Red action: do-nothing, Blue reward:0.85\n", - "step: 2473, Red action: do-nothing, Blue reward:0.85\n", - "step: 2474, Red action: do-nothing, Blue reward:0.85\n", - "step: 2475, Red action: do-nothing, Blue reward:0.85\n", - "step: 2476, Red action: do-nothing, Blue reward:0.85\n", - "step: 2477, Red action: do-nothing, Blue reward:0.85\n", - "step: 2478, Red action: do-nothing, Blue reward:0.85\n", - "step: 2479, Red action: do-nothing, Blue reward:0.85\n", - "step: 2480, Red action: do-nothing, Blue reward:0.85\n", - "step: 2481, Red action: do-nothing, Blue reward:0.85\n", - "step: 2482, Red action: do-nothing, Blue reward:0.85\n", - "step: 2483, Red action: do-nothing, Blue reward:0.85\n", - "step: 2484, Red action: do-nothing, Blue reward:0.85\n", - "step: 2485, Red action: do-nothing, Blue reward:0.85\n", - "step: 2486, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2487, Red action: do-nothing, Blue reward:0.85\n", - "step: 2488, Red action: do-nothing, Blue reward:0.85\n", - "step: 2489, Red action: do-nothing, Blue reward:0.85\n", - "step: 2490, Red action: do-nothing, Blue reward:0.85\n", - "step: 2491, Red action: do-nothing, Blue reward:0.85\n", - "step: 2492, Red action: do-nothing, Blue reward:0.85\n", - "step: 2493, Red action: do-nothing, Blue reward:0.85\n", - "step: 2494, Red action: do-nothing, Blue reward:0.85\n", - "step: 2495, Red action: do-nothing, Blue reward:0.85\n", - "step: 2496, Red action: do-nothing, Blue reward:0.85\n", - "step: 2497, Red action: do-nothing, Blue reward:0.85\n", - "step: 2498, Red action: do-nothing, Blue reward:0.85\n", - "step: 2499, Red action: do-nothing, Blue reward:0.85\n", - "step: 2500, Red action: do-nothing, Blue reward:0.85\n", - "step: 2501, Red action: do-nothing, Blue reward:0.85\n", - "step: 2502, Red action: do-nothing, Blue reward:0.85\n", - "step: 2503, Red action: do-nothing, Blue reward:0.85\n", - "step: 2504, Red action: do-nothing, Blue reward:0.85\n", - "step: 2505, Red action: do-nothing, Blue reward:0.85\n", - "step: 2506, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2507, Red action: do-nothing, Blue reward:0.85\n", - "step: 2508, Red action: do-nothing, Blue reward:0.85\n", - "step: 2509, Red action: do-nothing, Blue reward:0.85\n", - "step: 2510, Red action: do-nothing, Blue reward:0.85\n", - "step: 2511, Red action: do-nothing, Blue reward:0.85\n", - "step: 2512, Red action: do-nothing, Blue reward:0.85\n", - "step: 2513, Red action: do-nothing, Blue reward:0.85\n", - "step: 2514, Red action: do-nothing, Blue reward:0.85\n", - "step: 2515, Red action: do-nothing, Blue reward:0.85\n", - "step: 2516, Red action: do-nothing, Blue reward:0.85\n", - "step: 2517, Red action: do-nothing, Blue reward:0.85\n", - "step: 2518, Red action: do-nothing, Blue reward:0.85\n", - "step: 2519, Red action: do-nothing, Blue reward:0.85\n", - "step: 2520, Red action: do-nothing, Blue reward:0.85\n", - "step: 2521, Red action: do-nothing, Blue reward:0.85\n", - "step: 2522, Red action: do-nothing, Blue reward:0.85\n", - "step: 2523, Red action: do-nothing, Blue reward:0.85\n", - "step: 2524, Red action: do-nothing, Blue reward:0.85\n", - "step: 2525, Red action: do-nothing, Blue reward:0.85\n", - "step: 2526, Red action: do-nothing, Blue reward:0.85\n", - "step: 2527, Red action: do-nothing, Blue reward:0.85\n", - "step: 2528, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2529, Red action: do-nothing, Blue reward:0.85\n", - "step: 2530, Red action: do-nothing, Blue reward:0.85\n", - "step: 2531, Red action: do-nothing, Blue reward:0.85\n", - "step: 2532, Red action: do-nothing, Blue reward:0.85\n", - "step: 2533, Red action: do-nothing, Blue reward:0.85\n", - "step: 2534, Red action: do-nothing, Blue reward:0.85\n", - "step: 2535, Red action: do-nothing, Blue reward:0.85\n", - "step: 2536, Red action: do-nothing, Blue reward:0.85\n", - "step: 2537, Red action: do-nothing, Blue reward:0.85\n", - "step: 2538, Red action: do-nothing, Blue reward:0.85\n", - "step: 2539, Red action: do-nothing, Blue reward:0.85\n", - "step: 2540, Red action: do-nothing, Blue reward:0.85\n", - "step: 2541, Red action: do-nothing, Blue reward:0.85\n", - "step: 2542, Red action: do-nothing, Blue reward:0.85\n", - "step: 2543, Red action: do-nothing, Blue reward:0.85\n", - "step: 2544, Red action: do-nothing, Blue reward:0.85\n", - "step: 2545, Red action: do-nothing, Blue reward:0.85\n", - "step: 2546, Red action: do-nothing, Blue reward:0.85\n", - "step: 2547, Red action: do-nothing, Blue reward:0.85\n", - "step: 2548, Red action: do-nothing, Blue reward:0.85\n", - "step: 2549, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2550, Red action: do-nothing, Blue reward:0.85\n", - "step: 2551, Red action: do-nothing, Blue reward:0.85\n", - "step: 2552, Red action: do-nothing, Blue reward:0.85\n", - "step: 2553, Red action: do-nothing, Blue reward:0.85\n", - "step: 2554, Red action: do-nothing, Blue reward:0.85\n", - "step: 2555, Red action: do-nothing, Blue reward:0.85\n", - "step: 2556, Red action: do-nothing, Blue reward:0.85\n", - "step: 2557, Red action: do-nothing, Blue reward:0.85\n", - "step: 2558, Red action: do-nothing, Blue reward:0.85\n", - "step: 2559, Red action: do-nothing, Blue reward:0.85\n", - "step: 2560, Red action: do-nothing, Blue reward:0.85\n", - "step: 2561, Red action: do-nothing, Blue reward:0.85\n", - "step: 2562, Red action: do-nothing, Blue reward:0.85\n", - "step: 2563, Red action: do-nothing, Blue reward:0.85\n", - "step: 2564, Red action: do-nothing, Blue reward:0.85\n", - "step: 2565, Red action: do-nothing, Blue reward:0.85\n", - "step: 2566, Red action: do-nothing, Blue reward:0.85\n", - "step: 2567, Red action: do-nothing, Blue reward:0.85\n", - "step: 2568, Red action: do-nothing, Blue reward:0.85\n", - "step: 2569, Red action: do-nothing, Blue reward:0.85\n", - "step: 2570, Red action: do-nothing, Blue reward:0.85\n", - "step: 2571, Red action: do-nothing, Blue reward:0.85\n", - "step: 2572, Red action: do-nothing, Blue reward:0.85\n", - "step: 2573, Red action: do-nothing, Blue reward:0.85\n", - "step: 2574, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2575, Red action: do-nothing, Blue reward:0.85\n", - "step: 2576, Red action: do-nothing, Blue reward:0.85\n", - "step: 2577, Red action: do-nothing, Blue reward:0.85\n", - "step: 2578, Red action: do-nothing, Blue reward:0.85\n", - "step: 2579, Red action: do-nothing, Blue reward:0.85\n", - "step: 2580, Red action: do-nothing, Blue reward:0.85\n", - "step: 2581, Red action: do-nothing, Blue reward:0.85\n", - "step: 2582, Red action: do-nothing, Blue reward:0.85\n", - "step: 2583, Red action: do-nothing, Blue reward:0.85\n", - "step: 2584, Red action: do-nothing, Blue reward:0.85\n", - "step: 2585, Red action: do-nothing, Blue reward:0.85\n", - "step: 2586, Red action: do-nothing, Blue reward:0.85\n", - "step: 2587, Red action: do-nothing, Blue reward:0.85\n", - "step: 2588, Red action: do-nothing, Blue reward:0.85\n", - "step: 2589, Red action: do-nothing, Blue reward:0.85\n", - "step: 2590, Red action: do-nothing, Blue reward:0.85\n", - "step: 2591, Red action: do-nothing, Blue reward:0.85\n", - "step: 2592, Red action: do-nothing, Blue reward:0.85\n", - "step: 2593, Red action: do-nothing, Blue reward:0.85\n", - "step: 2594, Red action: do-nothing, Blue reward:0.85\n", - "step: 2595, Red action: do-nothing, Blue reward:0.85\n", - "step: 2596, Red action: do-nothing, Blue reward:0.85\n", - "step: 2597, Red action: do-nothing, Blue reward:0.85\n", - "step: 2598, Red action: do-nothing, Blue reward:0.85\n", - "step: 2599, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2600, Red action: do-nothing, Blue reward:0.85\n", - "step: 2601, Red action: do-nothing, Blue reward:0.85\n", - "step: 2602, Red action: do-nothing, Blue reward:0.85\n", - "step: 2603, Red action: do-nothing, Blue reward:0.85\n", - "step: 2604, Red action: do-nothing, Blue reward:0.85\n", - "step: 2605, Red action: do-nothing, Blue reward:0.85\n", - "step: 2606, Red action: do-nothing, Blue reward:0.85\n", - "step: 2607, Red action: do-nothing, Blue reward:0.85\n", - "step: 2608, Red action: do-nothing, Blue reward:0.85\n", - "step: 2609, Red action: do-nothing, Blue reward:0.85\n", - "step: 2610, Red action: do-nothing, Blue reward:0.85\n", - "step: 2611, Red action: do-nothing, Blue reward:0.85\n", - "step: 2612, Red action: do-nothing, Blue reward:0.85\n", - "step: 2613, Red action: do-nothing, Blue reward:0.85\n", - "step: 2614, Red action: do-nothing, Blue reward:0.85\n", - "step: 2615, Red action: do-nothing, Blue reward:0.85\n", - "step: 2616, Red action: do-nothing, Blue reward:0.85\n", - "step: 2617, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2618, Red action: do-nothing, Blue reward:0.85\n", - "step: 2619, Red action: do-nothing, Blue reward:0.85\n", - "step: 2620, Red action: do-nothing, Blue reward:0.85\n", - "step: 2621, Red action: do-nothing, Blue reward:0.85\n", - "step: 2622, Red action: do-nothing, Blue reward:0.85\n", - "step: 2623, Red action: do-nothing, Blue reward:0.85\n", - "step: 2624, Red action: do-nothing, Blue reward:0.85\n", - "step: 2625, Red action: do-nothing, Blue reward:0.85\n", - "step: 2626, Red action: do-nothing, Blue reward:0.85\n", - "step: 2627, Red action: do-nothing, Blue reward:0.85\n", - "step: 2628, Red action: do-nothing, Blue reward:0.85\n", - "step: 2629, Red action: do-nothing, Blue reward:0.85\n", - "step: 2630, Red action: do-nothing, Blue reward:0.85\n", - "step: 2631, Red action: do-nothing, Blue reward:0.85\n", - "step: 2632, Red action: do-nothing, Blue reward:0.85\n", - "step: 2633, Red action: do-nothing, Blue reward:0.85\n", - "step: 2634, Red action: do-nothing, Blue reward:0.85\n", - "step: 2635, Red action: do-nothing, Blue reward:0.85\n", - "step: 2636, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2637, Red action: do-nothing, Blue reward:0.85\n", - "step: 2638, Red action: do-nothing, Blue reward:0.85\n", - "step: 2639, Red action: do-nothing, Blue reward:0.85\n", - "step: 2640, Red action: do-nothing, Blue reward:0.85\n", - "step: 2641, Red action: do-nothing, Blue reward:0.85\n", - "step: 2642, Red action: do-nothing, Blue reward:0.85\n", - "step: 2643, Red action: do-nothing, Blue reward:0.85\n", - "step: 2644, Red action: do-nothing, Blue reward:0.85\n", - "step: 2645, Red action: do-nothing, Blue reward:0.85\n", - "step: 2646, Red action: do-nothing, Blue reward:0.85\n", - "step: 2647, Red action: do-nothing, Blue reward:0.85\n", - "step: 2648, Red action: do-nothing, Blue reward:0.85\n", - "step: 2649, Red action: do-nothing, Blue reward:0.85\n", - "step: 2650, Red action: do-nothing, Blue reward:0.85\n", - "step: 2651, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2652, Red action: do-nothing, Blue reward:0.85\n", - "step: 2653, Red action: do-nothing, Blue reward:0.85\n", - "step: 2654, Red action: do-nothing, Blue reward:0.85\n", - "step: 2655, Red action: do-nothing, Blue reward:0.85\n", - "step: 2656, Red action: do-nothing, Blue reward:0.85\n", - "step: 2657, Red action: do-nothing, Blue reward:0.85\n", - "step: 2658, Red action: do-nothing, Blue reward:0.85\n", - "step: 2659, Red action: do-nothing, Blue reward:0.85\n", - "step: 2660, Red action: do-nothing, Blue reward:0.85\n", - "step: 2661, Red action: do-nothing, Blue reward:0.85\n", - "step: 2662, Red action: do-nothing, Blue reward:0.85\n", - "step: 2663, Red action: do-nothing, Blue reward:0.85\n", - "step: 2664, Red action: do-nothing, Blue reward:0.85\n", - "step: 2665, Red action: do-nothing, Blue reward:0.85\n", - "step: 2666, Red action: do-nothing, Blue reward:0.85\n", - "step: 2667, Red action: do-nothing, Blue reward:0.85\n", - "step: 2668, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2669, Red action: do-nothing, Blue reward:0.85\n", - "step: 2670, Red action: do-nothing, Blue reward:0.85\n", - "step: 2671, Red action: do-nothing, Blue reward:0.85\n", - "step: 2672, Red action: do-nothing, Blue reward:0.85\n", - "step: 2673, Red action: do-nothing, Blue reward:0.85\n", - "step: 2674, Red action: do-nothing, Blue reward:0.85\n", - "step: 2675, Red action: do-nothing, Blue reward:0.85\n", - "step: 2676, Red action: do-nothing, Blue reward:0.85\n", - "step: 2677, Red action: do-nothing, Blue reward:0.85\n", - "step: 2678, Red action: do-nothing, Blue reward:0.85\n", - "step: 2679, Red action: do-nothing, Blue reward:0.85\n", - "step: 2680, Red action: do-nothing, Blue reward:0.85\n", - "step: 2681, Red action: do-nothing, Blue reward:0.85\n", - "step: 2682, Red action: do-nothing, Blue reward:0.85\n", - "step: 2683, Red action: do-nothing, Blue reward:0.85\n", - "step: 2684, Red action: do-nothing, Blue reward:0.85\n", - "step: 2685, Red action: do-nothing, Blue reward:0.85\n", - "step: 2686, Red action: do-nothing, Blue reward:0.85\n", - "step: 2687, Red action: do-nothing, Blue reward:0.85\n", - "step: 2688, Red action: do-nothing, Blue reward:0.85\n", - "step: 2689, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2690, Red action: do-nothing, Blue reward:0.85\n", - "step: 2691, Red action: do-nothing, Blue reward:0.85\n", - "step: 2692, Red action: do-nothing, Blue reward:0.85\n", - "step: 2693, Red action: do-nothing, Blue reward:0.85\n", - "step: 2694, Red action: do-nothing, Blue reward:0.85\n", - "step: 2695, Red action: do-nothing, Blue reward:0.85\n", - "step: 2696, Red action: do-nothing, Blue reward:0.85\n", - "step: 2697, Red action: do-nothing, Blue reward:0.85\n", - "step: 2698, Red action: do-nothing, Blue reward:0.85\n", - "step: 2699, Red action: do-nothing, Blue reward:0.85\n", - "step: 2700, Red action: do-nothing, Blue reward:0.85\n", - "step: 2701, Red action: do-nothing, Blue reward:0.85\n", - "step: 2702, Red action: do-nothing, Blue reward:0.85\n", - "step: 2703, Red action: do-nothing, Blue reward:0.85\n", - "step: 2704, Red action: do-nothing, Blue reward:0.85\n", - "step: 2705, Red action: do-nothing, Blue reward:0.85\n", - "step: 2706, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2707, Red action: do-nothing, Blue reward:0.85\n", - "step: 2708, Red action: do-nothing, Blue reward:0.85\n", - "step: 2709, Red action: do-nothing, Blue reward:0.85\n", - "step: 2710, Red action: do-nothing, Blue reward:0.85\n", - "step: 2711, Red action: do-nothing, Blue reward:0.85\n", - "step: 2712, Red action: do-nothing, Blue reward:0.85\n", - "step: 2713, Red action: do-nothing, Blue reward:0.85\n", - "step: 2714, Red action: do-nothing, Blue reward:0.85\n", - "step: 2715, Red action: do-nothing, Blue reward:0.85\n", - "step: 2716, Red action: do-nothing, Blue reward:0.85\n", - "step: 2717, Red action: do-nothing, Blue reward:0.85\n", - "step: 2718, Red action: do-nothing, Blue reward:0.85\n", - "step: 2719, Red action: do-nothing, Blue reward:0.85\n", - "step: 2720, Red action: do-nothing, Blue reward:0.85\n", - "step: 2721, Red action: do-nothing, Blue reward:0.85\n", - "step: 2722, Red action: do-nothing, Blue reward:0.85\n", - "step: 2723, Red action: do-nothing, Blue reward:0.85\n", - "step: 2724, Red action: do-nothing, Blue reward:0.85\n", - "step: 2725, Red action: do-nothing, Blue reward:0.85\n", - "step: 2726, Red action: do-nothing, Blue reward:0.85\n", - "step: 2727, Red action: do-nothing, Blue reward:0.85\n", - "step: 2728, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2729, Red action: do-nothing, Blue reward:0.85\n", - "step: 2730, Red action: do-nothing, Blue reward:0.85\n", - "step: 2731, Red action: do-nothing, Blue reward:0.85\n", - "step: 2732, Red action: do-nothing, Blue reward:0.85\n", - "step: 2733, Red action: do-nothing, Blue reward:0.85\n", - "step: 2734, Red action: do-nothing, Blue reward:0.85\n", - "step: 2735, Red action: do-nothing, Blue reward:0.85\n", - "step: 2736, Red action: do-nothing, Blue reward:0.85\n", - "step: 2737, Red action: do-nothing, Blue reward:0.85\n", - "step: 2738, Red action: do-nothing, Blue reward:0.85\n", - "step: 2739, Red action: do-nothing, Blue reward:0.85\n", - "step: 2740, Red action: do-nothing, Blue reward:0.85\n", - "step: 2741, Red action: do-nothing, Blue reward:0.85\n", - "step: 2742, Red action: do-nothing, Blue reward:0.85\n", - "step: 2743, Red action: do-nothing, Blue reward:0.85\n", - "step: 2744, Red action: do-nothing, Blue reward:0.85\n", - "step: 2745, Red action: do-nothing, Blue reward:0.85\n", - "step: 2746, Red action: do-nothing, Blue reward:0.85\n", - "step: 2747, Red action: do-nothing, Blue reward:0.85\n", - "step: 2748, Red action: do-nothing, Blue reward:0.85\n", - "step: 2749, Red action: do-nothing, Blue reward:0.85\n", - "step: 2750, Red action: do-nothing, Blue reward:0.85\n", - "step: 2751, Red action: do-nothing, Blue reward:0.85\n", - "step: 2752, Red action: do-nothing, Blue reward:0.85\n", - "step: 2753, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2754, Red action: do-nothing, Blue reward:0.85\n", - "step: 2755, Red action: do-nothing, Blue reward:0.85\n", - "step: 2756, Red action: do-nothing, Blue reward:0.85\n", - "step: 2757, Red action: do-nothing, Blue reward:0.85\n", - "step: 2758, Red action: do-nothing, Blue reward:0.85\n", - "step: 2759, Red action: do-nothing, Blue reward:0.85\n", - "step: 2760, Red action: do-nothing, Blue reward:0.85\n", - "step: 2761, Red action: do-nothing, Blue reward:0.85\n", - "step: 2762, Red action: do-nothing, Blue reward:0.85\n", - "step: 2763, Red action: do-nothing, Blue reward:0.85\n", - "step: 2764, Red action: do-nothing, Blue reward:0.85\n", - "step: 2765, Red action: do-nothing, Blue reward:0.85\n", - "step: 2766, Red action: do-nothing, Blue reward:0.85\n", - "step: 2767, Red action: do-nothing, Blue reward:0.85\n", - "step: 2768, Red action: do-nothing, Blue reward:0.85\n", - "step: 2769, Red action: do-nothing, Blue reward:0.85\n", - "step: 2770, Red action: do-nothing, Blue reward:0.85\n", - "step: 2771, Red action: do-nothing, Blue reward:0.85\n", - "step: 2772, Red action: do-nothing, Blue reward:0.85\n", - "step: 2773, Red action: do-nothing, Blue reward:0.85\n", - "step: 2774, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2775, Red action: do-nothing, Blue reward:0.85\n", - "step: 2776, Red action: do-nothing, Blue reward:0.85\n", - "step: 2777, Red action: do-nothing, Blue reward:0.85\n", - "step: 2778, Red action: do-nothing, Blue reward:0.85\n", - "step: 2779, Red action: do-nothing, Blue reward:0.85\n", - "step: 2780, Red action: do-nothing, Blue reward:0.85\n", - "step: 2781, Red action: do-nothing, Blue reward:0.85\n", - "step: 2782, Red action: do-nothing, Blue reward:0.85\n", - "step: 2783, Red action: do-nothing, Blue reward:0.85\n", - "step: 2784, Red action: do-nothing, Blue reward:0.85\n", - "step: 2785, Red action: do-nothing, Blue reward:0.85\n", - "step: 2786, Red action: do-nothing, Blue reward:0.85\n", - "step: 2787, Red action: do-nothing, Blue reward:0.85\n", - "step: 2788, Red action: do-nothing, Blue reward:0.85\n", - "step: 2789, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2790, Red action: do-nothing, Blue reward:0.85\n", - "step: 2791, Red action: do-nothing, Blue reward:0.85\n", - "step: 2792, Red action: do-nothing, Blue reward:0.85\n", - "step: 2793, Red action: do-nothing, Blue reward:0.85\n", - "step: 2794, Red action: do-nothing, Blue reward:0.85\n", - "step: 2795, Red action: do-nothing, Blue reward:0.85\n", - "step: 2796, Red action: do-nothing, Blue reward:0.85\n", - "step: 2797, Red action: do-nothing, Blue reward:0.85\n", - "step: 2798, Red action: do-nothing, Blue reward:0.85\n", - "step: 2799, Red action: do-nothing, Blue reward:0.85\n", - "step: 2800, Red action: do-nothing, Blue reward:0.85\n", - "step: 2801, Red action: do-nothing, Blue reward:0.85\n", - "step: 2802, Red action: do-nothing, Blue reward:0.85\n", - "step: 2803, Red action: do-nothing, Blue reward:0.85\n", - "step: 2804, Red action: do-nothing, Blue reward:0.85\n", - "step: 2805, Red action: do-nothing, Blue reward:0.85\n", - "step: 2806, Red action: do-nothing, Blue reward:0.85\n", - "step: 2807, Red action: do-nothing, Blue reward:0.85\n", - "step: 2808, Red action: do-nothing, Blue reward:0.85\n", - "step: 2809, Red action: do-nothing, Blue reward:0.85\n", - "step: 2810, Red action: do-nothing, Blue reward:0.85\n", - "step: 2811, Red action: do-nothing, Blue reward:0.85\n", - "step: 2812, Red action: do-nothing, Blue reward:0.85\n", - "step: 2813, Red action: do-nothing, Blue reward:0.85\n", - "step: 2814, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2815, Red action: do-nothing, Blue reward:0.85\n", - "step: 2816, Red action: do-nothing, Blue reward:0.85\n", - "step: 2817, Red action: do-nothing, Blue reward:0.85\n", - "step: 2818, Red action: do-nothing, Blue reward:0.85\n", - "step: 2819, Red action: do-nothing, Blue reward:0.85\n", - "step: 2820, Red action: do-nothing, Blue reward:0.85\n", - "step: 2821, Red action: do-nothing, Blue reward:0.85\n", - "step: 2822, Red action: do-nothing, Blue reward:0.85\n", - "step: 2823, Red action: do-nothing, Blue reward:0.85\n", - "step: 2824, Red action: do-nothing, Blue reward:0.85\n", - "step: 2825, Red action: do-nothing, Blue reward:0.85\n", - "step: 2826, Red action: do-nothing, Blue reward:0.85\n", - "step: 2827, Red action: do-nothing, Blue reward:0.85\n", - "step: 2828, Red action: do-nothing, Blue reward:0.85\n", - "step: 2829, Red action: do-nothing, Blue reward:0.85\n", - "step: 2830, Red action: do-nothing, Blue reward:0.85\n", - "step: 2831, Red action: do-nothing, Blue reward:0.85\n", - "step: 2832, Red action: do-nothing, Blue reward:0.85\n", - "step: 2833, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2834, Red action: do-nothing, Blue reward:0.85\n", - "step: 2835, Red action: do-nothing, Blue reward:0.85\n", - "step: 2836, Red action: do-nothing, Blue reward:0.85\n", - "step: 2837, Red action: do-nothing, Blue reward:0.85\n", - "step: 2838, Red action: do-nothing, Blue reward:0.85\n", - "step: 2839, Red action: do-nothing, Blue reward:0.85\n", - "step: 2840, Red action: do-nothing, Blue reward:0.85\n", - "step: 2841, Red action: do-nothing, Blue reward:0.85\n", - "step: 2842, Red action: do-nothing, Blue reward:0.85\n", - "step: 2843, Red action: do-nothing, Blue reward:0.85\n", - "step: 2844, Red action: do-nothing, Blue reward:0.85\n", - "step: 2845, Red action: do-nothing, Blue reward:0.85\n", - "step: 2846, Red action: do-nothing, Blue reward:0.85\n", - "step: 2847, Red action: do-nothing, Blue reward:0.85\n", - "step: 2848, Red action: do-nothing, Blue reward:0.85\n", - "step: 2849, Red action: do-nothing, Blue reward:0.85\n", - "step: 2850, Red action: do-nothing, Blue reward:0.85\n", - "step: 2851, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2852, Red action: do-nothing, Blue reward:0.85\n", - "step: 2853, Red action: do-nothing, Blue reward:0.85\n", - "step: 2854, Red action: do-nothing, Blue reward:0.85\n", - "step: 2855, Red action: do-nothing, Blue reward:0.85\n", - "step: 2856, Red action: do-nothing, Blue reward:0.85\n", - "step: 2857, Red action: do-nothing, Blue reward:0.85\n", - "step: 2858, Red action: do-nothing, Blue reward:0.85\n", - "step: 2859, Red action: do-nothing, Blue reward:0.85\n", - "step: 2860, Red action: do-nothing, Blue reward:0.85\n", - "step: 2861, Red action: do-nothing, Blue reward:0.85\n", - "step: 2862, Red action: do-nothing, Blue reward:0.85\n", - "step: 2863, Red action: do-nothing, Blue reward:0.85\n", - "step: 2864, Red action: do-nothing, Blue reward:0.85\n", - "step: 2865, Red action: do-nothing, Blue reward:0.85\n", - "step: 2866, Red action: do-nothing, Blue reward:0.85\n", - "step: 2867, Red action: do-nothing, Blue reward:0.85\n", - "step: 2868, Red action: do-nothing, Blue reward:0.85\n", - "step: 2869, Red action: do-nothing, Blue reward:0.85\n", - "step: 2870, Red action: do-nothing, Blue reward:0.85\n", - "step: 2871, Red action: do-nothing, Blue reward:0.85\n", - "step: 2872, Red action: do-nothing, Blue reward:0.85\n", - "step: 2873, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2874, Red action: do-nothing, Blue reward:0.85\n", - "step: 2875, Red action: do-nothing, Blue reward:0.85\n", - "step: 2876, Red action: do-nothing, Blue reward:0.85\n", - "step: 2877, Red action: do-nothing, Blue reward:0.85\n", - "step: 2878, Red action: do-nothing, Blue reward:0.85\n", - "step: 2879, Red action: do-nothing, Blue reward:0.85\n", - "step: 2880, Red action: do-nothing, Blue reward:0.85\n", - "step: 2881, Red action: do-nothing, Blue reward:0.85\n", - "step: 2882, Red action: do-nothing, Blue reward:0.85\n", - "step: 2883, Red action: do-nothing, Blue reward:0.85\n", - "step: 2884, Red action: do-nothing, Blue reward:0.85\n", - "step: 2885, Red action: do-nothing, Blue reward:0.85\n", - "step: 2886, Red action: do-nothing, Blue reward:0.85\n", - "step: 2887, Red action: do-nothing, Blue reward:0.85\n", - "step: 2888, Red action: do-nothing, Blue reward:0.85\n", - "step: 2889, Red action: do-nothing, Blue reward:0.85\n", - "step: 2890, Red action: do-nothing, Blue reward:0.85\n", - "step: 2891, Red action: do-nothing, Blue reward:0.85\n", - "step: 2892, Red action: do-nothing, Blue reward:0.85\n", - "step: 2893, Red action: do-nothing, Blue reward:0.85\n", - "step: 2894, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2895, Red action: do-nothing, Blue reward:0.85\n", - "step: 2896, Red action: do-nothing, Blue reward:0.85\n", - "step: 2897, Red action: do-nothing, Blue reward:0.85\n", - "step: 2898, Red action: do-nothing, Blue reward:0.85\n", - "step: 2899, Red action: do-nothing, Blue reward:0.85\n", - "step: 2900, Red action: do-nothing, Blue reward:0.85\n", - "step: 2901, Red action: do-nothing, Blue reward:0.85\n", - "step: 2902, Red action: do-nothing, Blue reward:0.85\n", - "step: 2903, Red action: do-nothing, Blue reward:0.85\n", - "step: 2904, Red action: do-nothing, Blue reward:0.85\n", - "step: 2905, Red action: do-nothing, Blue reward:0.85\n", - "step: 2906, Red action: do-nothing, Blue reward:0.85\n", - "step: 2907, Red action: do-nothing, Blue reward:0.85\n", - "step: 2908, Red action: do-nothing, Blue reward:0.85\n", - "step: 2909, Red action: do-nothing, Blue reward:0.85\n", - "step: 2910, Red action: do-nothing, Blue reward:0.85\n", - "step: 2911, Red action: do-nothing, Blue reward:0.85\n", - "step: 2912, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2913, Red action: do-nothing, Blue reward:0.85\n", - "step: 2914, Red action: do-nothing, Blue reward:0.85\n", - "step: 2915, Red action: do-nothing, Blue reward:0.85\n", - "step: 2916, Red action: do-nothing, Blue reward:0.85\n", - "step: 2917, Red action: do-nothing, Blue reward:0.85\n", - "step: 2918, Red action: do-nothing, Blue reward:0.85\n", - "step: 2919, Red action: do-nothing, Blue reward:0.85\n", - "step: 2920, Red action: do-nothing, Blue reward:0.85\n", - "step: 2921, Red action: do-nothing, Blue reward:0.85\n", - "step: 2922, Red action: do-nothing, Blue reward:0.85\n", - "step: 2923, Red action: do-nothing, Blue reward:0.85\n", - "step: 2924, Red action: do-nothing, Blue reward:0.85\n", - "step: 2925, Red action: do-nothing, Blue reward:0.85\n", - "step: 2926, Red action: do-nothing, Blue reward:0.85\n", - "step: 2927, Red action: do-nothing, Blue reward:0.85\n", - "step: 2928, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2929, Red action: do-nothing, Blue reward:0.85\n", - "step: 2930, Red action: do-nothing, Blue reward:0.85\n", - "step: 2931, Red action: do-nothing, Blue reward:0.85\n", - "step: 2932, Red action: do-nothing, Blue reward:0.85\n", - "step: 2933, Red action: do-nothing, Blue reward:0.85\n", - "step: 2934, Red action: do-nothing, Blue reward:0.85\n", - "step: 2935, Red action: do-nothing, Blue reward:0.85\n", - "step: 2936, Red action: do-nothing, Blue reward:0.85\n", - "step: 2937, Red action: do-nothing, Blue reward:0.85\n", - "step: 2938, Red action: do-nothing, Blue reward:0.85\n", - "step: 2939, Red action: do-nothing, Blue reward:0.85\n", - "step: 2940, Red action: do-nothing, Blue reward:0.85\n", - "step: 2941, Red action: do-nothing, Blue reward:0.85\n", - "step: 2942, Red action: do-nothing, Blue reward:0.85\n", - "step: 2943, Red action: do-nothing, Blue reward:0.85\n", - "step: 2944, Red action: do-nothing, Blue reward:0.85\n", - "step: 2945, Red action: do-nothing, Blue reward:0.85\n", - "step: 2946, Red action: do-nothing, Blue reward:0.85\n", - "step: 2947, Red action: do-nothing, Blue reward:0.85\n", - "step: 2948, Red action: do-nothing, Blue reward:0.85\n", - "step: 2949, Red action: do-nothing, Blue reward:0.85\n", - "step: 2950, Red action: do-nothing, Blue reward:0.85\n", - "step: 2951, Red action: do-nothing, Blue reward:0.85\n", - "step: 2952, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2953, Red action: do-nothing, Blue reward:0.85\n", - "step: 2954, Red action: do-nothing, Blue reward:0.85\n", - "step: 2955, Red action: do-nothing, Blue reward:0.85\n", - "step: 2956, Red action: do-nothing, Blue reward:0.85\n", - "step: 2957, Red action: do-nothing, Blue reward:0.85\n", - "step: 2958, Red action: do-nothing, Blue reward:0.85\n", - "step: 2959, Red action: do-nothing, Blue reward:0.85\n", - "step: 2960, Red action: do-nothing, Blue reward:0.85\n", - "step: 2961, Red action: do-nothing, Blue reward:0.85\n", - "step: 2962, Red action: do-nothing, Blue reward:0.85\n", - "step: 2963, Red action: do-nothing, Blue reward:0.85\n", - "step: 2964, Red action: do-nothing, Blue reward:0.85\n", - "step: 2965, Red action: do-nothing, Blue reward:0.85\n", - "step: 2966, Red action: do-nothing, Blue reward:0.85\n", - "step: 2967, Red action: do-nothing, Blue reward:0.85\n", - "step: 2968, Red action: do-nothing, Blue reward:0.85\n", - "step: 2969, Red action: do-nothing, Blue reward:0.85\n", - "step: 2970, Red action: do-nothing, Blue reward:0.85\n", - "step: 2971, Red action: do-nothing, Blue reward:0.85\n", - "step: 2972, Red action: do-nothing, Blue reward:0.85\n", - "step: 2973, Red action: do-nothing, Blue reward:0.85\n", - "step: 2974, Red action: do-nothing, Blue reward:0.85\n", - "step: 2975, Red action: do-nothing, Blue reward:0.85\n", - "step: 2976, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2977, Red action: do-nothing, Blue reward:0.85\n", - "step: 2978, Red action: do-nothing, Blue reward:0.85\n", - "step: 2979, Red action: do-nothing, Blue reward:0.85\n", - "step: 2980, Red action: do-nothing, Blue reward:0.85\n", - "step: 2981, Red action: do-nothing, Blue reward:0.85\n", - "step: 2982, Red action: do-nothing, Blue reward:0.85\n", - "step: 2983, Red action: do-nothing, Blue reward:0.85\n", - "step: 2984, Red action: do-nothing, Blue reward:0.85\n", - "step: 2985, Red action: do-nothing, Blue reward:0.85\n", - "step: 2986, Red action: do-nothing, Blue reward:0.85\n", - "step: 2987, Red action: do-nothing, Blue reward:0.85\n", - "step: 2988, Red action: do-nothing, Blue reward:0.85\n", - "step: 2989, Red action: do-nothing, Blue reward:0.85\n", - "step: 2990, Red action: do-nothing, Blue reward:0.85\n", - "step: 2991, Red action: do-nothing, Blue reward:0.85\n", - "step: 2992, Red action: do-nothing, Blue reward:0.85\n", - "step: 2993, Red action: do-nothing, Blue reward:0.85\n", - "step: 2994, Red action: do-nothing, Blue reward:0.85\n", - "step: 2995, Red action: do-nothing, Blue reward:0.85\n", - "step: 2996, Red action: do-nothing, Blue reward:0.85\n", - "step: 2997, Red action: node-application-execute, Blue reward:0.85\n", - "step: 2998, Red action: do-nothing, Blue reward:0.85\n", - "step: 2999, Red action: do-nothing, Blue reward:0.85\n", - "step: 3000, Red action: do-nothing, Blue reward:0.85\n", - "step: 3001, Red action: do-nothing, Blue reward:0.85\n", - "step: 3002, Red action: do-nothing, Blue reward:0.85\n", - "step: 3003, Red action: do-nothing, Blue reward:0.85\n", - "step: 3004, Red action: do-nothing, Blue reward:0.85\n", - "step: 3005, Red action: do-nothing, Blue reward:0.85\n", - "step: 3006, Red action: do-nothing, Blue reward:0.85\n", - "step: 3007, Red action: do-nothing, Blue reward:0.85\n", - "step: 3008, Red action: do-nothing, Blue reward:0.85\n", - "step: 3009, Red action: do-nothing, Blue reward:0.85\n", - "step: 3010, Red action: do-nothing, Blue reward:0.85\n", - "step: 3011, Red action: do-nothing, Blue reward:0.85\n", - "step: 3012, Red action: do-nothing, Blue reward:0.85\n", - "step: 3013, Red action: do-nothing, Blue reward:0.85\n", - "step: 3014, Red action: do-nothing, Blue reward:0.85\n", - "step: 3015, Red action: do-nothing, Blue reward:0.85\n", - "step: 3016, Red action: do-nothing, Blue reward:0.85\n", - "step: 3017, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3018, Red action: do-nothing, Blue reward:0.85\n", - "step: 3019, Red action: do-nothing, Blue reward:0.85\n", - "step: 3020, Red action: do-nothing, Blue reward:0.85\n", - "step: 3021, Red action: do-nothing, Blue reward:0.85\n", - "step: 3022, Red action: do-nothing, Blue reward:0.85\n", - "step: 3023, Red action: do-nothing, Blue reward:0.85\n", - "step: 3024, Red action: do-nothing, Blue reward:0.85\n", - "step: 3025, Red action: do-nothing, Blue reward:0.85\n", - "step: 3026, Red action: do-nothing, Blue reward:0.85\n", - "step: 3027, Red action: do-nothing, Blue reward:0.85\n", - "step: 3028, Red action: do-nothing, Blue reward:0.85\n", - "step: 3029, Red action: do-nothing, Blue reward:0.85\n", - "step: 3030, Red action: do-nothing, Blue reward:0.85\n", - "step: 3031, Red action: do-nothing, Blue reward:0.85\n", - "step: 3032, Red action: do-nothing, Blue reward:0.85\n", - "step: 3033, Red action: do-nothing, Blue reward:0.85\n", - "step: 3034, Red action: do-nothing, Blue reward:0.85\n", - "step: 3035, Red action: do-nothing, Blue reward:0.85\n", - "step: 3036, Red action: do-nothing, Blue reward:0.85\n", - "step: 3037, Red action: do-nothing, Blue reward:0.85\n", - "step: 3038, Red action: do-nothing, Blue reward:0.85\n", - "step: 3039, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3040, Red action: do-nothing, Blue reward:0.85\n", - "step: 3041, Red action: do-nothing, Blue reward:0.85\n", - "step: 3042, Red action: do-nothing, Blue reward:0.85\n", - "step: 3043, Red action: do-nothing, Blue reward:0.85\n", - "step: 3044, Red action: do-nothing, Blue reward:0.85\n", - "step: 3045, Red action: do-nothing, Blue reward:0.85\n", - "step: 3046, Red action: do-nothing, Blue reward:0.85\n", - "step: 3047, Red action: do-nothing, Blue reward:0.85\n", - "step: 3048, Red action: do-nothing, Blue reward:0.85\n", - "step: 3049, Red action: do-nothing, Blue reward:0.85\n", - "step: 3050, Red action: do-nothing, Blue reward:0.85\n", - "step: 3051, Red action: do-nothing, Blue reward:0.85\n", - "step: 3052, Red action: do-nothing, Blue reward:0.85\n", - "step: 3053, Red action: do-nothing, Blue reward:0.85\n", - "step: 3054, Red action: do-nothing, Blue reward:0.85\n", - "step: 3055, Red action: do-nothing, Blue reward:0.85\n", - "step: 3056, Red action: do-nothing, Blue reward:0.85\n", - "step: 3057, Red action: do-nothing, Blue reward:0.85\n", - "step: 3058, Red action: do-nothing, Blue reward:0.85\n", - "step: 3059, Red action: do-nothing, Blue reward:0.85\n", - "step: 3060, Red action: do-nothing, Blue reward:0.85\n", - "step: 3061, Red action: do-nothing, Blue reward:0.85\n", - "step: 3062, Red action: do-nothing, Blue reward:0.85\n", - "step: 3063, Red action: do-nothing, Blue reward:0.85\n", - "step: 3064, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3065, Red action: do-nothing, Blue reward:0.85\n", - "step: 3066, Red action: do-nothing, Blue reward:0.85\n", - "step: 3067, Red action: do-nothing, Blue reward:0.85\n", - "step: 3068, Red action: do-nothing, Blue reward:0.85\n", - "step: 3069, Red action: do-nothing, Blue reward:0.85\n", - "step: 3070, Red action: do-nothing, Blue reward:0.85\n", - "step: 3071, Red action: do-nothing, Blue reward:0.85\n", - "step: 3072, Red action: do-nothing, Blue reward:0.85\n", - "step: 3073, Red action: do-nothing, Blue reward:0.85\n", - "step: 3074, Red action: do-nothing, Blue reward:0.85\n", - "step: 3075, Red action: do-nothing, Blue reward:0.85\n", - "step: 3076, Red action: do-nothing, Blue reward:0.85\n", - "step: 3077, Red action: do-nothing, Blue reward:0.85\n", - "step: 3078, Red action: do-nothing, Blue reward:0.85\n", - "step: 3079, Red action: do-nothing, Blue reward:0.85\n", - "step: 3080, Red action: do-nothing, Blue reward:0.85\n", - "step: 3081, Red action: do-nothing, Blue reward:0.85\n", - "step: 3082, Red action: do-nothing, Blue reward:0.85\n", - "step: 3083, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3084, Red action: do-nothing, Blue reward:0.85\n", - "step: 3085, Red action: do-nothing, Blue reward:0.85\n", - "step: 3086, Red action: do-nothing, Blue reward:0.85\n", - "step: 3087, Red action: do-nothing, Blue reward:0.85\n", - "step: 3088, Red action: do-nothing, Blue reward:0.85\n", - "step: 3089, Red action: do-nothing, Blue reward:0.85\n", - "step: 3090, Red action: do-nothing, Blue reward:0.85\n", - "step: 3091, Red action: do-nothing, Blue reward:0.85\n", - "step: 3092, Red action: do-nothing, Blue reward:0.85\n", - "step: 3093, Red action: do-nothing, Blue reward:0.85\n", - "step: 3094, Red action: do-nothing, Blue reward:0.85\n", - "step: 3095, Red action: do-nothing, Blue reward:0.85\n", - "step: 3096, Red action: do-nothing, Blue reward:0.85\n", - "step: 3097, Red action: do-nothing, Blue reward:0.85\n", - "step: 3098, Red action: do-nothing, Blue reward:0.85\n", - "step: 3099, Red action: do-nothing, Blue reward:0.85\n", - "step: 3100, Red action: do-nothing, Blue reward:0.85\n", - "step: 3101, Red action: do-nothing, Blue reward:0.85\n", - "step: 3102, Red action: do-nothing, Blue reward:0.85\n", - "step: 3103, Red action: do-nothing, Blue reward:0.85\n", - "step: 3104, Red action: do-nothing, Blue reward:0.85\n", - "step: 3105, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3106, Red action: do-nothing, Blue reward:0.85\n", - "step: 3107, Red action: do-nothing, Blue reward:0.85\n", - "step: 3108, Red action: do-nothing, Blue reward:0.85\n", - "step: 3109, Red action: do-nothing, Blue reward:0.85\n", - "step: 3110, Red action: do-nothing, Blue reward:0.85\n", - "step: 3111, Red action: do-nothing, Blue reward:0.85\n", - "step: 3112, Red action: do-nothing, Blue reward:0.85\n", - "step: 3113, Red action: do-nothing, Blue reward:0.85\n", - "step: 3114, Red action: do-nothing, Blue reward:0.85\n", - "step: 3115, Red action: do-nothing, Blue reward:0.85\n", - "step: 3116, Red action: do-nothing, Blue reward:0.85\n", - "step: 3117, Red action: do-nothing, Blue reward:0.85\n", - "step: 3118, Red action: do-nothing, Blue reward:0.85\n", - "step: 3119, Red action: do-nothing, Blue reward:0.85\n", - "step: 3120, Red action: do-nothing, Blue reward:0.85\n", - "step: 3121, Red action: do-nothing, Blue reward:0.85\n", - "step: 3122, Red action: do-nothing, Blue reward:0.85\n", - "step: 3123, Red action: do-nothing, Blue reward:0.85\n", - "step: 3124, Red action: do-nothing, Blue reward:0.85\n", - "step: 3125, Red action: do-nothing, Blue reward:0.85\n", - "step: 3126, Red action: do-nothing, Blue reward:0.85\n", - "step: 3127, Red action: do-nothing, Blue reward:0.85\n", - "step: 3128, Red action: do-nothing, Blue reward:0.85\n", - "step: 3129, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3130, Red action: do-nothing, Blue reward:0.85\n", - "step: 3131, Red action: do-nothing, Blue reward:0.85\n", - "step: 3132, Red action: do-nothing, Blue reward:0.85\n", - "step: 3133, Red action: do-nothing, Blue reward:0.85\n", - "step: 3134, Red action: do-nothing, Blue reward:0.85\n", - "step: 3135, Red action: do-nothing, Blue reward:0.85\n", - "step: 3136, Red action: do-nothing, Blue reward:0.85\n", - "step: 3137, Red action: do-nothing, Blue reward:0.85\n", - "step: 3138, Red action: do-nothing, Blue reward:0.85\n", - "step: 3139, Red action: do-nothing, Blue reward:0.85\n", - "step: 3140, Red action: do-nothing, Blue reward:0.85\n", - "step: 3141, Red action: do-nothing, Blue reward:0.85\n", - "step: 3142, Red action: do-nothing, Blue reward:0.85\n", - "step: 3143, Red action: do-nothing, Blue reward:0.85\n", - "step: 3144, Red action: do-nothing, Blue reward:0.85\n", - "step: 3145, Red action: do-nothing, Blue reward:0.85\n", - "step: 3146, Red action: do-nothing, Blue reward:0.85\n", - "step: 3147, Red action: do-nothing, Blue reward:0.85\n", - "step: 3148, Red action: do-nothing, Blue reward:0.85\n", - "step: 3149, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3150, Red action: do-nothing, Blue reward:0.85\n", - "step: 3151, Red action: do-nothing, Blue reward:0.85\n", - "step: 3152, Red action: do-nothing, Blue reward:0.85\n", - "step: 3153, Red action: do-nothing, Blue reward:0.85\n", - "step: 3154, Red action: do-nothing, Blue reward:0.85\n", - "step: 3155, Red action: do-nothing, Blue reward:0.85\n", - "step: 3156, Red action: do-nothing, Blue reward:0.85\n", - "step: 3157, Red action: do-nothing, Blue reward:0.85\n", - "step: 3158, Red action: do-nothing, Blue reward:0.85\n", - "step: 3159, Red action: do-nothing, Blue reward:0.85\n", - "step: 3160, Red action: do-nothing, Blue reward:0.85\n", - "step: 3161, Red action: do-nothing, Blue reward:0.85\n", - "step: 3162, Red action: do-nothing, Blue reward:0.85\n", - "step: 3163, Red action: do-nothing, Blue reward:0.85\n", - "step: 3164, Red action: do-nothing, Blue reward:0.85\n", - "step: 3165, Red action: do-nothing, Blue reward:0.85\n", - "step: 3166, Red action: do-nothing, Blue reward:0.85\n", - "step: 3167, Red action: do-nothing, Blue reward:0.85\n", - "step: 3168, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3169, Red action: do-nothing, Blue reward:0.85\n", - "step: 3170, Red action: do-nothing, Blue reward:0.85\n", - "step: 3171, Red action: do-nothing, Blue reward:0.85\n", - "step: 3172, Red action: do-nothing, Blue reward:0.85\n", - "step: 3173, Red action: do-nothing, Blue reward:0.85\n", - "step: 3174, Red action: do-nothing, Blue reward:0.85\n", - "step: 3175, Red action: do-nothing, Blue reward:0.85\n", - "step: 3176, Red action: do-nothing, Blue reward:0.85\n", - "step: 3177, Red action: do-nothing, Blue reward:0.85\n", - "step: 3178, Red action: do-nothing, Blue reward:0.85\n", - "step: 3179, Red action: do-nothing, Blue reward:0.85\n", - "step: 3180, Red action: do-nothing, Blue reward:0.85\n", - "step: 3181, Red action: do-nothing, Blue reward:0.85\n", - "step: 3182, Red action: do-nothing, Blue reward:0.85\n", - "step: 3183, Red action: do-nothing, Blue reward:0.85\n", - "step: 3184, Red action: do-nothing, Blue reward:0.85\n", - "step: 3185, Red action: do-nothing, Blue reward:0.85\n", - "step: 3186, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3187, Red action: do-nothing, Blue reward:0.85\n", - "step: 3188, Red action: do-nothing, Blue reward:0.85\n", - "step: 3189, Red action: do-nothing, Blue reward:0.85\n", - "step: 3190, Red action: do-nothing, Blue reward:0.85\n", - "step: 3191, Red action: do-nothing, Blue reward:0.85\n", - "step: 3192, Red action: do-nothing, Blue reward:0.85\n", - "step: 3193, Red action: do-nothing, Blue reward:0.85\n", - "step: 3194, Red action: do-nothing, Blue reward:0.85\n", - "step: 3195, Red action: do-nothing, Blue reward:0.85\n", - "step: 3196, Red action: do-nothing, Blue reward:0.85\n", - "step: 3197, Red action: do-nothing, Blue reward:0.85\n", - "step: 3198, Red action: do-nothing, Blue reward:0.85\n", - "step: 3199, Red action: do-nothing, Blue reward:0.85\n", - "step: 3200, Red action: do-nothing, Blue reward:0.85\n", - "step: 3201, Red action: do-nothing, Blue reward:0.85\n", - "step: 3202, Red action: do-nothing, Blue reward:0.85\n", - "step: 3203, Red action: do-nothing, Blue reward:0.85\n", - "step: 3204, Red action: do-nothing, Blue reward:0.85\n", - "step: 3205, Red action: do-nothing, Blue reward:0.85\n", - "step: 3206, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3207, Red action: do-nothing, Blue reward:0.85\n", - "step: 3208, Red action: do-nothing, Blue reward:0.85\n", - "step: 3209, Red action: do-nothing, Blue reward:0.85\n", - "step: 3210, Red action: do-nothing, Blue reward:0.85\n", - "step: 3211, Red action: do-nothing, Blue reward:0.85\n", - "step: 3212, Red action: do-nothing, Blue reward:0.85\n", - "step: 3213, Red action: do-nothing, Blue reward:0.85\n", - "step: 3214, Red action: do-nothing, Blue reward:0.85\n", - "step: 3215, Red action: do-nothing, Blue reward:0.85\n", - "step: 3216, Red action: do-nothing, Blue reward:0.85\n", - "step: 3217, Red action: do-nothing, Blue reward:0.85\n", - "step: 3218, Red action: do-nothing, Blue reward:0.85\n", - "step: 3219, Red action: do-nothing, Blue reward:0.85\n", - "step: 3220, Red action: do-nothing, Blue reward:0.85\n", - "step: 3221, Red action: do-nothing, Blue reward:0.85\n", - "step: 3222, Red action: do-nothing, Blue reward:0.85\n", - "step: 3223, Red action: do-nothing, Blue reward:0.85\n", - "step: 3224, Red action: do-nothing, Blue reward:0.85\n", - "step: 3225, Red action: do-nothing, Blue reward:0.85\n", - "step: 3226, Red action: do-nothing, Blue reward:0.85\n", - "step: 3227, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3228, Red action: do-nothing, Blue reward:0.85\n", - "step: 3229, Red action: do-nothing, Blue reward:0.85\n", - "step: 3230, Red action: do-nothing, Blue reward:0.85\n", - "step: 3231, Red action: do-nothing, Blue reward:0.85\n", - "step: 3232, Red action: do-nothing, Blue reward:0.85\n", - "step: 3233, Red action: do-nothing, Blue reward:0.85\n", - "step: 3234, Red action: do-nothing, Blue reward:0.85\n", - "step: 3235, Red action: do-nothing, Blue reward:0.85\n", - "step: 3236, Red action: do-nothing, Blue reward:0.85\n", - "step: 3237, Red action: do-nothing, Blue reward:0.85\n", - "step: 3238, Red action: do-nothing, Blue reward:0.85\n", - "step: 3239, Red action: do-nothing, Blue reward:0.85\n", - "step: 3240, Red action: do-nothing, Blue reward:0.85\n", - "step: 3241, Red action: do-nothing, Blue reward:0.85\n", - "step: 3242, Red action: do-nothing, Blue reward:0.85\n", - "step: 3243, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3244, Red action: do-nothing, Blue reward:0.85\n", - "step: 3245, Red action: do-nothing, Blue reward:0.85\n", - "step: 3246, Red action: do-nothing, Blue reward:0.85\n", - "step: 3247, Red action: do-nothing, Blue reward:0.85\n", - "step: 3248, Red action: do-nothing, Blue reward:0.85\n", - "step: 3249, Red action: do-nothing, Blue reward:0.85\n", - "step: 3250, Red action: do-nothing, Blue reward:0.85\n", - "step: 3251, Red action: do-nothing, Blue reward:0.85\n", - "step: 3252, Red action: do-nothing, Blue reward:0.85\n", - "step: 3253, Red action: do-nothing, Blue reward:0.85\n", - "step: 3254, Red action: do-nothing, Blue reward:0.85\n", - "step: 3255, Red action: do-nothing, Blue reward:0.85\n", - "step: 3256, Red action: do-nothing, Blue reward:0.85\n", - "step: 3257, Red action: do-nothing, Blue reward:0.85\n", - "step: 3258, Red action: do-nothing, Blue reward:0.85\n", - "step: 3259, Red action: do-nothing, Blue reward:0.85\n", - "step: 3260, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3261, Red action: do-nothing, Blue reward:0.85\n", - "step: 3262, Red action: do-nothing, Blue reward:0.85\n", - "step: 3263, Red action: do-nothing, Blue reward:0.85\n", - "step: 3264, Red action: do-nothing, Blue reward:0.85\n", - "step: 3265, Red action: do-nothing, Blue reward:0.85\n", - "step: 3266, Red action: do-nothing, Blue reward:0.85\n", - "step: 3267, Red action: do-nothing, Blue reward:0.85\n", - "step: 3268, Red action: do-nothing, Blue reward:0.85\n", - "step: 3269, Red action: do-nothing, Blue reward:0.85\n", - "step: 3270, Red action: do-nothing, Blue reward:0.85\n", - "step: 3271, Red action: do-nothing, Blue reward:0.85\n", - "step: 3272, Red action: do-nothing, Blue reward:0.85\n", - "step: 3273, Red action: do-nothing, Blue reward:0.85\n", - "step: 3274, Red action: do-nothing, Blue reward:0.85\n", - "step: 3275, Red action: do-nothing, Blue reward:0.85\n", - "step: 3276, Red action: do-nothing, Blue reward:0.85\n", - "step: 3277, Red action: do-nothing, Blue reward:0.85\n", - "step: 3278, Red action: do-nothing, Blue reward:0.85\n", - "step: 3279, Red action: do-nothing, Blue reward:0.85\n", - "step: 3280, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3281, Red action: do-nothing, Blue reward:0.85\n", - "step: 3282, Red action: do-nothing, Blue reward:0.85\n", - "step: 3283, Red action: do-nothing, Blue reward:0.85\n", - "step: 3284, Red action: do-nothing, Blue reward:0.85\n", - "step: 3285, Red action: do-nothing, Blue reward:0.85\n", - "step: 3286, Red action: do-nothing, Blue reward:0.85\n", - "step: 3287, Red action: do-nothing, Blue reward:0.85\n", - "step: 3288, Red action: do-nothing, Blue reward:0.85\n", - "step: 3289, Red action: do-nothing, Blue reward:0.85\n", - "step: 3290, Red action: do-nothing, Blue reward:0.85\n", - "step: 3291, Red action: do-nothing, Blue reward:0.85\n", - "step: 3292, Red action: do-nothing, Blue reward:0.85\n", - "step: 3293, Red action: do-nothing, Blue reward:0.85\n", - "step: 3294, Red action: do-nothing, Blue reward:0.85\n", - "step: 3295, Red action: do-nothing, Blue reward:0.85\n", - "step: 3296, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3297, Red action: do-nothing, Blue reward:0.85\n", - "step: 3298, Red action: do-nothing, Blue reward:0.85\n", - "step: 3299, Red action: do-nothing, Blue reward:0.85\n", - "step: 3300, Red action: do-nothing, Blue reward:0.85\n", - "step: 3301, Red action: do-nothing, Blue reward:0.85\n", - "step: 3302, Red action: do-nothing, Blue reward:0.85\n", - "step: 3303, Red action: do-nothing, Blue reward:0.85\n", - "step: 3304, Red action: do-nothing, Blue reward:0.85\n", - "step: 3305, Red action: do-nothing, Blue reward:0.85\n", - "step: 3306, Red action: do-nothing, Blue reward:0.85\n", - "step: 3307, Red action: do-nothing, Blue reward:0.85\n", - "step: 3308, Red action: do-nothing, Blue reward:0.85\n", - "step: 3309, Red action: do-nothing, Blue reward:0.85\n", - "step: 3310, Red action: do-nothing, Blue reward:0.85\n", - "step: 3311, Red action: do-nothing, Blue reward:0.85\n", - "step: 3312, Red action: do-nothing, Blue reward:0.85\n", - "step: 3313, Red action: do-nothing, Blue reward:0.85\n", - "step: 3314, Red action: do-nothing, Blue reward:0.85\n", - "step: 3315, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3316, Red action: do-nothing, Blue reward:0.85\n", - "step: 3317, Red action: do-nothing, Blue reward:0.85\n", - "step: 3318, Red action: do-nothing, Blue reward:0.85\n", - "step: 3319, Red action: do-nothing, Blue reward:0.85\n", - "step: 3320, Red action: do-nothing, Blue reward:0.85\n", - "step: 3321, Red action: do-nothing, Blue reward:0.85\n", - "step: 3322, Red action: do-nothing, Blue reward:0.85\n", - "step: 3323, Red action: do-nothing, Blue reward:0.85\n", - "step: 3324, Red action: do-nothing, Blue reward:0.85\n", - "step: 3325, Red action: do-nothing, Blue reward:0.85\n", - "step: 3326, Red action: do-nothing, Blue reward:0.85\n", - "step: 3327, Red action: do-nothing, Blue reward:0.85\n", - "step: 3328, Red action: do-nothing, Blue reward:0.85\n", - "step: 3329, Red action: do-nothing, Blue reward:0.85\n", - "step: 3330, Red action: do-nothing, Blue reward:0.85\n", - "step: 3331, Red action: do-nothing, Blue reward:0.85\n", - "step: 3332, Red action: do-nothing, Blue reward:0.85\n", - "step: 3333, Red action: do-nothing, Blue reward:0.85\n", - "step: 3334, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3335, Red action: do-nothing, Blue reward:0.85\n", - "step: 3336, Red action: do-nothing, Blue reward:0.85\n", - "step: 3337, Red action: do-nothing, Blue reward:0.85\n", - "step: 3338, Red action: do-nothing, Blue reward:0.85\n", - "step: 3339, Red action: do-nothing, Blue reward:0.85\n", - "step: 3340, Red action: do-nothing, Blue reward:0.85\n", - "step: 3341, Red action: do-nothing, Blue reward:0.85\n", - "step: 3342, Red action: do-nothing, Blue reward:0.85\n", - "step: 3343, Red action: do-nothing, Blue reward:0.85\n", - "step: 3344, Red action: do-nothing, Blue reward:0.85\n", - "step: 3345, Red action: do-nothing, Blue reward:0.85\n", - "step: 3346, Red action: do-nothing, Blue reward:0.85\n", - "step: 3347, Red action: do-nothing, Blue reward:0.85\n", - "step: 3348, Red action: do-nothing, Blue reward:0.85\n", - "step: 3349, Red action: do-nothing, Blue reward:0.85\n", - "step: 3350, Red action: do-nothing, Blue reward:0.85\n", - "step: 3351, Red action: do-nothing, Blue reward:0.85\n", - "step: 3352, Red action: do-nothing, Blue reward:0.85\n", - "step: 3353, Red action: do-nothing, Blue reward:0.85\n", - "step: 3354, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3355, Red action: do-nothing, Blue reward:0.85\n", - "step: 3356, Red action: do-nothing, Blue reward:0.85\n", - "step: 3357, Red action: do-nothing, Blue reward:0.85\n", - "step: 3358, Red action: do-nothing, Blue reward:0.85\n", - "step: 3359, Red action: do-nothing, Blue reward:0.85\n", - "step: 3360, Red action: do-nothing, Blue reward:0.85\n", - "step: 3361, Red action: do-nothing, Blue reward:0.85\n", - "step: 3362, Red action: do-nothing, Blue reward:0.85\n", - "step: 3363, Red action: do-nothing, Blue reward:0.85\n", - "step: 3364, Red action: do-nothing, Blue reward:0.85\n", - "step: 3365, Red action: do-nothing, Blue reward:0.85\n", - "step: 3366, Red action: do-nothing, Blue reward:0.85\n", - "step: 3367, Red action: do-nothing, Blue reward:0.85\n", - "step: 3368, Red action: do-nothing, Blue reward:0.85\n", - "step: 3369, Red action: do-nothing, Blue reward:0.85\n", - "step: 3370, Red action: do-nothing, Blue reward:0.85\n", - "step: 3371, Red action: do-nothing, Blue reward:0.85\n", - "step: 3372, Red action: do-nothing, Blue reward:0.85\n", - "step: 3373, Red action: do-nothing, Blue reward:0.85\n", - "step: 3374, Red action: do-nothing, Blue reward:0.85\n", - "step: 3375, Red action: do-nothing, Blue reward:0.85\n", - "step: 3376, Red action: do-nothing, Blue reward:0.85\n", - "step: 3377, Red action: do-nothing, Blue reward:0.85\n", - "step: 3378, Red action: do-nothing, Blue reward:0.85\n", - "step: 3379, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3380, Red action: do-nothing, Blue reward:0.85\n", - "step: 3381, Red action: do-nothing, Blue reward:0.85\n", - "step: 3382, Red action: do-nothing, Blue reward:0.85\n", - "step: 3383, Red action: do-nothing, Blue reward:0.85\n", - "step: 3384, Red action: do-nothing, Blue reward:0.85\n", - "step: 3385, Red action: do-nothing, Blue reward:0.85\n", - "step: 3386, Red action: do-nothing, Blue reward:0.85\n", - "step: 3387, Red action: do-nothing, Blue reward:0.85\n", - "step: 3388, Red action: do-nothing, Blue reward:0.85\n", - "step: 3389, Red action: do-nothing, Blue reward:0.85\n", - "step: 3390, Red action: do-nothing, Blue reward:0.85\n", - "step: 3391, Red action: do-nothing, Blue reward:0.85\n", - "step: 3392, Red action: do-nothing, Blue reward:0.85\n", - "step: 3393, Red action: do-nothing, Blue reward:0.85\n", - "step: 3394, Red action: do-nothing, Blue reward:0.85\n", - "step: 3395, Red action: do-nothing, Blue reward:0.85\n", - "step: 3396, Red action: do-nothing, Blue reward:0.85\n", - "step: 3397, Red action: do-nothing, Blue reward:0.85\n", - "step: 3398, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3399, Red action: do-nothing, Blue reward:0.85\n", - "step: 3400, Red action: do-nothing, Blue reward:0.85\n", - "step: 3401, Red action: do-nothing, Blue reward:0.85\n", - "step: 3402, Red action: do-nothing, Blue reward:0.85\n", - "step: 3403, Red action: do-nothing, Blue reward:0.85\n", - "step: 3404, Red action: do-nothing, Blue reward:0.85\n", - "step: 3405, Red action: do-nothing, Blue reward:0.85\n", - "step: 3406, Red action: do-nothing, Blue reward:0.85\n", - "step: 3407, Red action: do-nothing, Blue reward:0.85\n", - "step: 3408, Red action: do-nothing, Blue reward:0.85\n", - "step: 3409, Red action: do-nothing, Blue reward:0.85\n", - "step: 3410, Red action: do-nothing, Blue reward:0.85\n", - "step: 3411, Red action: do-nothing, Blue reward:0.85\n", - "step: 3412, Red action: do-nothing, Blue reward:0.85\n", - "step: 3413, Red action: do-nothing, Blue reward:0.85\n", - "step: 3414, Red action: do-nothing, Blue reward:0.85\n", - "step: 3415, Red action: do-nothing, Blue reward:0.85\n", - "step: 3416, Red action: do-nothing, Blue reward:0.85\n", - "step: 3417, Red action: do-nothing, Blue reward:0.85\n", - "step: 3418, Red action: do-nothing, Blue reward:0.85\n", - "step: 3419, Red action: do-nothing, Blue reward:0.85\n", - "step: 3420, Red action: do-nothing, Blue reward:0.85\n", - "step: 3421, Red action: do-nothing, Blue reward:0.85\n", - "step: 3422, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3423, Red action: do-nothing, Blue reward:0.85\n", - "step: 3424, Red action: do-nothing, Blue reward:0.85\n", - "step: 3425, Red action: do-nothing, Blue reward:0.85\n", - "step: 3426, Red action: do-nothing, Blue reward:0.85\n", - "step: 3427, Red action: do-nothing, Blue reward:0.85\n", - "step: 3428, Red action: do-nothing, Blue reward:0.85\n", - "step: 3429, Red action: do-nothing, Blue reward:0.85\n", - "step: 3430, Red action: do-nothing, Blue reward:0.85\n", - "step: 3431, Red action: do-nothing, Blue reward:0.85\n", - "step: 3432, Red action: do-nothing, Blue reward:0.85\n", - "step: 3433, Red action: do-nothing, Blue reward:0.85\n", - "step: 3434, Red action: do-nothing, Blue reward:0.85\n", - "step: 3435, Red action: do-nothing, Blue reward:0.85\n", - "step: 3436, Red action: do-nothing, Blue reward:0.85\n", - "step: 3437, Red action: do-nothing, Blue reward:0.85\n", - "step: 3438, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3439, Red action: do-nothing, Blue reward:0.85\n", - "step: 3440, Red action: do-nothing, Blue reward:0.85\n", - "step: 3441, Red action: do-nothing, Blue reward:0.85\n", - "step: 3442, Red action: do-nothing, Blue reward:0.85\n", - "step: 3443, Red action: do-nothing, Blue reward:0.85\n", - "step: 3444, Red action: do-nothing, Blue reward:0.85\n", - "step: 3445, Red action: do-nothing, Blue reward:0.85\n", - "step: 3446, Red action: do-nothing, Blue reward:0.85\n", - "step: 3447, Red action: do-nothing, Blue reward:0.85\n", - "step: 3448, Red action: do-nothing, Blue reward:0.85\n", - "step: 3449, Red action: do-nothing, Blue reward:0.85\n", - "step: 3450, Red action: do-nothing, Blue reward:0.85\n", - "step: 3451, Red action: do-nothing, Blue reward:0.85\n", - "step: 3452, Red action: do-nothing, Blue reward:0.85\n", - "step: 3453, Red action: do-nothing, Blue reward:0.85\n", - "step: 3454, Red action: do-nothing, Blue reward:0.85\n", - "step: 3455, Red action: do-nothing, Blue reward:0.85\n", - "step: 3456, Red action: do-nothing, Blue reward:0.85\n", - "step: 3457, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3458, Red action: do-nothing, Blue reward:0.85\n", - "step: 3459, Red action: do-nothing, Blue reward:0.85\n", - "step: 3460, Red action: do-nothing, Blue reward:0.85\n", - "step: 3461, Red action: do-nothing, Blue reward:0.85\n", - "step: 3462, Red action: do-nothing, Blue reward:0.85\n", - "step: 3463, Red action: do-nothing, Blue reward:0.85\n", - "step: 3464, Red action: do-nothing, Blue reward:0.85\n", - "step: 3465, Red action: do-nothing, Blue reward:0.85\n", - "step: 3466, Red action: do-nothing, Blue reward:0.85\n", - "step: 3467, Red action: do-nothing, Blue reward:0.85\n", - "step: 3468, Red action: do-nothing, Blue reward:0.85\n", - "step: 3469, Red action: do-nothing, Blue reward:0.85\n", - "step: 3470, Red action: do-nothing, Blue reward:0.85\n", - "step: 3471, Red action: do-nothing, Blue reward:0.85\n", - "step: 3472, Red action: do-nothing, Blue reward:0.85\n", - "step: 3473, Red action: do-nothing, Blue reward:0.85\n", - "step: 3474, Red action: do-nothing, Blue reward:0.85\n", - "step: 3475, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3476, Red action: do-nothing, Blue reward:0.85\n", - "step: 3477, Red action: do-nothing, Blue reward:0.85\n", - "step: 3478, Red action: do-nothing, Blue reward:0.85\n", - "step: 3479, Red action: do-nothing, Blue reward:0.85\n", - "step: 3480, Red action: do-nothing, Blue reward:0.85\n", - "step: 3481, Red action: do-nothing, Blue reward:0.85\n", - "step: 3482, Red action: do-nothing, Blue reward:0.85\n", - "step: 3483, Red action: do-nothing, Blue reward:0.85\n", - "step: 3484, Red action: do-nothing, Blue reward:0.85\n", - "step: 3485, Red action: do-nothing, Blue reward:0.85\n", - "step: 3486, Red action: do-nothing, Blue reward:0.85\n", - "step: 3487, Red action: do-nothing, Blue reward:0.85\n", - "step: 3488, Red action: do-nothing, Blue reward:0.85\n", - "step: 3489, Red action: do-nothing, Blue reward:0.85\n", - "step: 3490, Red action: do-nothing, Blue reward:0.85\n", - "step: 3491, Red action: do-nothing, Blue reward:0.85\n", - "step: 3492, Red action: do-nothing, Blue reward:0.85\n", - "step: 3493, Red action: do-nothing, Blue reward:0.85\n", - "step: 3494, Red action: do-nothing, Blue reward:0.85\n", - "step: 3495, Red action: do-nothing, Blue reward:0.85\n", - "step: 3496, Red action: do-nothing, Blue reward:0.85\n", - "step: 3497, Red action: do-nothing, Blue reward:0.85\n", - "step: 3498, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3499, Red action: do-nothing, Blue reward:0.85\n", - "step: 3500, Red action: do-nothing, Blue reward:0.85\n", - "step: 3501, Red action: do-nothing, Blue reward:0.85\n", - "step: 3502, Red action: do-nothing, Blue reward:0.85\n", - "step: 3503, Red action: do-nothing, Blue reward:0.85\n", - "step: 3504, Red action: do-nothing, Blue reward:0.85\n", - "step: 3505, Red action: do-nothing, Blue reward:0.85\n", - "step: 3506, Red action: do-nothing, Blue reward:0.85\n", - "step: 3507, Red action: do-nothing, Blue reward:0.85\n", - "step: 3508, Red action: do-nothing, Blue reward:0.85\n", - "step: 3509, Red action: do-nothing, Blue reward:0.85\n", - "step: 3510, Red action: do-nothing, Blue reward:0.85\n", - "step: 3511, Red action: do-nothing, Blue reward:0.85\n", - "step: 3512, Red action: do-nothing, Blue reward:0.85\n", - "step: 3513, Red action: do-nothing, Blue reward:0.85\n", - "step: 3514, Red action: do-nothing, Blue reward:0.85\n", - "step: 3515, Red action: do-nothing, Blue reward:0.85\n", - "step: 3516, Red action: do-nothing, Blue reward:0.85\n", - "step: 3517, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3518, Red action: do-nothing, Blue reward:0.85\n", - "step: 3519, Red action: do-nothing, Blue reward:0.85\n", - "step: 3520, Red action: do-nothing, Blue reward:0.85\n", - "step: 3521, Red action: do-nothing, Blue reward:0.85\n", - "step: 3522, Red action: do-nothing, Blue reward:0.85\n", - "step: 3523, Red action: do-nothing, Blue reward:0.85\n", - "step: 3524, Red action: do-nothing, Blue reward:0.85\n", - "step: 3525, Red action: do-nothing, Blue reward:0.85\n", - "step: 3526, Red action: do-nothing, Blue reward:0.85\n", - "step: 3527, Red action: do-nothing, Blue reward:0.85\n", - "step: 3528, Red action: do-nothing, Blue reward:0.85\n", - "step: 3529, Red action: do-nothing, Blue reward:0.85\n", - "step: 3530, Red action: do-nothing, Blue reward:0.85\n", - "step: 3531, Red action: do-nothing, Blue reward:0.85\n", - "step: 3532, Red action: do-nothing, Blue reward:0.85\n", - "step: 3533, Red action: do-nothing, Blue reward:0.85\n", - "step: 3534, Red action: do-nothing, Blue reward:0.85\n", - "step: 3535, Red action: do-nothing, Blue reward:0.85\n", - "step: 3536, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3537, Red action: do-nothing, Blue reward:0.85\n", - "step: 3538, Red action: do-nothing, Blue reward:0.85\n", - "step: 3539, Red action: do-nothing, Blue reward:0.85\n", - "step: 3540, Red action: do-nothing, Blue reward:0.85\n", - "step: 3541, Red action: do-nothing, Blue reward:0.85\n", - "step: 3542, Red action: do-nothing, Blue reward:0.85\n", - "step: 3543, Red action: do-nothing, Blue reward:0.85\n", - "step: 3544, Red action: do-nothing, Blue reward:0.85\n", - "step: 3545, Red action: do-nothing, Blue reward:0.85\n", - "step: 3546, Red action: do-nothing, Blue reward:0.85\n", - "step: 3547, Red action: do-nothing, Blue reward:0.85\n", - "step: 3548, Red action: do-nothing, Blue reward:0.85\n", - "step: 3549, Red action: do-nothing, Blue reward:0.85\n", - "step: 3550, Red action: do-nothing, Blue reward:0.85\n", - "step: 3551, Red action: do-nothing, Blue reward:0.85\n", - "step: 3552, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3553, Red action: do-nothing, Blue reward:0.85\n", - "step: 3554, Red action: do-nothing, Blue reward:0.85\n", - "step: 3555, Red action: do-nothing, Blue reward:0.85\n", - "step: 3556, Red action: do-nothing, Blue reward:0.85\n", - "step: 3557, Red action: do-nothing, Blue reward:0.85\n", - "step: 3558, Red action: do-nothing, Blue reward:0.85\n", - "step: 3559, Red action: do-nothing, Blue reward:0.85\n", - "step: 3560, Red action: do-nothing, Blue reward:0.85\n", - "step: 3561, Red action: do-nothing, Blue reward:0.85\n", - "step: 3562, Red action: do-nothing, Blue reward:0.85\n", - "step: 3563, Red action: do-nothing, Blue reward:0.85\n", - "step: 3564, Red action: do-nothing, Blue reward:0.85\n", - "step: 3565, Red action: do-nothing, Blue reward:0.85\n", - "step: 3566, Red action: do-nothing, Blue reward:0.85\n", - "step: 3567, Red action: do-nothing, Blue reward:0.85\n", - "step: 3568, Red action: do-nothing, Blue reward:0.85\n", - "step: 3569, Red action: do-nothing, Blue reward:0.85\n", - "step: 3570, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3571, Red action: do-nothing, Blue reward:0.85\n", - "step: 3572, Red action: do-nothing, Blue reward:0.85\n", - "step: 3573, Red action: do-nothing, Blue reward:0.85\n", - "step: 3574, Red action: do-nothing, Blue reward:0.85\n", - "step: 3575, Red action: do-nothing, Blue reward:0.85\n", - "step: 3576, Red action: do-nothing, Blue reward:0.85\n", - "step: 3577, Red action: do-nothing, Blue reward:0.85\n", - "step: 3578, Red action: do-nothing, Blue reward:0.85\n", - "step: 3579, Red action: do-nothing, Blue reward:0.85\n", - "step: 3580, Red action: do-nothing, Blue reward:0.85\n", - "step: 3581, Red action: do-nothing, Blue reward:0.85\n", - "step: 3582, Red action: do-nothing, Blue reward:0.85\n", - "step: 3583, Red action: do-nothing, Blue reward:0.85\n", - "step: 3584, Red action: do-nothing, Blue reward:0.85\n", - "step: 3585, Red action: do-nothing, Blue reward:0.85\n", - "step: 3586, Red action: do-nothing, Blue reward:0.85\n", - "step: 3587, Red action: do-nothing, Blue reward:0.85\n", - "step: 3588, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3589, Red action: do-nothing, Blue reward:0.85\n", - "step: 3590, Red action: do-nothing, Blue reward:0.85\n", - "step: 3591, Red action: do-nothing, Blue reward:0.85\n", - "step: 3592, Red action: do-nothing, Blue reward:0.85\n", - "step: 3593, Red action: do-nothing, Blue reward:0.85\n", - "step: 3594, Red action: do-nothing, Blue reward:0.85\n", - "step: 3595, Red action: do-nothing, Blue reward:0.85\n", - "step: 3596, Red action: do-nothing, Blue reward:0.85\n", - "step: 3597, Red action: do-nothing, Blue reward:0.85\n", - "step: 3598, Red action: do-nothing, Blue reward:0.85\n", - "step: 3599, Red action: do-nothing, Blue reward:0.85\n", - "step: 3600, Red action: do-nothing, Blue reward:0.85\n", - "step: 3601, Red action: do-nothing, Blue reward:0.85\n", - "step: 3602, Red action: do-nothing, Blue reward:0.85\n", - "step: 3603, Red action: do-nothing, Blue reward:0.85\n", - "step: 3604, Red action: do-nothing, Blue reward:0.85\n", - "step: 3605, Red action: do-nothing, Blue reward:0.85\n", - "step: 3606, Red action: do-nothing, Blue reward:0.85\n", - "step: 3607, Red action: do-nothing, Blue reward:0.85\n", - "step: 3608, Red action: do-nothing, Blue reward:0.85\n", - "step: 3609, Red action: do-nothing, Blue reward:0.85\n", - "step: 3610, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3611, Red action: do-nothing, Blue reward:0.85\n", - "step: 3612, Red action: do-nothing, Blue reward:0.85\n", - "step: 3613, Red action: do-nothing, Blue reward:0.85\n", - "step: 3614, Red action: do-nothing, Blue reward:0.85\n", - "step: 3615, Red action: do-nothing, Blue reward:0.85\n", - "step: 3616, Red action: do-nothing, Blue reward:0.85\n", - "step: 3617, Red action: do-nothing, Blue reward:0.85\n", - "step: 3618, Red action: do-nothing, Blue reward:0.85\n", - "step: 3619, Red action: do-nothing, Blue reward:0.85\n", - "step: 3620, Red action: do-nothing, Blue reward:0.85\n", - "step: 3621, Red action: do-nothing, Blue reward:0.85\n", - "step: 3622, Red action: do-nothing, Blue reward:0.85\n", - "step: 3623, Red action: do-nothing, Blue reward:0.85\n", - "step: 3624, Red action: do-nothing, Blue reward:0.85\n", - "step: 3625, Red action: do-nothing, Blue reward:0.85\n", - "step: 3626, Red action: do-nothing, Blue reward:0.85\n", - "step: 3627, Red action: do-nothing, Blue reward:0.85\n", - "step: 3628, Red action: do-nothing, Blue reward:0.85\n", - "step: 3629, Red action: do-nothing, Blue reward:0.85\n", - "step: 3630, Red action: do-nothing, Blue reward:0.85\n", - "step: 3631, Red action: do-nothing, Blue reward:0.85\n", - "step: 3632, Red action: do-nothing, Blue reward:0.85\n", - "step: 3633, Red action: do-nothing, Blue reward:0.85\n", - "step: 3634, Red action: do-nothing, Blue reward:0.85\n", - "step: 3635, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3636, Red action: do-nothing, Blue reward:0.85\n", - "step: 3637, Red action: do-nothing, Blue reward:0.85\n", - "step: 3638, Red action: do-nothing, Blue reward:0.85\n", - "step: 3639, Red action: do-nothing, Blue reward:0.85\n", - "step: 3640, Red action: do-nothing, Blue reward:0.85\n", - "step: 3641, Red action: do-nothing, Blue reward:0.85\n", - "step: 3642, Red action: do-nothing, Blue reward:0.85\n", - "step: 3643, Red action: do-nothing, Blue reward:0.85\n", - "step: 3644, Red action: do-nothing, Blue reward:0.85\n", - "step: 3645, Red action: do-nothing, Blue reward:0.85\n", - "step: 3646, Red action: do-nothing, Blue reward:0.85\n", - "step: 3647, Red action: do-nothing, Blue reward:0.85\n", - "step: 3648, Red action: do-nothing, Blue reward:0.85\n", - "step: 3649, Red action: do-nothing, Blue reward:0.85\n", - "step: 3650, Red action: do-nothing, Blue reward:0.85\n", - "step: 3651, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3652, Red action: do-nothing, Blue reward:0.85\n", - "step: 3653, Red action: do-nothing, Blue reward:0.85\n", - "step: 3654, Red action: do-nothing, Blue reward:0.85\n", - "step: 3655, Red action: do-nothing, Blue reward:0.85\n", - "step: 3656, Red action: do-nothing, Blue reward:0.85\n", - "step: 3657, Red action: do-nothing, Blue reward:0.85\n", - "step: 3658, Red action: do-nothing, Blue reward:0.85\n", - "step: 3659, Red action: do-nothing, Blue reward:0.85\n", - "step: 3660, Red action: do-nothing, Blue reward:0.85\n", - "step: 3661, Red action: do-nothing, Blue reward:0.85\n", - "step: 3662, Red action: do-nothing, Blue reward:0.85\n", - "step: 3663, Red action: do-nothing, Blue reward:0.85\n", - "step: 3664, Red action: do-nothing, Blue reward:0.85\n", - "step: 3665, Red action: do-nothing, Blue reward:0.85\n", - "step: 3666, Red action: do-nothing, Blue reward:0.85\n", - "step: 3667, Red action: do-nothing, Blue reward:0.85\n", - "step: 3668, Red action: do-nothing, Blue reward:0.85\n", - "step: 3669, Red action: do-nothing, Blue reward:0.85\n", - "step: 3670, Red action: do-nothing, Blue reward:0.85\n", - "step: 3671, Red action: do-nothing, Blue reward:0.85\n", - "step: 3672, Red action: do-nothing, Blue reward:0.85\n", - "step: 3673, Red action: do-nothing, Blue reward:0.85\n", - "step: 3674, Red action: do-nothing, Blue reward:0.85\n", - "step: 3675, Red action: do-nothing, Blue reward:0.85\n", - "step: 3676, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3677, Red action: do-nothing, Blue reward:0.85\n", - "step: 3678, Red action: do-nothing, Blue reward:0.85\n", - "step: 3679, Red action: do-nothing, Blue reward:0.85\n", - "step: 3680, Red action: do-nothing, Blue reward:0.85\n", - "step: 3681, Red action: do-nothing, Blue reward:0.85\n", - "step: 3682, Red action: do-nothing, Blue reward:0.85\n", - "step: 3683, Red action: do-nothing, Blue reward:0.85\n", - "step: 3684, Red action: do-nothing, Blue reward:0.85\n", - "step: 3685, Red action: do-nothing, Blue reward:0.85\n", - "step: 3686, Red action: do-nothing, Blue reward:0.85\n", - "step: 3687, Red action: do-nothing, Blue reward:0.85\n", - "step: 3688, Red action: do-nothing, Blue reward:0.85\n", - "step: 3689, Red action: do-nothing, Blue reward:0.85\n", - "step: 3690, Red action: do-nothing, Blue reward:0.85\n", - "step: 3691, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3692, Red action: do-nothing, Blue reward:0.85\n", - "step: 3693, Red action: do-nothing, Blue reward:0.85\n", - "step: 3694, Red action: do-nothing, Blue reward:0.85\n", - "step: 3695, Red action: do-nothing, Blue reward:0.85\n", - "step: 3696, Red action: do-nothing, Blue reward:0.85\n", - "step: 3697, Red action: do-nothing, Blue reward:0.85\n", - "step: 3698, Red action: do-nothing, Blue reward:0.85\n", - "step: 3699, Red action: do-nothing, Blue reward:0.85\n", - "step: 3700, Red action: do-nothing, Blue reward:0.85\n", - "step: 3701, Red action: do-nothing, Blue reward:0.85\n", - "step: 3702, Red action: do-nothing, Blue reward:0.85\n", - "step: 3703, Red action: do-nothing, Blue reward:0.85\n", - "step: 3704, Red action: do-nothing, Blue reward:0.85\n", - "step: 3705, Red action: do-nothing, Blue reward:0.85\n", - "step: 3706, Red action: do-nothing, Blue reward:0.85\n", - "step: 3707, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3708, Red action: do-nothing, Blue reward:0.85\n", - "step: 3709, Red action: do-nothing, Blue reward:0.85\n", - "step: 3710, Red action: do-nothing, Blue reward:0.85\n", - "step: 3711, Red action: do-nothing, Blue reward:0.85\n", - "step: 3712, Red action: do-nothing, Blue reward:0.85\n", - "step: 3713, Red action: do-nothing, Blue reward:0.85\n", - "step: 3714, Red action: do-nothing, Blue reward:0.85\n", - "step: 3715, Red action: do-nothing, Blue reward:0.85\n", - "step: 3716, Red action: do-nothing, Blue reward:0.85\n", - "step: 3717, Red action: do-nothing, Blue reward:0.85\n", - "step: 3718, Red action: do-nothing, Blue reward:0.85\n", - "step: 3719, Red action: do-nothing, Blue reward:0.85\n", - "step: 3720, Red action: do-nothing, Blue reward:0.85\n", - "step: 3721, Red action: do-nothing, Blue reward:0.85\n", - "step: 3722, Red action: do-nothing, Blue reward:0.85\n", - "step: 3723, Red action: do-nothing, Blue reward:0.85\n", - "step: 3724, Red action: do-nothing, Blue reward:0.85\n", - "step: 3725, Red action: do-nothing, Blue reward:0.85\n", - "step: 3726, Red action: do-nothing, Blue reward:0.85\n", - "step: 3727, Red action: do-nothing, Blue reward:0.85\n", - "step: 3728, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3729, Red action: do-nothing, Blue reward:0.85\n", - "step: 3730, Red action: do-nothing, Blue reward:0.85\n", - "step: 3731, Red action: do-nothing, Blue reward:0.85\n", - "step: 3732, Red action: do-nothing, Blue reward:0.85\n", - "step: 3733, Red action: do-nothing, Blue reward:0.85\n", - "step: 3734, Red action: do-nothing, Blue reward:0.85\n", - "step: 3735, Red action: do-nothing, Blue reward:0.85\n", - "step: 3736, Red action: do-nothing, Blue reward:0.85\n", - "step: 3737, Red action: do-nothing, Blue reward:0.85\n", - "step: 3738, Red action: do-nothing, Blue reward:0.85\n", - "step: 3739, Red action: do-nothing, Blue reward:0.85\n", - "step: 3740, Red action: do-nothing, Blue reward:0.85\n", - "step: 3741, Red action: do-nothing, Blue reward:0.85\n", - "step: 3742, Red action: do-nothing, Blue reward:0.85\n", - "step: 3743, Red action: do-nothing, Blue reward:0.85\n", - "step: 3744, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3745, Red action: do-nothing, Blue reward:0.85\n", - "step: 3746, Red action: do-nothing, Blue reward:0.85\n", - "step: 3747, Red action: do-nothing, Blue reward:0.85\n", - "step: 3748, Red action: do-nothing, Blue reward:0.85\n", - "step: 3749, Red action: do-nothing, Blue reward:0.85\n", - "step: 3750, Red action: do-nothing, Blue reward:0.85\n", - "step: 3751, Red action: do-nothing, Blue reward:0.85\n", - "step: 3752, Red action: do-nothing, Blue reward:0.85\n", - "step: 3753, Red action: do-nothing, Blue reward:0.85\n", - "step: 3754, Red action: do-nothing, Blue reward:0.85\n", - "step: 3755, Red action: do-nothing, Blue reward:0.85\n", - "step: 3756, Red action: do-nothing, Blue reward:0.85\n", - "step: 3757, Red action: do-nothing, Blue reward:0.85\n", - "step: 3758, Red action: do-nothing, Blue reward:0.85\n", - "step: 3759, Red action: do-nothing, Blue reward:0.85\n", - "step: 3760, Red action: do-nothing, Blue reward:0.85\n", - "step: 3761, Red action: do-nothing, Blue reward:0.85\n", - "step: 3762, Red action: do-nothing, Blue reward:0.85\n", - "step: 3763, Red action: do-nothing, Blue reward:0.85\n", - "step: 3764, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3765, Red action: do-nothing, Blue reward:0.85\n", - "step: 3766, Red action: do-nothing, Blue reward:0.85\n", - "step: 3767, Red action: do-nothing, Blue reward:0.85\n", - "step: 3768, Red action: do-nothing, Blue reward:0.85\n", - "step: 3769, Red action: do-nothing, Blue reward:0.85\n", - "step: 3770, Red action: do-nothing, Blue reward:0.85\n", - "step: 3771, Red action: do-nothing, Blue reward:0.85\n", - "step: 3772, Red action: do-nothing, Blue reward:0.85\n", - "step: 3773, Red action: do-nothing, Blue reward:0.85\n", - "step: 3774, Red action: do-nothing, Blue reward:0.85\n", - "step: 3775, Red action: do-nothing, Blue reward:0.85\n", - "step: 3776, Red action: do-nothing, Blue reward:0.85\n", - "step: 3777, Red action: do-nothing, Blue reward:0.85\n", - "step: 3778, Red action: do-nothing, Blue reward:0.85\n", - "step: 3779, Red action: do-nothing, Blue reward:0.85\n", - "step: 3780, Red action: do-nothing, Blue reward:0.85\n", - "step: 3781, Red action: do-nothing, Blue reward:0.85\n", - "step: 3782, Red action: do-nothing, Blue reward:0.85\n", - "step: 3783, Red action: do-nothing, Blue reward:0.85\n", - "step: 3784, Red action: node-application-execute, Blue reward:0.85\n", - "step: 3785, Red action: do-nothing, Blue reward:0.85\n", - "step: 3786, Red action: do-nothing, Blue reward:0.85\n", - "step: 3787, Red action: do-nothing, Blue reward:0.85\n", - "step: 3788, Red action: do-nothing, Blue reward:0.85\n", - "step: 3789, Red action: do-nothing, Blue reward:0.85\n", - "step: 3790, Red action: do-nothing, Blue reward:0.85\n", - "step: 3791, Red action: do-nothing, Blue reward:0.85\n", - "step: 3792, Red action: do-nothing, Blue reward:0.85\n", - "step: 3793, Red action: do-nothing, Blue reward:0.85\n", - "step: 3794, Red action: do-nothing, Blue reward:0.85\n", - "step: 3795, Red action: do-nothing, Blue reward:0.85\n", - "step: 3796, Red action: do-nothing, Blue reward:0.85\n", - "step: 3797, Red action: do-nothing, Blue reward:0.85\n", - "step: 3798, Red action: do-nothing, Blue reward:0.85\n", - "step: 3799, Red action: do-nothing, Blue reward:0.85\n", - "step: 3800, Red action: do-nothing, Blue reward:0.85\n", - "step: 3801, Red action: do-nothing, Blue reward:0.85\n", - "step: 3802, Red action: do-nothing, Blue reward:0.85\n" - ] - }, - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[11], line 11\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstep: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00menv\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mstep_counter\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Red action: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00minfo[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124magent_actions\u001b[39m\u001b[38;5;124m'\u001b[39m][\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata_manipulation_attacker\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39maction\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Blue reward:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mreward\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m )\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28mabs\u001b[39m(reward \u001b[38;5;241m-\u001b[39m \u001b[38;5;241m0.8\u001b[39m) \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m1e-5\u001b[39m:\n\u001b[0;32m---> 11\u001b[0m obs, reward, terminated, truncated, info \u001b[38;5;241m=\u001b[39m \u001b[43menv\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstep\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# do nothing\u001b[39;00m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstep: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00menv\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mstep_counter\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Red action: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00minfo[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124magent_actions\u001b[39m\u001b[38;5;124m'\u001b[39m][\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mdata_manipulation_attacker\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39maction\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m, Blue reward:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mreward\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m )\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m env\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mstep_counter \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m10000\u001b[39m:\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/session/environment.py:111\u001b[0m, in \u001b[0;36mPrimaiteGymEnv.step\u001b[0;34m(self, action)\u001b[0m\n\u001b[1;32m 109\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mapply_agent_actions()\n\u001b[1;32m 110\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39madvance_timestep()\n\u001b[0;32m--> 111\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mgame\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_sim_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 112\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgame\u001b[38;5;241m.\u001b[39mupdate_agents(state)\n\u001b[1;32m 114\u001b[0m next_obs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_obs() \u001b[38;5;66;03m# this doesn't update observation, just gets the current observation\u001b[39;00m\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/game/game.py:151\u001b[0m, in \u001b[0;36mPrimaiteGame.get_sim_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 149\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_sim_state\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Dict:\n\u001b[1;32m 150\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get the current state of the simulation.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 151\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msimulation\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/sim_container.py:57\u001b[0m, in \u001b[0;36mSimulation.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 46\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 47\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of this object.\u001b[39;00m\n\u001b[1;32m 48\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 52\u001b[0m \u001b[38;5;124;03m:rtype: Dict\u001b[39;00m\n\u001b[1;32m 53\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 54\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 55\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 56\u001b[0m {\n\u001b[0;32m---> 57\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnetwork\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnetwork\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m 58\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mdomain\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdomain\u001b[38;5;241m.\u001b[39mdescribe_state(),\n\u001b[1;32m 59\u001b[0m }\n\u001b[1;32m 60\u001b[0m )\n\u001b[1;32m 61\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/container.py:260\u001b[0m, in \u001b[0;36mNetwork.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 252\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 253\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of the Network.\u001b[39;00m\n\u001b[1;32m 254\u001b[0m \n\u001b[1;32m 255\u001b[0m \u001b[38;5;124;03m:return: A dictionary capturing the current state of the Network and its child objects.\u001b[39;00m\n\u001b[1;32m 256\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 257\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 258\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 259\u001b[0m {\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnodes\u001b[39m\u001b[38;5;124m\"\u001b[39m: {node\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname: node\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m node \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnodes\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 261\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinks\u001b[39m\u001b[38;5;124m\"\u001b[39m: {},\n\u001b[1;32m 262\u001b[0m }\n\u001b[1;32m 263\u001b[0m )\n\u001b[1;32m 264\u001b[0m \u001b[38;5;66;03m# Update the links one-by-one. The key is a 4-tuple of `hostname_a, port_a, hostname_b, port_b`\u001b[39;00m\n\u001b[1;32m 265\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m _, link \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlinks\u001b[38;5;241m.\u001b[39mitems():\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/container.py:260\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 252\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 253\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of the Network.\u001b[39;00m\n\u001b[1;32m 254\u001b[0m \n\u001b[1;32m 255\u001b[0m \u001b[38;5;124;03m:return: A dictionary capturing the current state of the Network and its child objects.\u001b[39;00m\n\u001b[1;32m 256\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 257\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 258\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 259\u001b[0m {\n\u001b[0;32m--> 260\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnodes\u001b[39m\u001b[38;5;124m\"\u001b[39m: {node\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname: \u001b[43mnode\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m node \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnodes\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 261\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinks\u001b[39m\u001b[38;5;124m\"\u001b[39m: {},\n\u001b[1;32m 262\u001b[0m }\n\u001b[1;32m 263\u001b[0m )\n\u001b[1;32m 264\u001b[0m \u001b[38;5;66;03m# Update the links one-by-one. The key is a 4-tuple of `hostname_a, port_a, hostname_b, port_b`\u001b[39;00m\n\u001b[1;32m 265\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m _, link \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlinks\u001b[38;5;241m.\u001b[39mitems():\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/hardware/base.py:1900\u001b[0m, in \u001b[0;36mNode.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 1881\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1882\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of this object.\u001b[39;00m\n\u001b[1;32m 1883\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1887\u001b[0m \u001b[38;5;124;03m:rtype: Dict\u001b[39;00m\n\u001b[1;32m 1888\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1889\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1890\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 1891\u001b[0m {\n\u001b[1;32m 1892\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhostname\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname,\n\u001b[1;32m 1893\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moperating_state\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39moperating_state\u001b[38;5;241m.\u001b[39mvalue,\n\u001b[1;32m 1894\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNICs\u001b[39m\u001b[38;5;124m\"\u001b[39m: {\n\u001b[1;32m 1895\u001b[0m eth_num: network_interface\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1896\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m eth_num, network_interface \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnetwork_interface\u001b[38;5;241m.\u001b[39mitems()\n\u001b[1;32m 1897\u001b[0m },\n\u001b[1;32m 1898\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfile_system\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfile_system\u001b[38;5;241m.\u001b[39mdescribe_state(),\n\u001b[1;32m 1899\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapplications\u001b[39m\u001b[38;5;124m\"\u001b[39m: {app\u001b[38;5;241m.\u001b[39mname: app\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m app \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapplications\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[0;32m-> 1900\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mservices\u001b[39m\u001b[38;5;124m\"\u001b[39m: {svc\u001b[38;5;241m.\u001b[39mname: svc\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m svc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mservices\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1901\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprocess\u001b[39m\u001b[38;5;124m\"\u001b[39m: {proc\u001b[38;5;241m.\u001b[39mname: proc\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m proc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprocesses\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1902\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrevealed_to_red\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mrevealed_to_red,\n\u001b[1;32m 1903\u001b[0m }\n\u001b[1;32m 1904\u001b[0m )\n\u001b[1;32m 1905\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/network/hardware/base.py:1900\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 1881\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1882\u001b[0m \u001b[38;5;124;03mProduce a dictionary describing the current state of this object.\u001b[39;00m\n\u001b[1;32m 1883\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1887\u001b[0m \u001b[38;5;124;03m:rtype: Dict\u001b[39;00m\n\u001b[1;32m 1888\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 1889\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1890\u001b[0m state\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[1;32m 1891\u001b[0m {\n\u001b[1;32m 1892\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhostname\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mhostname,\n\u001b[1;32m 1893\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124moperating_state\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39moperating_state\u001b[38;5;241m.\u001b[39mvalue,\n\u001b[1;32m 1894\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNICs\u001b[39m\u001b[38;5;124m\"\u001b[39m: {\n\u001b[1;32m 1895\u001b[0m eth_num: network_interface\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 1896\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m eth_num, network_interface \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnetwork_interface\u001b[38;5;241m.\u001b[39mitems()\n\u001b[1;32m 1897\u001b[0m },\n\u001b[1;32m 1898\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfile_system\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfile_system\u001b[38;5;241m.\u001b[39mdescribe_state(),\n\u001b[1;32m 1899\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mapplications\u001b[39m\u001b[38;5;124m\"\u001b[39m: {app\u001b[38;5;241m.\u001b[39mname: app\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m app \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mapplications\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[0;32m-> 1900\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mservices\u001b[39m\u001b[38;5;124m\"\u001b[39m: {svc\u001b[38;5;241m.\u001b[39mname: \u001b[43msvc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdescribe_state\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m svc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mservices\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1901\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprocess\u001b[39m\u001b[38;5;124m\"\u001b[39m: {proc\u001b[38;5;241m.\u001b[39mname: proc\u001b[38;5;241m.\u001b[39mdescribe_state() \u001b[38;5;28;01mfor\u001b[39;00m proc \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mprocesses\u001b[38;5;241m.\u001b[39mvalues()},\n\u001b[1;32m 1902\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrevealed_to_red\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mconfig\u001b[38;5;241m.\u001b[39mrevealed_to_red,\n\u001b[1;32m 1903\u001b[0m }\n\u001b[1;32m 1904\u001b[0m )\n\u001b[1;32m 1905\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", - "File \u001b[0;32m~/arcd/PrimAITE/src/primaite/simulator/system/services/ntp/ntp_client.py:61\u001b[0m, in \u001b[0;36mNTPClient.describe_state\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 50\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdescribe_state\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Dict:\n\u001b[1;32m 51\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 52\u001b[0m \u001b[38;5;124;03m Describes the current state of the software.\u001b[39;00m\n\u001b[1;32m 53\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[38;5;124;03m :rtype: Dict\u001b[39;00m\n\u001b[1;32m 60\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m---> 61\u001b[0m state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mdescribe_state()\n\u001b[1;32m 62\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m state\n", - "\u001b[0;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], + "outputs": [], "source": [ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", From 3651d033d6f2ba5d527de70c00f796c2ff96bfeb Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 17 Feb 2025 17:34:22 +0000 Subject: [PATCH 208/224] fix instantiation of network nodes --- src/primaite/simulator/network/hardware/base.py | 4 +--- .../simulator/network/hardware/nodes/host/host_node.py | 4 ++-- .../simulator/network/hardware/nodes/network/router.py | 1 - .../simulator/network/hardware/nodes/network/switch.py | 5 ++--- src/primaite/simulator/system/services/arp/arp.py | 4 ++-- src/primaite/simulator/system/software.py | 2 +- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 8653359a..b20fb351 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -639,10 +639,8 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): `default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption. """ super().enable() - try: + if hasattr(self._connected_node, "default_gateway_hello"): self._connected_node.default_gateway_hello() - except AttributeError: - pass return True @abstractmethod diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index 76d9167c..2c1910c4 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -333,7 +333,7 @@ class HostNode(Node, discriminator="host-node"): class ConfigSchema(Node.ConfigSchema): """Configuration Schema for HostNode class.""" - type: Literal["host-node"] + type: Literal["host-node"] = "host-node" hostname: str = "HostNode" subnet_mask: IPV4Address = "255.255.255.0" ip_address: IPV4Address @@ -375,7 +375,7 @@ class HostNode(Node, discriminator="host-node"): This method is invoked to ensure the host node can communicate with its default gateway, primarily to confirm network connectivity and populate the ARP cache with the gateway's MAC address. """ - if self.operating_state == NodeOperatingState.ON and self.default_gateway: + if self.operating_state == NodeOperatingState.ON and self.config.default_gateway: self.software_manager.arp.get_default_gateway_mac_address() def receive_frame(self, frame: Frame, from_network_interface: NIC): diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 3b35600b..f2a4652c 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1351,7 +1351,6 @@ class Router(NetworkNode, discriminator="router"): :return: A dictionary representing the current state. """ state = super().describe_state() - state["num_ports"] = self.config.num_ports state["acl"] = self.acl.describe_state() return state diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 1f2bc135..6e5814d0 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -103,8 +103,8 @@ class Switch(NetworkNode, discriminator="switch"): type: Literal["switch"] = "switch" hostname: str = "Switch" - num_ports: int = 24 - "The number of ports on the switch. Default is 24." + num_ports: int = 8 + "The number of ports on the switch." config: ConfigSchema = Field(default_factory=lambda: Switch.ConfigSchema()) @@ -139,7 +139,6 @@ class Switch(NetworkNode, discriminator="switch"): """ state = super().describe_state() state["ports"] = {port_num: port.describe_state() for port_num, port in self.network_interface.items()} - state["num_ports"] = self.config.num_ports # redundant? state["mac_address_table"] = {mac: port.port_num for mac, port in self.mac_address_table.items()} return state diff --git a/src/primaite/simulator/system/services/arp/arp.py b/src/primaite/simulator/system/services/arp/arp.py index b0630d5d..348cc03e 100644 --- a/src/primaite/simulator/system/services/arp/arp.py +++ b/src/primaite/simulator/system/services/arp/arp.py @@ -137,8 +137,8 @@ class ARP(Service, discriminator="arp"): break if use_default_gateway: - if self.software_manager.node.default_gateway: - target_ip_address = self.software_manager.node.default_gateway + if self.software_manager.node.config.default_gateway: + target_ip_address = self.software_manager.node.config.default_gateway else: return diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 950f77c6..86b57818 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -82,7 +82,7 @@ class Software(SimComponent, ABC): """Configurable options for all software.""" model_config = ConfigDict(extra="forbid") - starting_health_state: SoftwareHealthState = SoftwareHealthState.UNUSED + starting_health_state: SoftwareHealthState = SoftwareHealthState.GOOD criticality: SoftwareCriticality = SoftwareCriticality.LOWEST fixing_duration: int = 2 From de88974332076c0f1841b0178674f4b73543701f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 17 Feb 2025 18:14:07 +0000 Subject: [PATCH 209/224] Fix airspace hello --- src/primaite/simulator/network/airspace.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 7ede0bb0..434940f8 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -449,10 +449,8 @@ class IPWirelessNetworkInterface(WirelessNetworkInterface, Layer3Interface, ABC) `default_gateway_hello` method is not defined, ignoring such errors to proceed without interruption. """ super().enable() - try: + if hasattr(self._connected_node, "default_gateway_hello"): self._connected_node.default_gateway_hello() - except AttributeError: - pass @abstractmethod def receive_frame(self, frame: Frame) -> bool: From 46240e49a41510b4f2b708b6d489a6a97e27d40f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 17 Feb 2025 18:46:09 +0000 Subject: [PATCH 210/224] update tests and make office lan creation work like previously --- src/primaite/simulator/network/creation.py | 15 +++++++++++++-- .../_network/_hardware/nodes/test_switch.py | 1 - .../_system/_applications/test_applications.py | 6 +++--- .../_simulator/_system/_services/test_services.py | 12 ++++++------ .../_primaite/_simulator/_system/test_software.py | 4 ++-- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/primaite/simulator/network/creation.py b/src/primaite/simulator/network/creation.py index 255b7bf5..089ed00d 100644 --- a/src/primaite/simulator/network/creation.py +++ b/src/primaite/simulator/network/creation.py @@ -155,7 +155,12 @@ class OfficeLANAdder(NetworkNodeAdder, discriminator="office-lan"): # Create a core switch if more than one edge switch is needed if num_of_switches > 1: core_switch = Switch.from_config( - config={"type": "switch", "hostname": f"switch_core_{config.lan_name}", "start_up_duration": 0} + config={ + "type": "switch", + "hostname": f"switch_core_{config.lan_name}", + "start_up_duration": 0, + "num_ports": 24, + } ) core_switch.power_on() network.add_node(core_switch) @@ -183,7 +188,12 @@ class OfficeLANAdder(NetworkNodeAdder, discriminator="office-lan"): switch_port = 0 switch_n = 1 switch = Switch.from_config( - config={"type": "switch", "hostname": f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration": 0} + config={ + "type": "switch", + "hostname": f"switch_edge_{switch_n}_{config.lan_name}", + "start_up_duration": 0, + "num_ports": 24, + } ) switch.power_on() network.add_node(switch) @@ -207,6 +217,7 @@ class OfficeLANAdder(NetworkNodeAdder, discriminator="office-lan"): "type": "switch", "hostname": f"switch_edge_{switch_n}_{config.lan_name}", "start_up_duration": 0, + "num_ports": 24, } ) switch.power_on() diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py index e45fe45d..94b1764d 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -17,4 +17,3 @@ def switch() -> Switch: def test_describe_state(switch): state = switch.describe_state() assert len(state.get("ports")) is 8 - assert state.get("num_ports") is 8 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py index dd29f18e..6cccad91 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_applications.py @@ -18,7 +18,7 @@ def test_scan(application): def test_run_application(application): assert application.operating_state == ApplicationOperatingState.CLOSED - assert application.health_state_actual == SoftwareHealthState.UNUSED + assert application.health_state_actual == SoftwareHealthState.GOOD application.run() assert application.operating_state == ApplicationOperatingState.RUNNING @@ -37,9 +37,9 @@ def test_close_application(application): def test_application_describe_states(application): assert application.operating_state == ApplicationOperatingState.CLOSED - assert application.health_state_actual == SoftwareHealthState.UNUSED + assert application.health_state_actual == SoftwareHealthState.GOOD - assert SoftwareHealthState.UNUSED.value == application.describe_state().get("health_state_actual") + assert SoftwareHealthState.GOOD.value == application.describe_state().get("health_state_actual") application.run() assert SoftwareHealthState.GOOD.value == application.describe_state().get("health_state_actual") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py index 5598e1a7..fe78aa65 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_services.py @@ -22,7 +22,7 @@ def test_scan(service): def test_start_service(service): assert service.operating_state == ServiceOperatingState.STOPPED - assert service.health_state_actual == SoftwareHealthState.UNUSED + assert service.health_state_actual == SoftwareHealthState.GOOD service.start() assert service.operating_state == ServiceOperatingState.RUNNING @@ -43,7 +43,7 @@ def test_pause_and_resume_service(service): assert service.operating_state == ServiceOperatingState.STOPPED service.resume() assert service.operating_state == ServiceOperatingState.STOPPED - assert service.health_state_actual == SoftwareHealthState.UNUSED + assert service.health_state_actual == SoftwareHealthState.GOOD service.start() assert service.health_state_actual == SoftwareHealthState.GOOD @@ -58,11 +58,11 @@ def test_pause_and_resume_service(service): def test_restart(service): assert service.operating_state == ServiceOperatingState.STOPPED - assert service.health_state_actual == SoftwareHealthState.UNUSED + assert service.health_state_actual == SoftwareHealthState.GOOD service.restart() # Service is STOPPED. Restart will only work if the service was PAUSED or RUNNING assert service.operating_state == ServiceOperatingState.STOPPED - assert service.health_state_actual == SoftwareHealthState.UNUSED + assert service.health_state_actual == SoftwareHealthState.GOOD service.start() assert service.operating_state == ServiceOperatingState.RUNNING @@ -157,11 +157,11 @@ def test_service_fixing(service): def test_enable_disable(service): service.disable() assert service.operating_state == ServiceOperatingState.DISABLED - assert service.health_state_actual == SoftwareHealthState.UNUSED + assert service.health_state_actual == SoftwareHealthState.GOOD service.enable() assert service.operating_state == ServiceOperatingState.STOPPED - assert service.health_state_actual == SoftwareHealthState.UNUSED + assert service.health_state_actual == SoftwareHealthState.GOOD def test_overwhelm_service(service): diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index 8c39c41d..9ad0dbcb 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -39,6 +39,6 @@ def test_software_creation(software): def test_software_set_health_state(software): - assert software.health_state_actual == SoftwareHealthState.UNUSED - software.set_health_state(SoftwareHealthState.GOOD) assert software.health_state_actual == SoftwareHealthState.GOOD + software.set_health_state(SoftwareHealthState.COMPROMISED) + assert software.health_state_actual == SoftwareHealthState.COMPROMISED From 22b197a79aaf287895398083e75396a1c9d7c04f Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 18 Feb 2025 09:13:33 +0000 Subject: [PATCH 211/224] Removal of leftover comment from software.py --- src/primaite/simulator/system/software.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 8fb64d73..950f77c6 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -110,7 +110,6 @@ class Software(SimComponent, ABC): "The folder on the file system the Software uses." _fixing_countdown: Optional[int] = None "Current number of ticks left to patch the software." - # parent: Optional[Node] = None def __init__(self, **kwargs): super().__init__(**kwargs) From 8c7f8cd0ecb257df594825201dce6b598f35f5cf Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 18 Feb 2025 11:36:22 +0000 Subject: [PATCH 212/224] #3075: Fix final two demo notebooks --- .../create-simulation_demo.ipynb | 49 ++++++++++++++++--- .../network_simulator_demo.ipynb | 16 +----- src/primaite/simulator/network/networks.py | 2 +- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb index 690e7856..46f03be6 100644 --- a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb +++ b/src/primaite/simulator/_package_data/create-simulation_demo.ipynb @@ -6,7 +6,7 @@ "source": [ "# Build a simulation using the Python API\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "Currently, this notebook manipulates the simulation by directly placing objects inside of the attributes of the network and domain. It should be refactored when proper methods exist for adding these objects." ] @@ -70,9 +70,23 @@ "metadata": {}, "outputs": [], "source": [ - "my_pc = Computer(hostname=\"Computer\", ip_address=\"192.168.1.10\", subnet_mask=\"255.255.255.0\")\n", + "my_pc = Computer.from_config(\n", + " config={\n", + " \"type\": \"computer\",\n", + " \"hostname\":\"pc_1\",\n", + " \"ip_address\":\"192.168.1.10\",\n", + " \"subnet_mask\":\"255.255.255.0\",\n", + " }\n", + " )\n", "net.add_node(my_pc)\n", - "my_server = Server(hostname=\"Server\", ip_address=\"192.168.1.11\", subnet_mask=\"255.255.255.0\")\n", + "my_server = Server.from_config(\n", + " config={\n", + " \"type\": \"server\",\n", + " \"hostname\":\"Server\",\n", + " \"ip_address\":\"192.168.1.11\",\n", + " \"subnet_mask\":\"255.255.255.0\"\n", + " }\n", + ")\n", "net.add_node(my_server)\n" ] }, @@ -99,7 +113,13 @@ "metadata": {}, "outputs": [], "source": [ - "my_switch = Switch(hostname=\"switch1\", num_ports=12)\n", + "my_switch = Switch.from_config(\n", + " config = {\n", + " \"type\":\"switch\",\n", + " \"hostname\":\"switch1\",\n", + " \"num_ports\":12\n", + " }\n", + ")\n", "net.add_node(my_switch)\n", "\n", "pc_nic = NIC(ip_address=\"130.1.1.1\", gateway=\"130.1.1.255\", subnet_mask=\"255.255.255.0\")\n", @@ -163,16 +183,30 @@ "metadata": {}, "outputs": [], "source": [ + "from pydantic import Field\n", + "\n", "from pathlib import Path\n", "from primaite.simulator.system.applications.application import Application, ApplicationOperatingState\n", "from primaite.simulator.system.software import SoftwareHealthState, SoftwareCriticality\n", "from primaite.simulator.file_system.file_system import FileSystem\n", "from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP\n", "from primaite.utils.validation.port import PORT_LOOKUP\n", + "from primaite.simulator.system.core.sys_log import SysLog\n", "\n", "\n", "# no applications exist yet so we will create our own.\n", "class MSPaint(Application, discriminator=\"MSPaint\"):\n", + " class ConfigSchema(Application.ConfigSchema):\n", + " type: str = \"MSPaint\"\n", + "\n", + " config: ConfigSchema = Field(default_factory=lambda: MSPaint.ConfigSchema())\n", + "\n", + " def __init__(self, **kwargs):\n", + " kwargs[\"name\"] = \"MSPaint\"\n", + " kwargs[\"port\"] = PORT_LOOKUP[\"HTTP\"]\n", + " kwargs[\"protocol\"] = PROTOCOL_LOOKUP[\"NONE\"]\n", + " super().__init__(**kwargs)\n", + "\n", " def describe_state(self):\n", " return super().describe_state()" ] @@ -183,7 +217,8 @@ "metadata": {}, "outputs": [], "source": [ - "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, port=PORT_LOOKUP[\"HTTP\"], protocol = PROTOCOL_LOOKUP[\"NONE\"],operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual', file_system=FileSystem(sys_log=SysLog(hostname=\"Test\"), sim_root=Path(__name__).parent),)" + "my_pc.software_manager.install(MSPaint)\n", + "mspaint = my_pc.software_manager.software.get(\"MSPaint\")" ] }, { @@ -250,7 +285,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -264,7 +299,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb index 58c07fe5..be930ac0 100644 --- a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb +++ b/src/primaite/simulator/_package_data/network_simulator_demo.ipynb @@ -7,7 +7,7 @@ "source": [ "# PrimAITE Router Simulation Demo\n", "\n", - "© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK\n", + "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n", "\n", "This demo uses a modified version of the ARCD Use Case 2 Network (seen below) to demonstrate the capabilities of the Network simulator in PrimAITE." ] @@ -650,21 +650,9 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" } }, "nbformat": 4, diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 5e677679..5d558e80 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -295,7 +295,7 @@ def arcd_uc2_network() -> Network: # Security Suite security_suite_cfg = { "type": "server", - "hostname": "backup_server", + "hostname": "security_suite", "ip_address": "192.168.1.110", "subnet_mask": "255.255.255.0", "default_gateway": "192.168.1.1", From 72f80e55da0a17766975bf1d73aaff75786ec303 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 18 Feb 2025 11:51:35 +0000 Subject: [PATCH 213/224] #3075: Move demo notebook to correct folder. --- .../_package_data => notebooks}/create-simulation_demo.ipynb | 0 .../_package_data => notebooks}/network_simulator_demo.ipynb | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/primaite/{simulator/_package_data => notebooks}/create-simulation_demo.ipynb (100%) rename src/primaite/{simulator/_package_data => notebooks}/network_simulator_demo.ipynb (100%) diff --git a/src/primaite/simulator/_package_data/create-simulation_demo.ipynb b/src/primaite/notebooks/create-simulation_demo.ipynb similarity index 100% rename from src/primaite/simulator/_package_data/create-simulation_demo.ipynb rename to src/primaite/notebooks/create-simulation_demo.ipynb diff --git a/src/primaite/simulator/_package_data/network_simulator_demo.ipynb b/src/primaite/notebooks/network_simulator_demo.ipynb similarity index 100% rename from src/primaite/simulator/_package_data/network_simulator_demo.ipynb rename to src/primaite/notebooks/network_simulator_demo.ipynb From 90d7af3ff9fb0bc5fd60515b3d30752d7da27493 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 18 Feb 2025 12:42:33 +0000 Subject: [PATCH 214/224] #3075: Remove references to src/primaite/simulator/_package_data after notebook move. --- .azure/azure-ci-build-pipeline.yaml | 4 ---- MANIFEST.in | 1 - 2 files changed, 5 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 624c9ca4..20c5b239 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -113,8 +113,6 @@ stages: - script: | pytest --nbmake -n=auto src/primaite/notebooks --junit-xml=./notebook-tests/notebooks.xml notebooks_exit_code=$? - pytest --nbmake -n=auto src/primaite/simulator/_package_data --junit-xml=./notebook-tests/package-notebooks.xml - package_notebooks_exit_code=$? # Fail step if either of these do not have exit code 0 if [ $notebooks_exit_code -ne 0 ] || [ $package_notebooks_exit_code -ne 0 ]; then exit 1 @@ -126,8 +124,6 @@ stages: - script: | pytest --nbmake -n=auto src/primaite/notebooks --junit-xml=./notebook-tests/notebooks.xml set notebooks_exit_code=%ERRORLEVEL% - pytest --nbmake -n=auto src/primaite/simulator/_package_data --junit-xml=./notebook-tests/package-notebooks.xml - set package_notebooks_exit_code=%ERRORLEVEL% rem Fail step if either of these do not have exit code 0 if %notebooks_exit_code% NEQ 0 exit /b 1 if %package_notebooks_exit_code% NEQ 0 exit /b 1 diff --git a/MANIFEST.in b/MANIFEST.in index 2ac7b306..51ae4ddf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ include src/primaite/setup/_package_data/primaite_config.yaml include src/primaite/config/_package_data/*.yaml -include src/primaite/simulator/_package_data/*.ipynb From f5cfaeedab633498274bbc05e34679dea19fec7a Mon Sep 17 00:00:00 2001 From: Archer Bowen Date: Fri, 21 Feb 2025 14:57:48 +0000 Subject: [PATCH 215/224] #3075 Cleaned up yaml snippets that were rendering as a strings. It seems that adding '...' to yaml snippets seems to end up causing the snippets to render as strings rather than actual yaml. --- ...ommand-and-Control-E2E-Demonstration.ipynb | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 6dc8e077..f187c8d5 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -120,6 +120,9 @@ " options:\n", " node_name: web_server\n", " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: tcp\n", + " masquerade_port: dns\n", " 9:\n", " action: configure-c2-beacon\n", " options:\n", @@ -235,23 +238,18 @@ "The yaml snippet below shows all the relevant agent options for this action:\n", "\n", "```yaml\n", + "\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " - node_name: web_server\n", - " ...\n", - " ...\n", " action_map:\n", - " ...\n", " 2:\n", " action: configure-c2-beacon\n", " options:\n", - " node_id: 0 # Node Index\n", - " config: # Further information about these config options can be found at the bottom of this notebook.\n", - " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency:\n", - " masquerade_protocol:\n", - " masquerade_port:\n", + " node_name: web_server\n", + " c2_server_ip_address: 192.168.10.21 # Further information about these config options can be found at the bottom of this notebook.\n", + " keep_alive_frequency:\n", + " masquerade_protocol:\n", + " masquerade_port:\n", + "\n", "```" ] }, @@ -279,20 +277,12 @@ "\n", "```yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " - node_name: web_server\n", - " applications: \n", - " - application_name: c2-beacon\n", - " ...\n", - " ...\n", " action_map:\n", - " ...\n", " 3:\n", " action: node-application-execute\n", " options:\n", - " node_id: 0\n", - " application_id: 0\n", + " node_name: web_server\n", + " application_name: c2-beacon\n", "```" ] }, @@ -346,7 +336,6 @@ "``` yaml\n", " action_space:\n", " action_map:\n", - " ...\n", " 4:\n", " action: c2-server-terminal-command\n", " options:\n", @@ -396,14 +385,12 @@ "``` yaml\n", " action_space:\n", " action_map:\n", - " ...\n", " 5:\n", " action: c2-server-ransomware-configure\n", " options:\n", - " node_id: 1\n", - " config:\n", - " server_ip_address: 192.168.1.14\n", - " payload: ENCRYPT\n", + " node_name: client_1\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", "```\n" ] }, @@ -442,7 +429,6 @@ "``` yaml\n", " action_space:\n", " action_map:\n", - " ...\n", " 6:\n", " action: c2-server-data-exfiltrate\n", " options:\n", @@ -451,7 +437,7 @@ " target_folder_name: \"database\"\n", " exfiltration_folder_name: \"spoils\"\n", " target_ip_address: \"192.168.1.14\"\n", - " username: \"admin\",\n", + " username: \"admin\"\n", " password: \"admin\"\n", "\n", "```" @@ -500,7 +486,6 @@ "\n", "``` yaml\n", " action_space:\n", - " ...\n", " action_map:\n", " 7:\n", " action: c2-server-ransomware-launch\n", @@ -1313,13 +1298,18 @@ "source": [ "As demonstrated earlier, red agents can use the ``configure-c2-beacon`` action to configure these settings mid episode through the configuration options:\n", "\n", - "``` YAML\n", - "...\n", - " action: configure-c2-beacon\n", - " options:\n", - " node_name: web_server\n", - " config:\n", + "```YAML\n", + "\n", + " action_space:\n", + " action_map:\n", + " 8:\n", + " action: configure-c2-beacon\n", + " options:\n", + " node_name: web_server\n", " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: tcp\n", + " masquerade_port: dns\n", "```" ] }, @@ -1684,7 +1674,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, From 892cdb82b91d621b92328dc0b2014348a08aac94 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Tue, 25 Feb 2025 14:25:27 +0000 Subject: [PATCH 216/224] #3075: Code review changes. --- .azure/azure-ci-build-pipeline.yaml | 7 +++---- .../Data-Manipulation-Customising-Red-Agent.ipynb | 4 ++-- tests/assets/configs/basic_switched_network.yaml | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 20c5b239..b6f24777 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -113,8 +113,8 @@ stages: - script: | pytest --nbmake -n=auto src/primaite/notebooks --junit-xml=./notebook-tests/notebooks.xml notebooks_exit_code=$? - # Fail step if either of these do not have exit code 0 - if [ $notebooks_exit_code -ne 0 ] || [ $package_notebooks_exit_code -ne 0 ]; then + # Fail step if exit code not equal to 0 + if [ $notebooks_exit_code -ne 0 ]; then exit 1 fi displayName: 'Run notebooks on Linux and macOS' @@ -124,9 +124,8 @@ stages: - script: | pytest --nbmake -n=auto src/primaite/notebooks --junit-xml=./notebook-tests/notebooks.xml set notebooks_exit_code=%ERRORLEVEL% - rem Fail step if either of these do not have exit code 0 + rem Fail step if exit code not equal to 0 if %notebooks_exit_code% NEQ 0 exit /b 1 - if %package_notebooks_exit_code% NEQ 0 exit /b 1 displayName: 'Run notebooks on Windows' condition: eq(variables['Agent.OS'], 'Windows_NT') diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 1eb814e3..5982407b 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -289,10 +289,10 @@ " action_space:\n", " action_map:\n", " 0:\n", - " action: do_nothing\n", + " action: do-nothing\n", " options: {}\n", " 1:\n", - " action: node_application_execute\n", + " action: node-application-execute\n", " options:\n", " node_name: client_1\n", " application_name: DataManipulationBot\n", diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 2af48ea2..7339c34c 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -37,10 +37,10 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} 1: - action: node_application_execute + action: node-application-execute options: node_name: client_2 application_name: web-browser From 1605b0989962a9c00c44cd63dc470a17b432e35e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 10:49:01 +0000 Subject: [PATCH 217/224] Apply suggestions from code review --- tests/assets/configs/basic_switched_network.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 7339c34c..b57ed3e0 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -112,7 +112,7 @@ agents: action_space: action_map: 0: - action: do_nothing + action: do-nothing options: {} reward_function: From b4b0f99c23baaefc4cf9750cad1f28c8edba23c8 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 17:57:23 +0000 Subject: [PATCH 218/224] Fix mismerge of agent show_history method --- src/primaite/game/agent/interface.py | 57 +++++----------------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 1fef14ef..a6e9739f 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -107,66 +107,27 @@ class AbstractAgent(BaseModel, ABC): self.reward_function = RewardFunction(config=self.config.reward_function) return super().model_post_init(__context) - def add_agent_action(self, item: AgentHistoryItem, table: PrettyTable) -> PrettyTable: - """Update the given table with information from given AgentHistoryItem.""" - node, application = "unknown", "unknown" - if (node_id := item.parameters.get("node_id")) is not None: - node = self.action_manager.node_names[node_id] - if (application_id := item.parameters.get("application_id")) is not None: - application = self.action_manager.application_names[node_id][application_id] - if (application_name := item.parameters.get("application_name")) is not None: - application = application_name - table.add_row([item.timestep, item.action, node, application, item.response.status]) - return table - def show_history(self, ignored_actions: Optional[list] = None): """ - Print an agent action provided it's not the DONOTHING action. + Print an agent action provided it's not the do-nothing action. :param ignored_actions: OPTIONAL: List of actions to be ignored when displaying the history. - If not provided, defaults to ignore DONOTHING actions. + If not provided, defaults to ignore do-nothing actions. """ if not ignored_actions: - ignored_actions = ["DONOTHING"] + ignored_actions = ["do-nothing"] table = PrettyTable() - table.field_names = ["Step", "Action", "Node", "Application", "Response"] - print(f"Actions for '{self.agent_name}':") + table.field_names = ["Step", "Action", "Params", "Response", "Response Data"] + print(f"Actions for '{self.config.ref}':") for item in self.history: if item.action in ignored_actions: pass else: - table = self.add_agent_action(item=item, table=table) - print(table) + # format dict by putting each key-value entry on a separate line and putting a blank line on the end. + param_string = "\n".join([*[f"{k}: {v:.30}" for k, v in item.parameters.items()], ""]) + data_string = "\n".join([*[f"{k}: {v:.30}" for k, v in item.response.data], ""]) - def add_agent_action(self, item: AgentHistoryItem, table: PrettyTable) -> PrettyTable: - """Update the given table with information from given AgentHistoryItem.""" - node, application = "unknown", "unknown" - if (node_id := item.parameters.get("node_id")) is not None: - node = self.action_manager.node_names[node_id] - if (application_id := item.parameters.get("application_id")) is not None: - application = self.action_manager.application_names[node_id][application_id] - if (application_name := item.parameters.get("application_name")) is not None: - application = application_name - table.add_row([item.timestep, item.action, node, application, item.response.status]) - return table - - def show_history(self, ignored_actions: Optional[list] = None): - """ - Print an agent action provided it's not the DONOTHING action. - - :param ignored_actions: OPTIONAL: List of actions to be ignored when displaying the history. - If not provided, defaults to ignore DONOTHING actions. - """ - if not ignored_actions: - ignored_actions = ["DONOTHING"] - table = PrettyTable() - table.field_names = ["Step", "Action", "Node", "Application", "Response"] - print(f"Actions for '{self.agent_name}':") - for item in self.history: - if item.action in ignored_actions: - pass - else: - table = self.add_agent_action(item=item, table=table) + table.add_row([item.timestep, item.action, param_string, item.response.status, data_string]) print(table) def update_observation(self, state: Dict) -> ObsType: From 8c399c4f61052c7bd55e0d330ee0e3542e61a0d5 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 18:11:42 +0000 Subject: [PATCH 219/224] Fix mismerge of c2 e2e notebook --- ...ommand-and-Control-E2E-Demonstration.ipynb | 157 +++++------------- 1 file changed, 39 insertions(+), 118 deletions(-) diff --git a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb index 882c3429..f187c8d5 100644 --- a/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-and-Control-E2E-Demonstration.ipynb @@ -59,7 +59,7 @@ "custom_c2_agent = \"\"\"\n", " - ref: CustomC2Agent\n", " team: RED\n", - " type: ProxyAgent\n", + " type: proxy-agent\n", "\n", " action_space:\n", " action_map:\n", @@ -74,12 +74,8 @@ " 2:\n", " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency:\n", - " masquerade_protocol:\n", - " masquerade_port:\n", + " node_name: web_server\n", + " c2_server_ip_address: 192.168.10.21\n", " 3:\n", " action: node-application-execute\n", " options:\n", @@ -101,10 +97,9 @@ " 5:\n", " action: c2-server-ransomware-configure\n", " options:\n", - " node_id: 1\n", - " config:\n", - " server_ip_address: 192.168.1.14\n", - " payload: ENCRYPT\n", + " node_name: client_1\n", + " server_ip_address: 192.168.1.14\n", + " payload: ENCRYPT\n", " 6:\n", " action: c2-server-data-exfiltrate\n", " options:\n", @@ -123,25 +118,20 @@ " 8:\n", " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.21\n", - " keep_alive_frequency: 10\n", - " masquerade_protocol: TCP\n", - " masquerade_port: DNS\n", + " node_name: web_server\n", + " c2_server_ip_address: 192.168.10.21\n", + " keep_alive_frequency: 10\n", + " masquerade_protocol: tcp\n", + " masquerade_port: dns\n", " 9:\n", " action: configure-c2-beacon\n", " options:\n", - " node_id: 0\n", - " config:\n", - " c2_server_ip_address: 192.168.10.22\n", - " keep_alive_frequency:\n", - " masquerade_protocol:\n", - " masquerade_port:\n", + " node_name: web_server\n", + " c2_server_ip_address: 192.168.10.22\n", "\n", " reward_function:\n", " reward_components:\n", - " - type: DUMMY\n", + " - type: dummy\n", "\"\"\"\n", "c2_agent_yaml = yaml.safe_load(custom_c2_agent)" ] @@ -287,13 +277,6 @@ "\n", "```yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " - node_name: web_server\n", - " applications: \n", - " - application_name: C2Beacon\n", - " ...\n", - " ...\n", " action_map:\n", " 3:\n", " action: node-application-execute\n", @@ -352,13 +335,6 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", " 4:\n", " action: c2-server-terminal-command\n", @@ -408,13 +384,6 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", " 5:\n", " action: c2-server-ransomware-configure\n", @@ -459,13 +428,6 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", " 6:\n", " action: c2-server-data-exfiltrate\n", @@ -524,13 +486,6 @@ "\n", "``` yaml\n", " action_space:\n", - " options:\n", - " nodes: # Node List\n", - " ...\n", - " - node_name: client_1\n", - " applications: \n", - " - application_name: C2Server\n", - " ...\n", " action_map:\n", " 7:\n", " action: c2-server-ransomware-launch\n", @@ -584,8 +539,8 @@ " type: custom\n", " options:\n", " components:\n", - " - type: NODES\n", - " label: NODES\n", + " - type: nodes\n", + " label: nodes\n", " options:\n", " hosts:\n", " - hostname: web_server\n", @@ -667,55 +622,29 @@ " 1:\n", " action: node-application-remove\n", " options:\n", - " node_id: 0\n", - " application_name: C2Beacon\n", + " node_name: web_server\n", + " application_name: c2-beacon\n", " 2:\n", " action: node-shutdown\n", " options:\n", - " node_id: 0\n", + " node_name: web_server\n", " 3:\n", " action: router-acl-add-rule\n", " options:\n", " target_router: router_1\n", " position: 1\n", - " permission: 2\n", - " source_ip_id: 2\n", - " dest_ip_id: 3\n", - " source_port_id: 2\n", - " dest_port_id: 2\n", - " protocol_id: 1\n", - " source_wildcard_id: 0\n", - " dest_wildcard_id: 0\n", + " permission: DENY\n", + " src_ip: 192.168.10.21\n", + " dst_ip: 192.168.1.12\n", + " src_port: HTTP\n", + " dst_port: HTTP\n", + " protocol_name: ALL\n", + " src_wildcard: 0.0.0.1\n", + " dst_wildcard: 0.0.0.1\n", "\n", - "\n", - " options:\n", - " nodes:\n", - " - node_name: web_server\n", - " applications:\n", - " - application_name: C2Beacon\n", - "\n", - " - node_name: database_server\n", - " folders:\n", - " - folder_name: database\n", - " files:\n", - " - file_name: database.db\n", - " services:\n", - " - service_name: DatabaseService\n", - " - node_name: router_1\n", - "\n", - " max_folders_per_node: 2\n", - " max_files_per_folder: 2\n", - " max_services_per_node: 2\n", - " max_nics_per_node: 8\n", - " max_acl_rules: 10\n", - " ip_list:\n", - " - 192.168.10.21\n", - " - 192.168.1.12\n", - " wildcard_list:\n", - " - 0.0.0.1\n", " reward_function:\n", " reward_components:\n", - " - type: DUMMY\n", + " - type: dummy\n", "\n", " agent_settings:\n", " flatten_obs: False\n", @@ -1112,7 +1041,7 @@ "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", - "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"ransomware-script\"]],\n", + "ransomware_install_command = {\"commands\":[[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"]],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", @@ -1200,7 +1129,7 @@ "outputs": [], "source": [ "# Attempting to install the C2 RansomwareScript\n", - "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"ransomware-script\"],\n", + "ransomware_install_command = {\"commands\":[\"software_manager\", \"application\", \"install\", \"RansomwareScript\"],\n", " \"username\": \"admin\",\n", " \"password\": \"admin\"}\n", "\n", @@ -1325,7 +1254,7 @@ "metadata": {}, "outputs": [], "source": [ - "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database-server\")\n", + "database_server: Server = blue_env.game.simulation.network.get_node_by_hostname(\"database_server\")\n", "database_server.software_manager.file_system.show(full=True)" ] }, @@ -1369,12 +1298,14 @@ "source": [ "As demonstrated earlier, red agents can use the ``configure-c2-beacon`` action to configure these settings mid episode through the configuration options:\n", "\n", - "``` YAML\n", - "...\n", - " action: configure_c2_beacon\n", - " options:\n", - " node_id: 0\n", - " config:\n", + "```YAML\n", + "\n", + " action_space:\n", + " action_map:\n", + " 8:\n", + " action: configure-c2-beacon\n", + " options:\n", + " node_name: web_server\n", " c2_server_ip_address: 192.168.10.21\n", " keep_alive_frequency: 10\n", " masquerade_protocol: tcp\n", @@ -1739,16 +1670,6 @@ "\n", "display_obs_diffs(tcp_c2_obs, udp_c2_obs, blue_config_env.game.step_counter)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "env.game.agents[\"CustomC2Agent\"].show_history()" - ] } ], "metadata": { From cf33dcdcf9e9948bcd9c22331e304fafff15088e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 18:12:20 +0000 Subject: [PATCH 220/224] remove outdated information from agents doc page --- docs/source/configuration/agents.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index ee84aede..c2674e31 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -20,12 +20,6 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo - ref: green_agent_example team: GREEN type: probabilistic-agent - observation_space: - type: UC2GreenObservation # TODO: what - action_space: - reward_function: - reward_components: - - type: dummy agent_settings: start_settings: From f1a36cafaac887573f8b7d9be6d552d0383a4951 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 18:13:45 +0000 Subject: [PATCH 221/224] remove outdated information from data manipulation bot doc page --- .../applications/data_manipulation_bot.rst | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 3ddb8bca..04c581bd 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -97,26 +97,6 @@ If not using the data manipulation bot manually, it needs to be used with a data team: RED type: red-database-corrupting-agent - observation_space: - type: uc2-red-observation #TODO what - options: - nodes: - - node_name: client_1 - observations: - - logon_status - - operating_status - applications: - - application_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} - - action_space: - reward_function: - reward_components: - - type: dummy - agent_settings: start_settings: start_step: 25 From fd367d1f0eff88095612cfefcdf7a20f559b395d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 18:21:28 +0000 Subject: [PATCH 222/224] Fix typos and duplicate identifiers in docs --- docs/source/request_system.rst | 2 -- .../system/applications/database_client.rst | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index 30ced50a..93fc2a9f 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -4,8 +4,6 @@ .. _request_system: -.. _request_system: - Request System ************** diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 472b504c..465827d9 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -59,7 +59,7 @@ Python # install DatabaseClient client.software_manager.install(DatabaseClient) - database_client: DatabaseClient = client.software_manager.software.get("database-sclient") + database_client: DatabaseClient = client.software_manager.software.get("database-client") # Configure the DatabaseClient database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) # address of the DatabaseService From bab40603788f2f06fa6945527238217b79e54743 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 26 Feb 2025 18:26:54 +0000 Subject: [PATCH 223/224] Fix agent config in terminal processing notebook --- src/primaite/notebooks/Terminal-Processing.ipynb | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/primaite/notebooks/Terminal-Processing.ipynb b/src/primaite/notebooks/Terminal-Processing.ipynb index 07d38791..755b0184 100644 --- a/src/primaite/notebooks/Terminal-Processing.ipynb +++ b/src/primaite/notebooks/Terminal-Processing.ipynb @@ -298,21 +298,7 @@ " - ref: CustomC2Agent\n", " team: RED\n", " type: proxy-agent\n", - " observation_space: null\n", " action_space:\n", - " options:\n", - " nodes:\n", - " - node_name: client_1\n", - " max_folders_per_node: 1\n", - " max_files_per_folder: 1\n", - " max_services_per_node: 2\n", - " max_nics_per_node: 8\n", - " max_acl_rules: 10\n", - " ip_list:\n", - " - 192.168.1.21\n", - " - 192.168.1.14\n", - " wildcard_list:\n", - " - 0.0.0.1\n", " action_map:\n", " 0:\n", " action: do-nothing\n", @@ -508,7 +494,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "venv", "language": "python", "name": "python3" }, From 2b04695c2e8e8d0ca2af074c6eb45f41ee78d63f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 27 Feb 2025 10:07:17 +0000 Subject: [PATCH 224/224] Apply suggestions from code review --- src/primaite/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/VERSION b/src/primaite/VERSION index 21a8fb4d..d9b058f1 100644 --- a/src/primaite/VERSION +++ b/src/primaite/VERSION @@ -1 +1 @@ -4.0.0a1-dev +4.0.0-dev