From 861cfe2c0a2e6955c0f3e1d62f6963c160998d9e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 11 Oct 2024 15:00:26 +0100 Subject: [PATCH 01/95] #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 02/95] #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 a90aec2bcd133ffbc2f9f63d028a54e01f382bae Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 17 Oct 2024 16:59:44 +0100 Subject: [PATCH 03/95] #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 04/95] #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 05/95] #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 06/95] #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 11357f87caacfe8531b5b4b0e707456a009e7dfc Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Mon, 21 Oct 2024 17:51:55 +0100 Subject: [PATCH 07/95] #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 518b934e09086d7dcc1c9ebc7e0b763dbbf85b5e Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 22 Oct 2024 17:02:54 +0100 Subject: [PATCH 08/95] #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 09/95] #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 844a3a60fa4f26d95baa0585d8112a566fde0208 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Wed, 30 Oct 2024 18:34:05 +0000 Subject: [PATCH 10/95] #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 d757bd01f0f634965e5999582412801edb177f37 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Tue, 12 Nov 2024 14:49:44 +0000 Subject: [PATCH 11/95] #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 12/95] #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 13/95] #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 14/95] #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 15/95] #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 16/95] #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 17/95] #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 18/95] #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 19/95] #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 20/95] #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 21/95] #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 22/95] #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 23/95] #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 24/95] #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 25/95] #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 26/95] #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 27/95] #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 28/95] #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 29/95] #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 30/95] #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 31/95] #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 32/95] #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 33/95] #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 34/95] #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 35/95] #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 36/95] '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 37/95] #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 38/95] #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 39/95] #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 40/95] #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 41/95] #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 42/95] #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 43/95] #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 44/95] #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 45/95] #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 46/95] 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 47/95] #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 48/95] #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 49/95] #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 50/95] #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 51/95] #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 2108b914e332f1c9d1628a481dcead6a43336032 Mon Sep 17 00:00:00 2001 From: Charlie Crane Date: Thu, 2 Jan 2025 17:41:24 +0000 Subject: [PATCH 52/95] #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 c481847b01266e1cd93aa02ce805c3ff95bbd169 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 3 Jan 2025 13:39:58 +0000 Subject: [PATCH 53/95] #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 54/95] #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 55/95] #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 56/95] 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 57/95] #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 58/95] #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 59/95] #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 60/95] #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 61/95] #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 62/95] #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 63/95] #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 64/95] #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 65/95] #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 66/95] #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 67/95] #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 68/95] #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 69/95] #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 70/95] #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 71/95] #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 72/95] #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 73/95] #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 74/95] #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 75/95] #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 76/95] #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 77/95] #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 78/95] 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 79/95] #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 80/95] #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 81/95] #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 f8fb052dadfa3f6a4d43376c281e91f587b8c30c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 15 Jan 2025 16:44:17 +0000 Subject: [PATCH 82/95] #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 83/95] #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 84/95] 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 85/95] #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 86/95] #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 87/95] 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 88/95] 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 89/95] 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 90/95] 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 91/95] 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 92/95] 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 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 b3pV`v|N86&mg_Y%fWrqZjPw3*yz+kl6K4t4 delta 55859 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 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 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 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 93/95] 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 94/95] 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 95/95] 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(