From ff17062e1cc0bfcfee645d553a5745e688cfaf3c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 8 Feb 2024 09:19:18 +0000 Subject: [PATCH 001/128] Vary start node of red agent. --- .../config/_package_data/example_config.yaml | 12 +- .../example_config_2_rl_agents.yaml | 2 +- .../game/agent/data_manipulation_bot.py | 16 +- src/primaite/game/game.py | 12 +- src/primaite/notebooks/uc2_demo.ipynb | 739 +++++++++++++++++- .../assets/configs/bad_primaite_session.yaml | 2 +- .../configs/eval_only_primaite_session.yaml | 2 +- tests/assets/configs/multi_agent_session.yaml | 2 +- .../assets/configs/test_primaite_session.yaml | 2 +- .../configs/train_only_primaite_session.yaml | 2 +- 10 files changed, 745 insertions(+), 46 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 700a0c18..7290339e 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -85,7 +85,7 @@ agents: - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent @@ -106,6 +106,9 @@ agents: - 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 @@ -730,6 +733,13 @@ simulation: type: WebBrowser options: target_url: http://arcd.com/users/ + - ref: data_manipulation_bot + type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 services: - ref: client_2_dns_client type: DNSClient diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 6aa54487..993b3283 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -54,7 +54,7 @@ agents: frequency: 4 variance: 3 - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/data_manipulation_bot.py index 58b790ec..126c55ec 100644 --- a/src/primaite/game/agent/data_manipulation_bot.py +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -1,21 +1,20 @@ import random -from typing import Dict, List, Tuple +from typing import Dict, Tuple from gymnasium.core import ObsType from primaite.game.agent.interface import AbstractScriptedAgent -from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot class DataManipulationAgent(AbstractScriptedAgent): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" - data_manipulation_bots: List["DataManipulationBot"] = [] next_execution_timestep: int = 0 + starting_node_idx: int = 0 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) + self.reset_agent_for_episode() def _set_next_execution_timestep(self, timestep: int) -> None: """Set the next execution timestep with a configured random variance. @@ -44,9 +43,16 @@ class DataManipulationAgent(AbstractScriptedAgent): self._set_next_execution_timestep(current_timestep + self.agent_settings.start_settings.frequency) - return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} + return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} def reset_agent_for_episode(self) -> None: """Set the next execution timestep when the episode resets.""" super().reset_agent_for_episode() + self._select_start_node() self._set_next_execution_timestep(self.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) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index a2c4e86d..1fd0dc8b 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,6 +1,6 @@ """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address -from typing import Dict, List +from typing import Dict, List, Tuple from pydantic import BaseModel, ConfigDict @@ -131,8 +131,14 @@ class PrimaiteGame: agent.update_reward(state) agent.reward_function.total_reward += agent.reward_function.current_reward - def apply_agent_actions(self) -> None: - """Apply all actions to simulation as requests.""" + def apply_agent_actions(self) -> Dict[str, Tuple[str, Dict]]: + """ + Apply all actions to simulation as requests. + + :return: A recap of each agent's actions, in CAOS format. + :rtype: Dict[str, Tuple[str, Dict]] + + """ agent_actions = {} for agent in self.agents: obs = agent.observation_manager.current_observation diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index 7454b6c4..b37e69fc 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -55,7 +55,7 @@ "source": [ "## Red agent\n", "\n", - "The red agent waits a bit then sends a DELETE query to the database from client 1. If the delete is successful, the database file is flagged as compromised to signal that data is not available.\n", + "At the start of every episode, the red agent randomly chooses either client 1 or client 2 to login to. It waits a bit then sends a DELETE query to the database from its chosen client. If the delete is successful, the database file is flagged as compromised to signal that data is not available.\n", "\n", "[](_package_data/uc2_attack.png)\n", "\n", @@ -68,7 +68,7 @@ "source": [ "## Blue agent\n", "\n", - "The blue agent can view the entire network, but the health statuses of components are not updated until a scan is performed. The blue agent should restore the database file from backup after it was compromised. It can also prevent further attacks by blocking client 1 from sending the malicious SQL query to the database server. This can be done by implementing an ACL rule on the router." + "The blue agent can view the entire network, but the health statuses of components are not updated until a scan is performed. The blue agent should restore the database file from backup after it was compromised. It can also prevent further attacks by blocking the red agent client from sending the malicious SQL query to the database server. This can be done by implementing an ACL rule on the router." ] }, { @@ -84,7 +84,7 @@ "source": [ "## Scripted agents:\n", "### Red\n", - "The red agent sits on client 1 and uses an application called DataManipulationBot whose sole purpose is to send a DELETE query to the database.\n", + "The red agent sits on a client and uses an application called DataManipulationBot whose sole purpose is to send a DELETE query to the database.\n", "The red agent can choose one of two action each timestep:\n", "1. do nothing\n", "2. execute the data manipulation application\n", @@ -92,6 +92,7 @@ "- start time\n", "- frequency\n", "- variance\n", + "\n", "Attacks start at a random timestep between (start_time - variance) and (start_time + variance). After each attack, another is attempted after a random delay between (frequency - variance) and (frequency + variance) timesteps.\n", "\n", "The data manipulation app itself has an element of randomness because the attack has a probability of success. The default is 0.8 to succeed with the port scan step and 0.8 to succeed with the attack itself.\n", @@ -290,10 +291,16 @@ "- `9`: Scan the database file - this refreshes the health status of the database file\n", "- `13`: Patch the database service - This triggers the database to restore data from the backup server\n", "- `19`: Shut down client 1\n", + "- `20`: Start up client 1\n", "- `22`: Block outgoing traffic from client 1\n", + "- `23`: Block outgoing traffic from client 2\n", "- `26`: Block TCP traffic from client 1 to the database node\n", + "- `27`: Block TCP traffic from client 2 to the database node\n", "- `28-37`: Remove ACL rules 1-10\n", "- `42`: Disconnect client 1 from the network\n", + "- `43`: Reconnect client 1 to the network\n", + "- `44`: Disconnect client 2 from the network\n", + "- `45`: Reconnect client 2 to the network\n", "\n", "The other actions will either have no effect or will negatively impact the network, so the blue agent should avoid taking them." ] @@ -326,7 +333,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -336,9 +343,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/cade/repos/PrimAITE/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n", + "2024-02-07 10:58:13,192\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2024-02-07 10:58:17,136\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n" + ] + } + ], "source": [ "# Imports\n", "from primaite.config.load import example_config_path\n", @@ -359,16 +377,143 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resetting environment, episode 0, avg. reward: 0.0\n", + "env created successfully\n", + "{'ACL': {1: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 2: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 3: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 4: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 5: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 6: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 7: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 8: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 9: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 10: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0}},\n", + " 'ICS': 0,\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 0}},\n", + " 2: {'PROTOCOLS': {'ALL': 0}},\n", + " 3: {'PROTOCOLS': {'ALL': 0}},\n", + " 4: {'PROTOCOLS': {'ALL': 0}},\n", + " 5: {'PROTOCOLS': {'ALL': 0}},\n", + " 6: {'PROTOCOLS': {'ALL': 0}},\n", + " 7: {'PROTOCOLS': {'ALL': 0}},\n", + " 8: {'PROTOCOLS': {'ALL': 0}},\n", + " 9: {'PROTOCOLS': {'ALL': 0}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", + " 'health_status': 1}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}}\n" + ] + } + ], "source": [ "# create the env\n", "with open(example_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n", " # set success probability to 1.0 to avoid rerunning cells.\n", " cfg['simulation']['network']['nodes'][8]['applications'][0]['options']['data_manipulation_p_of_success'] = 1.0\n", + " cfg['simulation']['network']['nodes'][9]['applications'][0]['options']['data_manipulation_p_of_success'] = 1.0\n", " cfg['simulation']['network']['nodes'][8]['applications'][0]['options']['port_scan_p_of_success'] = 1.0\n", + " cfg['simulation']['network']['nodes'][9]['applications'][0]['options']['port_scan_p_of_success'] = 1.0\n", "game = PrimaiteGame.from_config(cfg)\n", "env = PrimaiteGymEnv(game = game)\n", "# Don't flatten obs as we are not training an agent and we wish to see the dict-formatted observations\n", @@ -382,18 +527,78 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The red agent will start attacking at some point between step 20 and 30. When this happens, the reward will go from 1.0 to 0.0, and to -1.0 when the green agent tries to access the webpage." + "The red agent will start attacking at some point between step 20 and 30. When this happens, the reward will drop immediately, then drop to -1.0 when green agents try to access the webpage." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "for step in range(32):\n", + "def friendly_output_red_action(info):\n", + " # parse the info dict form step output and write out what the red agent is doing\n", + " red_info = info['agent_actions']['data_manipulation_attacker']\n", + " red_action = red_info[0]\n", + " if red_action == 'DONOTHING':\n", + " red_str = 'DO NOTHING'\n", + " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", + " client = \"client 1\" if red_info[1]['node_id'] == 0 else \"client 2\"\n", + " red_str = f\"ATTACK from {client}\"\n", + " return red_str" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 1, Red action: DO NOTHING, Blue reward:0.34\n", + "step: 2, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 3, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 4, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 5, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 6, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 7, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 8, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 9, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 10, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 11, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 12, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 13, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 14, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 15, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 16, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 17, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 18, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 19, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 20, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 21, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 22, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 23, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 24, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 25, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 26, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 27, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 28, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 29, Red action: DO NOTHING, Blue reward:1.0\n", + "step: 30, Red action: ATTACK from client 1, Blue reward:0.32\n", + "step: 31, Red action: DO NOTHING, Blue reward:0.32\n", + "step: 32, Red action: DO NOTHING, Blue reward:0.32\n", + "step: 33, Red action: DO NOTHING, Blue reward:-1.0\n", + "step: 34, Red action: DO NOTHING, Blue reward:-1.0\n", + "step: 35, Red action: DO NOTHING, Blue reward:-1.0\n" + ] + } + ], + "source": [ + "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['client_1_data_manipulation_red_bot'][0]}, Blue reward:{reward}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {friendly_output_red_action(info)}, Blue reward:{reward}\" )" ] }, { @@ -405,9 +610,44 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}\n" + ] + } + ], "source": [ "pprint(obs['NODES'])" ] @@ -421,9 +661,44 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -447,13 +722,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 38\n", + "Red action: DONOTHING\n", + "Green action: NODE_APPLICATION_EXECUTE\n", + "Green action: NODE_APPLICATION_EXECUTE\n", + "Blue reward:-1.0\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", - "print(f\"Red action: {info['agent_actions']['client_1_data_manipulation_red_bot'][0]}\" )\n", + "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'][0]}\" )\n", "print(f\"Green action: {info['agent_actions']['client_1_green_user'][0]}\" )\n", "print(f\"Green action: {info['agent_actions']['client_2_green_user'][0]}\" )\n", "print(f\"Blue reward:{reward}\" )" @@ -465,20 +752,32 @@ "source": [ "The patching takes two steps, so the reward hasn't changed yet. Let's do nothing for another timestep, the reward should improve.\n", "\n", - "The reward will be 0 as soon as the file finishes restoring. Then, the reward will increase to 1 when the green agent makes a request. (Because the webapp access part of the reward does not update until a successful request is made.)\n", + "The reward will increase slightly as soon as the file finishes restoring. Then, the reward will increase to 1 when both green agents make successful requests.\n", "\n", "Run the following cell until the green action is `NODE_APPLICATION_EXECUTE`, then the reward should become 1. If you run it enough times, another red attack will happen and the reward will drop again." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 39\n", + "Red action: DONOTHING\n", + "Green action: NODE_APPLICATION_EXECUTE\n", + "Green action: DONOTHING\n", + "Blue reward:-0.32\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", - "print(f\"Red action: {info['agent_actions']['client_1_data_manipulation_red_bot'][0]}\" )\n", + "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'][0]}\" )\n", "print(f\"Green action: {info['agent_actions']['client_2_green_user'][0]}\" )\n", "print(f\"Green action: {info['agent_actions']['client_1_green_user'][0]}\" )\n", "print(f\"Blue reward:{reward}\" )" @@ -488,24 +787,69 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The blue agent can prevent attacks by implementing an ACL rule to stop client_1 from sending POSTGRES traffic to the database. (Let's also patch the database file to get the reward back up.)" + "The blue agent can prevent attacks by implementing an ACL rule to stop client_1 or client_2 from sending POSTGRES traffic to the database. (Let's also patch the database file to get the reward back up.)\n", + "\n", + "Let's block both clients from communicating directly with the database." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 139, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 140, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 141, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 142, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 143, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 144, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 145, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 146, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 147, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 148, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 149, Red action: NODE_APPLICATION_EXECUTE, Blue reward:-0.32\n", + "step: 150, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 151, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 152, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 153, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 154, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 155, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 156, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 157, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 158, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 159, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 160, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 161, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 162, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 163, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 164, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 165, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 166, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 167, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 168, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 169, Red action: NODE_APPLICATION_EXECUTE, Blue reward:-0.32\n", + "step: 170, Red action: DONOTHING, Blue reward:-0.32\n", + "step: 171, Red action: DONOTHING, Blue reward:-0.32\n" + ] + } + ], "source": [ "env.step(13) # Patch the database\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['client_1_data_manipulation_red_bot'][0]}, Blue reward:{reward}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", "\n", "env.step(26) # Block client 1\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['client_1_data_manipulation_red_bot'][0]}, Blue reward:{reward}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", + "\n", + "env.step(27) # Block client 2\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", "\n", "for step in range(30):\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['client_1_data_manipulation_red_bot'][0]}, Blue reward:{reward}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )" ] }, { @@ -519,7 +863,340 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's also have a look at the ACL observation to verify our new ACL rule at position 5." + "Let's also have a look at the ACL observation to verify our new ACL rule at positions 5 and 6." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 2,\n", + " 'source_node_id': 7,\n", + " 'source_port': 1,\n", + " 'dest_node_id': 4,\n", + " 'dest_port': 1,\n", + " 'protocol': 3},\n", + " 6: {'position': 5,\n", + " 'permission': 2,\n", + " 'source_node_id': 8,\n", + " 'source_port': 1,\n", + " 'dest_node_id': 4,\n", + " 'dest_port': 1,\n", + " 'protocol': 3},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0}}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "obs['ACL']" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "router = env.game.simulation.network.get_node_by_hostname('router_1')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+------------------------------------------------------------------------------------------------------------+\n", + "| router_1 Access Control List |\n", + "+-------+--------+----------+---------------+------------------------+--------------+------------------------+\n", + "| Index | Action | Protocol | Src IP | Src Port | Dst IP | Dst Port |\n", + "+-------+--------+----------+---------------+------------------------+--------------+------------------------+\n", + "| 5 | DENY | TCP | 192.168.10.21 | ANY | 192.168.1.14 | ANY |\n", + "| 6 | DENY | TCP | 192.168.10.22 | ANY | 192.168.1.14 | ANY |\n", + "| 18 | PERMIT | ANY | ANY | 5432 (POSTGRES_SERVER) | ANY | 5432 (POSTGRES_SERVER) |\n", + "| 19 | PERMIT | ANY | ANY | 53 (DNS) | ANY | 53 (DNS) |\n", + "| 20 | PERMIT | ANY | ANY | 21 (FTP) | ANY | 21 (FTP) |\n", + "| 21 | PERMIT | ANY | ANY | 80 (HTTP) | ANY | 80 (HTTP) |\n", + "| 22 | PERMIT | ANY | ANY | 219 (ARP) | ANY | 219 (ARP) |\n", + "| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY |\n", + "| 24 | DENY | ANY | ANY | ANY | ANY | ANY |\n", + "+-------+--------+----------+---------------+------------------------+--------------+------------------------+\n" + ] + } + ], + "source": [ + "router.acl.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(, 0.34),\n", + " (,\n", + " 0.33),\n", + " (,\n", + " 0.33)]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "env.agent.reward_function.reward_components" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "client_1 = env.game.simulation.network.get_node_by_hostname('client_1')\n", + "client_2 = env.game.simulation.network.get_node_by_hostname('client_2')" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "client_1_browser = client_1.software_manager.software.get(\"WebBrowser\")\n", + "client_2_browser = client_2.software_manager.software.get(\"WebBrowser\")" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", + " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=)]" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client_2_browser.history" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client_1_browser.get_webpage()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "database_server = env.game.simulation.network.get_node_by_hostname('database_server')" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={'e63e23dc-c443-4434-822d-3c2c01cbfe1e': File(uuid='e63e23dc-c443-4434-822d-3c2c01cbfe1e', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '646a198e-6ac1-4aea-b526-7ca5c2c30dfc': File(uuid='646a198e-6ac1-4aea-b526-7ca5c2c30dfc', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '23df7aba-074e-4ffa-939a-d87ad1fe7af1': File(uuid='23df7aba-074e-4ffa-939a-d87ad1fe7af1', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={'e63e23dc-c443-4434-822d-3c2c01cbfe1e': File(uuid='e63e23dc-c443-4434-822d-3c2c01cbfe1e', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '646a198e-6ac1-4aea-b526-7ca5c2c30dfc': File(uuid='646a198e-6ac1-4aea-b526-7ca5c2c30dfc', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '23df7aba-074e-4ffa-939a-d87ad1fe7af1': File(uuid='23df7aba-074e-4ffa-939a-d87ad1fe7af1', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "database_server.file_system.get_file('database', 'database.db')" ] }, { @@ -528,7 +1205,7 @@ "metadata": {}, "outputs": [], "source": [ - "obs['ACL']" + "database_server." ] } ], diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 552351b2..662d704f 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -46,7 +46,7 @@ agents: frequency: 20 variance: 5 - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index d49562c8..254f7974 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -51,7 +51,7 @@ agents: frequency: 20 variance: 5 - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 29f0ae7f..f01b1a63 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -57,7 +57,7 @@ agents: frequency: 20 variance: 5 - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 0c70840d..55e8a273 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -55,7 +55,7 @@ agents: frequency: 20 variance: 5 - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent diff --git a/tests/assets/configs/train_only_primaite_session.yaml b/tests/assets/configs/train_only_primaite_session.yaml index 0466a5ac..27236470 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -58,7 +58,7 @@ agents: frequency: 20 variance: 5 - - ref: client_1_data_manipulation_red_bot + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent From 2c743005cd4800f68a90462259393128483fc21c Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 12 Feb 2024 18:58:10 +0000 Subject: [PATCH 002/128] #2257: moved config tests into its own directory + added dmz_network.yaml to use in tests --- .../configs/basic_switched_network.yaml | 7 + tests/assets/configs/dmz_network.yaml | 230 ++++++++++++++++++ .../configuration_file_parsing/__init__.py | 0 .../router_game_configuration.py | 58 +++++ ...oftware_installation_and_configuration.py} | 0 5 files changed, 295 insertions(+) create mode 100644 tests/assets/configs/dmz_network.yaml create mode 100644 tests/integration_tests/configuration_file_parsing/__init__.py create mode 100644 tests/integration_tests/configuration_file_parsing/router_game_configuration.py rename tests/integration_tests/{game_configuration.py => configuration_file_parsing/software_installation_and_configuration.py} (100%) diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index d1cec079..a248065c 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -1,3 +1,10 @@ +# Basic Switched network +# +# -------------- -------------- -------------- +# | client_1 |------| switch_1 |------| client_2 | +# -------------- -------------- -------------- +# + training_config: rl_framework: SB3 rl_algorithm: PPO diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml new file mode 100644 index 00000000..ddf8fb36 --- /dev/null +++ b/tests/assets/configs/dmz_network.yaml @@ -0,0 +1,230 @@ +# Network with DMZ +# +# An example network configuration with an internal network, a DMZ network and a couple of external networks. +# +# ............................................................................ +# . . +# . Internal Network . +# . . +# . -------------- -------------- -------------- . +# . | client_1 |------| switch_1 |------| router_1 | . +# . -------------- -------------- -------------- . +# . (Computer) | . +# ......................................................|..................... +# | +# | +# ......................................................|..................... +# . | . +# . DMZ Network | . +# . | . +# . -------------- -------------- -------------- . +# . | client_2 |------| switch_2 |------| router_2 | . +# . -------------- -------------- -------------- . +# . (Computer) | . +# ......................................................|..................... +# | +# External Network | +# | +# | +# ----------------------- -------------- --------------------- +# | external_computer |------| switch_3 |------| external_server | +# ----------------------- -------------- --------------------- +# +training_config: + rl_framework: SB3 + rl_algorithm: PPO + seed: 333 + n_learn_episodes: 1 + n_eval_episodes: 5 + max_steps_per_episode: 128 + deterministic_eval: false + n_agents: 1 + agent_references: + - defender + +io_settings: + save_checkpoints: true + checkpoint_interval: 5 + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + + +game: + max_episode_length: 256 + ports: + - ARP + - DNS + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + +agents: + - ref: client_2_green_user + team: GREEN + type: GreenWebBrowsingAgent + 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 + + agent_settings: + start_settings: + start_step: 5 + frequency: 4 + variance: 3 + + +simulation: + network: + nodes: + - ref: client_1 + type: computer + hostname: client_1 + ip_address: 192.168.0.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.0.1 + dns_server: 192.168.20.10 + + - ref: switch_1 + type: switch + hostname: switch_1 + num_ports: 8 + + - ref: router_1 + type: router + hostname: router_1 + num_ports: 5 + ports: + 1: + ip_address: 192.168.0.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + + - ref: client_2 + type: computer + hostname: client_2 + ip_address: 192.168.10.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.20.10 + + - ref: switch_2 + type: switch + hostname: switch_2 + num_ports: 8 + + - ref: router_2 + type: router + hostname: router_2 + num_ports: 5 + ports: + 1: + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.11.1 + subnet_mask: 255.255.255.0 + 3: + ip_address: 192.168.20.1 + subnet_mask: 255.255.255.0 + acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + + - ref: switch_3 + type: switch + hostname: switch_3 + num_ports: 8 + + - ref: external_computer + type: computer + hostname: external_computer + ip_address: 192.168.20.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.20.1 + dns_server: 192.168.20.10 + + - ref: external_server + type: server + hostname: external_server + ip_address: 192.168.20.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.20.1 + services: + - ref: domain_controller_dns_server + type: DNSServer + links: + - ref: client_1___switch_1 + endpoint_a_ref: client_1 + endpoint_a_port: 1 + endpoint_b_ref: switch_1 + endpoint_b_port: 1 + - ref: router_1___switch_1 + endpoint_a_ref: router_1 + endpoint_a_port: 1 + endpoint_b_ref: switch_1 + endpoint_b_port: 8 + - ref: router_1___router_2 + endpoint_a_ref: router_1 + endpoint_a_port: 2 + endpoint_b_ref: router_2 + endpoint_b_port: 2 + - ref: router_2___switch_2 + endpoint_a_ref: router_2 + endpoint_a_port: 1 + endpoint_b_ref: switch_2 + endpoint_b_port: 8 + - ref: client_2___switch_2 + endpoint_a_ref: client_2 + endpoint_a_port: 1 + endpoint_b_ref: switch_2 + endpoint_b_port: 1 + - ref: router_2___switch_3 + endpoint_a_ref: router_2 + endpoint_a_port: 3 + endpoint_b_ref: switch_3 + endpoint_b_port: 8 + - ref: external_computer___switch_3 + endpoint_a_ref: external_computer + endpoint_a_port: 1 + endpoint_b_ref: switch_3 + endpoint_b_port: 1 + - ref: external_server___switch_3 + endpoint_a_ref: external_server + endpoint_a_port: 1 + endpoint_b_ref: switch_3 + endpoint_b_port: 2 diff --git a/tests/integration_tests/configuration_file_parsing/__init__.py b/tests/integration_tests/configuration_file_parsing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration_tests/configuration_file_parsing/router_game_configuration.py b/tests/integration_tests/configuration_file_parsing/router_game_configuration.py new file mode 100644 index 00000000..49b889d7 --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/router_game_configuration.py @@ -0,0 +1,58 @@ +from pathlib import Path +from typing import Union + +import yaml + +from primaite.game.game import PrimaiteGame +from primaite.simulator.network.container import Network +from tests import TEST_ASSETS_ROOT + +DMZ_NETWORK = TEST_ASSETS_ROOT / "configs/dmz_network.yaml" + + +def load_config(config_path: Union[str, Path]) -> PrimaiteGame: + """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" + with open(config_path, "r") as f: + cfg = yaml.safe_load(f) + + return PrimaiteGame.from_config(cfg) + + +def test_dmz_config(): + """Test that the DMZ network config can be parsed properly.""" + game = load_config(DMZ_NETWORK) + + network: Network = game.simulation.network + + assert len(network.nodes) == 9 # 9 nodes in network + assert len(network.routers) == 2 # 2 routers in network + assert len(network.switches) == 3 # 3 switches in network + assert len(network.servers) == 1 # 1 server in network + + +def test_router_routes_are_correctly_added(): + """Test that makes sure that router routes have been added from the configuration file.""" + pass + + +def test_firewall_node_added_to_network(): + """Test that the firewall has been correctly added to and configured in the network.""" + pass + + +def test_router_acl_rules_correctly_added(): + """Test that makes sure that the router ACLs have been configured onto the router node via configuration file.""" + pass + + +def test_firewall_routes_are_correctly_added(): + """Test that the firewall routes have been correctly added to and configured in the network.""" + pass + + +def test_firewall_acl_rules_correctly_added(): + """ + Test that makes sure that the firewall ACLs have been configured onto the firewall + node via configuration file. + """ + pass diff --git a/tests/integration_tests/game_configuration.py b/tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py similarity index 100% rename from tests/integration_tests/game_configuration.py rename to tests/integration_tests/configuration_file_parsing/software_installation_and_configuration.py From b277034e8b8d48afef97eccda9ccc213d9cdf0f8 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Tue, 13 Feb 2024 13:02:24 +0000 Subject: [PATCH 003/128] #2257: temporarily commit changes - added startup and shut down durations to node config + adding routes --- src/primaite/game/game.py | 14 ++- .../network/hardware/nodes/network/router.py | 8 ++ tests/assets/configs/dmz_network.yaml | 98 +++++++++++++------ .../router_game_configuration.py | 22 ++++- 4 files changed, 105 insertions(+), 37 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c03bca36..3bc3789a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -266,6 +266,10 @@ class PrimaiteGame: game.ref_map_services[service_ref] = new_service.uuid else: _LOGGER.warning(f"service type not found {service_type}") + + # start the service + new_service.start() + # service-dependent options if service_type == "DNSClient": if "options" in service_cfg: @@ -282,17 +286,14 @@ class PrimaiteGame: if "options" in service_cfg: opt = service_cfg["options"] new_service.configure_backup(backup_server=IPv4Address(opt.get("backup_server_ip"))) - new_service.start() if service_type == "FTPServer": if "options" in service_cfg: opt = service_cfg["options"] new_service.server_password = opt.get("server_password") - new_service.start() if service_type == "NTPClient": if "options" in service_cfg: opt = service_cfg["options"] new_service.ntp_server = IPv4Address(opt.get("ntp_server_ip")) - new_service.start() if "applications" in node_cfg: for application_cfg in node_cfg["applications"]: new_application = None @@ -306,6 +307,9 @@ class PrimaiteGame: else: _LOGGER.warning(f"application type not found {application_type}") + # run the application + new_application.run() + if application_type == "DataManipulationBot": if "options" in application_cfg: opt = application_cfg["options"] @@ -327,7 +331,6 @@ class PrimaiteGame: 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"] @@ -344,6 +347,9 @@ class PrimaiteGame: 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"])) + new_node.start_up_duration = int(node_cfg.get("start_up_duration", 3)) + new_node.shut_down_duration = int(node_cfg.get("shut_down_duration", 3)) + net.add_node(new_node) new_node.power_on() game.ref_map_nodes[node_ref] = new_node.uuid diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 40cbc16d..f034fcbd 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1276,4 +1276,12 @@ class Router(NetworkNode): if "acl" in cfg: new.acl._default_config = cfg["acl"] # save the config to allow resetting new.acl._reset_rules_to_default() # read the config and apply rules + if "routes" in cfg: + for route in cfg.get("routes"): + new.route_table.add_route( + address=IPv4Address(route.get("address")), + subnet_mask=IPv4Address(route.get("subnet_mask")), + next_hop_ip_address=IPv4Address(route.get("subnet_mask")), + metric=float(route.get("metric")), + ) return new diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index ddf8fb36..0c67ba7c 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -9,26 +9,26 @@ # . -------------- -------------- -------------- . # . | client_1 |------| switch_1 |------| router_1 | . # . -------------- -------------- -------------- . -# . (Computer) | . -# ......................................................|..................... -# | -# | -# ......................................................|..................... -# . | . -# . DMZ Network | . -# . | . -# . -------------- -------------- -------------- . -# . | client_2 |------| switch_2 |------| router_2 | . -# . -------------- -------------- -------------- . -# . (Computer) | . -# ......................................................|..................... -# | -# External Network | -# | -# | -# ----------------------- -------------- --------------------- -# | external_computer |------| switch_3 |------| external_server | -# ----------------------- -------------- --------------------- +# . (Computer) | . +# ........................................................|..................... +# | +# | +# ........................................................|..................... +# . | . +# . DMZ Network | . +# . | . +# . ---------------- -------------- -------------- . +# . | dmz_server |------| switch_2 |------| router_2 | . +# . ---------------- -------------- -------------- . +# . (Computer) | . +# ........................................................|................... +# | +# External Network | +# | +# | +# ----------------------- -------------- --------------------- +# | external_computer |------| switch_3 |------| external_server | +# ----------------------- -------------- --------------------- # training_config: rl_framework: SB3 @@ -63,7 +63,7 @@ game: - UDP agents: - - ref: client_2_green_user + - ref: client_1_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: @@ -74,7 +74,7 @@ agents: - type: NODE_APPLICATION_EXECUTE options: nodes: - - node_name: client_2 + - node_name: client_1 applications: - application_name: WebBrowser max_folders_per_node: 1 @@ -102,17 +102,23 @@ simulation: ip_address: 192.168.0.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.0.1 - dns_server: 192.168.20.10 + dns_server: 192.168.20.11 + start_up_duration: 0 + shut_down_duration: 0 - ref: switch_1 type: switch hostname: switch_1 num_ports: 8 + start_up_duration: 0 + shut_down_duration: 0 - ref: router_1 type: router hostname: router_1 num_ports: 5 + start_up_duration: 0 + shut_down_duration: 0 ports: 1: ip_address: 192.168.0.1 @@ -128,24 +134,43 @@ simulation: 23: action: PERMIT protocol: ICMP + routes: + - address: 192.168.10.10 + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.11.1 + metric: 0 + - address: 192.168.20.10 + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.11.1 + metric: 0 + - address: 192.168.20.11 + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.11.1 + metric: 0 - - ref: client_2 - type: computer - hostname: client_2 + - ref: dmz_server + type: server + hostname: dmz_server ip_address: 192.168.10.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 - dns_server: 192.168.20.10 + dns_server: 192.168.20.11 + start_up_duration: 0 + shut_down_duration: 0 - ref: switch_2 type: switch hostname: switch_2 num_ports: 8 + start_up_duration: 0 + shut_down_duration: 0 - ref: router_2 type: router hostname: router_2 num_ports: 5 + start_up_duration: 0 + shut_down_duration: 0 ports: 1: ip_address: 192.168.10.1 @@ -164,11 +189,18 @@ simulation: 23: action: PERMIT protocol: ICMP + routes: + - address: 192.168.0.10 + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.1.1 + metric: 0 - ref: switch_3 type: switch hostname: switch_3 num_ports: 8 + start_up_duration: 0 + shut_down_duration: 0 - ref: external_computer type: computer @@ -176,14 +208,18 @@ simulation: ip_address: 192.168.20.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.20.1 - dns_server: 192.168.20.10 + dns_server: 192.168.20.11 + start_up_duration: 0 + shut_down_duration: 0 - ref: external_server type: server hostname: external_server - ip_address: 192.168.20.10 + ip_address: 192.168.20.11 subnet_mask: 255.255.255.0 default_gateway: 192.168.20.1 + start_up_duration: 0 + shut_down_duration: 0 services: - ref: domain_controller_dns_server type: DNSServer @@ -208,8 +244,8 @@ simulation: endpoint_a_port: 1 endpoint_b_ref: switch_2 endpoint_b_port: 8 - - ref: client_2___switch_2 - endpoint_a_ref: client_2 + - ref: dmz_server___switch_2 + endpoint_a_ref: dmz_server endpoint_a_port: 1 endpoint_b_ref: switch_2 endpoint_b_port: 1 diff --git a/tests/integration_tests/configuration_file_parsing/router_game_configuration.py b/tests/integration_tests/configuration_file_parsing/router_game_configuration.py index 49b889d7..9d682dcc 100644 --- a/tests/integration_tests/configuration_file_parsing/router_game_configuration.py +++ b/tests/integration_tests/configuration_file_parsing/router_game_configuration.py @@ -5,6 +5,9 @@ import yaml from primaite.game.game import PrimaiteGame from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import Router from tests import TEST_ASSETS_ROOT DMZ_NETWORK = TEST_ASSETS_ROOT / "configs/dmz_network.yaml" @@ -27,12 +30,27 @@ def test_dmz_config(): assert len(network.nodes) == 9 # 9 nodes in network assert len(network.routers) == 2 # 2 routers in network assert len(network.switches) == 3 # 3 switches in network - assert len(network.servers) == 1 # 1 server in network + assert len(network.servers) == 2 # 2 servers in network def test_router_routes_are_correctly_added(): """Test that makes sure that router routes have been added from the configuration file.""" - pass + game = load_config(DMZ_NETWORK) + + network: Network = game.simulation.network + + router_1: Router = network.get_node_by_hostname("router_1") + client_1: Computer = network.get_node_by_hostname("client_1") + dmz_server: Server = network.get_node_by_hostname("dmz_server") + external_computer: Computer = network.get_node_by_hostname("external_computer") + external_server: Server = network.get_node_by_hostname("external_server") + + # test that client_1 has a route to the DMZ and external nodes - they are on a second router + + # there should be a route to the dmz server + assert router_1.route_table.find_best_route(dmz_server.network_interface[1].ip_address) + # ping DMZ server + # assert client_1.ping(dmz_server.network_interface[1].ip_address) def test_firewall_node_added_to_network(): From b7398233188ec12dcc11ed4a57c07f347e45d94f Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 15 Feb 2024 15:45:18 +0000 Subject: [PATCH 004/128] #2257: add firewall via config + fix router hop ip address + shuffling around tests --- src/primaite/game/game.py | 9 +- src/primaite/simulator/__init__.py | 4 +- .../hardware/nodes/network/firewall.py | 67 ++++++++++ .../network/hardware/nodes/network/router.py | 2 +- tests/assets/configs/dmz_network.yaml | 122 ++++++++++++------ tests/conftest.py | 19 +++ .../configuration_file_parsing/__init__.py | 19 +++ .../nodes/__init__.py | 0 .../nodes/network/__init__.py | 0 .../nodes/network/test_firewall_config.py | 45 +++++++ .../nodes/network/test_router_config.py | 54 ++++++++ .../nodes/test_node_config.py | 26 ++++ .../router_game_configuration.py | 76 ----------- ...software_installation_and_configuration.py | 52 +------- 14 files changed, 322 insertions(+), 173 deletions(-) create mode 100644 tests/integration_tests/configuration_file_parsing/nodes/__init__.py create mode 100644 tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py create mode 100644 tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py create mode 100644 tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py create mode 100644 tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py delete mode 100644 tests/integration_tests/configuration_file_parsing/router_game_configuration.py diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 3bc3789a..b860fb2a 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -15,6 +15,7 @@ from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.transport_layer import Port @@ -252,6 +253,8 @@ class PrimaiteGame: ) elif n_type == "router": new_node = Router.from_config(node_cfg) + elif n_type == "firewall": + new_node = Firewall.from_config(node_cfg) else: _LOGGER.warning(f"invalid node type {n_type} in config") if "services" in node_cfg: @@ -264,12 +267,12 @@ class PrimaiteGame: new_node.software_manager.install(SERVICE_TYPES_MAPPING[service_type]) new_service = new_node.software_manager.software[service_type] game.ref_map_services[service_ref] = new_service.uuid + + # start the service + new_service.start() else: _LOGGER.warning(f"service type not found {service_type}") - # start the service - new_service.start() - # service-dependent options if service_type == "DNSClient": if "options" in service_cfg: diff --git a/src/primaite/simulator/__init__.py b/src/primaite/simulator/__init__.py index 97bcd57b..aebd77cf 100644 --- a/src/primaite/simulator/__init__.py +++ b/src/primaite/simulator/__init__.py @@ -12,8 +12,8 @@ class _SimOutput: self._path: Path = ( _PRIMAITE_ROOT.parent.parent / "simulation_output" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S") ) - self.save_pcap_logs: bool = True - self.save_sys_logs: bool = True + self.save_pcap_logs: bool = False + self.save_sys_logs: bool = False @property def path(self) -> Path: diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 22effa2a..f48d0561 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -1,8 +1,10 @@ +from ipaddress import IPv4Address from typing import Dict, Final, Optional, Union from prettytable import MARKDOWN, PrettyTable from pydantic import validate_call +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.router import ( AccessControlList, ACLAction, @@ -491,3 +493,68 @@ class Firewall(Router): """ self.configure_port(DMZ_PORT_ID, ip_address, subnet_mask) self.dmz_port.enable() + + @classmethod + def from_config(cls, cfg: dict) -> "Firewall": + """Create a firewall based on a config dict.""" + new = Firewall(hostname=cfg["hostname"], operating_state=NodeOperatingState.ON) + if "ports" in cfg: + internal_port = cfg["ports"]["internal_port"] + external_port = cfg["ports"]["external_port"] + dmz_port = cfg["ports"]["dmz_port"] + + # configure internal port + new.configure_internal_port( + ip_address=IPV4Address(internal_port.get("ip_address")), + subnet_mask=IPV4Address(internal_port.get("subnet_mask")), + ) + + # configure external port + new.configure_external_port( + ip_address=IPV4Address(external_port.get("ip_address")), + subnet_mask=IPV4Address(external_port.get("subnet_mask")), + ) + + # configure dmz port + new.configure_dmz_port( + ip_address=IPV4Address(dmz_port.get("ip_address")), subnet_mask=IPV4Address(dmz_port.get("subnet_mask")) + ) + if "acl" in cfg: + # acl rules for internal_inbound_acl + if cfg["acl"]["internal_inbound_acl"]: + new.internal_inbound_acl._default_config = cfg["acl"]["internal_inbound_acl"] + new.internal_inbound_acl._reset_rules_to_default() + + # acl rules for internal_outbound_acl + if cfg["acl"]["internal_outbound_acl"]: + new.internal_outbound_acl._default_config = cfg["acl"]["internal_outbound_acl"] + new.internal_outbound_acl._reset_rules_to_default() + + # acl rules for dmz_inbound_acl + if cfg["acl"]["dmz_inbound_acl"]: + new.dmz_inbound_acl._default_config = cfg["acl"]["dmz_inbound_acl"] + new.dmz_inbound_acl._reset_rules_to_default() + + # acl rules for dmz_outbound_acl + if cfg["acl"]["dmz_outbound_acl"]: + new.dmz_outbound_acl._default_config = cfg["acl"]["dmz_outbound_acl"] + new.dmz_outbound_acl._reset_rules_to_default() + + # acl rules for external_inbound_acl + if cfg["acl"]["external_inbound_acl"]: + new.external_inbound_acl._default_config = cfg["acl"]["external_inbound_acl"] + new.external_inbound_acl._reset_rules_to_default() + + # acl rules for external_outbound_acl + if cfg["acl"]["external_outbound_acl"]: + new.external_outbound_acl._default_config = cfg["acl"]["external_outbound_acl"] + new.external_outbound_acl._reset_rules_to_default() + if "routes" in cfg: + for route in cfg.get("routes"): + new.route_table.add_route( + address=IPv4Address(route.get("address")), + subnet_mask=IPv4Address(route.get("subnet_mask")), + next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), + metric=float(route.get("metric")), + ) + return new diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index fd18ce70..d52028a8 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1500,7 +1500,7 @@ class Router(NetworkNode): new.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask")), - next_hop_ip_address=IPv4Address(route.get("subnet_mask")), + next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric")), ) return new diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 0c67ba7c..1a099e41 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -6,19 +6,19 @@ # . . # . Internal Network . # . . -# . -------------- -------------- -------------- . -# . | client_1 |------| switch_1 |------| router_1 | . -# . -------------- -------------- -------------- . +# . -------------- -------------- -------------- . +# . | client_1 |------| switch_1 |--------| router_1 | . +# . -------------- -------------- -------------- . # . (Computer) | . -# ........................................................|..................... +# ........................................................|................... # | # | -# ........................................................|..................... +# ........................................................|................... # . | . # . DMZ Network | . # . | . # . ---------------- -------------- -------------- . -# . | dmz_server |------| switch_2 |------| router_2 | . +# . | dmz_server |------| switch_2 |------| firewall | . # . ---------------- -------------- -------------- . # . (Computer) | . # ........................................................|................... @@ -135,17 +135,17 @@ simulation: action: PERMIT protocol: ICMP routes: - - address: 192.168.10.10 + - address: 192.168.10.10 # route to dmz_server subnet_mask: 255.255.255.0 - next_hop_ip_address: 192.168.11.1 + next_hop_ip_address: 192.168.1.2 metric: 0 - - address: 192.168.20.10 + - address: 192.168.20.10 # route to external_computer subnet_mask: 255.255.255.0 - next_hop_ip_address: 192.168.11.1 + next_hop_ip_address: 192.168.1.2 metric: 0 - - address: 192.168.20.11 + - address: 192.168.20.11 # route to external_server subnet_mask: 255.255.255.0 - next_hop_ip_address: 192.168.11.1 + next_hop_ip_address: 192.168.1.2 metric: 0 - ref: dmz_server @@ -165,32 +165,72 @@ simulation: start_up_duration: 0 shut_down_duration: 0 - - ref: router_2 - type: router - hostname: router_2 - num_ports: 5 + - ref: firewall + type: firewall + hostname: firewall start_up_duration: 0 shut_down_duration: 0 ports: - 1: - ip_address: 192.168.10.1 - subnet_mask: 255.255.255.0 - 2: - ip_address: 192.168.11.1 - subnet_mask: 255.255.255.0 - 3: + external_port: # port 1 ip_address: 192.168.20.1 subnet_mask: 255.255.255.0 + internal_port: # port 2 + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + dmz_port: # port 3 + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 acl: - 22: - action: PERMIT - src_port: ARP - dst_port: ARP - 23: - action: PERMIT - protocol: ICMP + internal_inbound_acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + internal_outbound_acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + dmz_inbound_acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + dmz_outbound_acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + external_inbound_acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + external_outbound_acl: + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP routes: - - address: 192.168.0.10 + - address: 192.168.0.10 # route to client_1 subnet_mask: 255.255.255.0 next_hop_ip_address: 192.168.1.1 metric: 0 @@ -234,14 +274,14 @@ simulation: endpoint_a_port: 1 endpoint_b_ref: switch_1 endpoint_b_port: 8 - - ref: router_1___router_2 - endpoint_a_ref: router_1 - endpoint_a_port: 2 - endpoint_b_ref: router_2 + - ref: router_1___firewall + endpoint_a_ref: firewall + endpoint_a_port: 2 # internal firewall port + endpoint_b_ref: router_1 endpoint_b_port: 2 - - ref: router_2___switch_2 - endpoint_a_ref: router_2 - endpoint_a_port: 1 + - ref: firewall___switch_2 + endpoint_a_ref: firewall + endpoint_a_port: 3 # dmz firewall port endpoint_b_ref: switch_2 endpoint_b_port: 8 - ref: dmz_server___switch_2 @@ -249,9 +289,9 @@ simulation: endpoint_a_port: 1 endpoint_b_ref: switch_2 endpoint_b_port: 1 - - ref: router_2___switch_3 - endpoint_a_ref: router_2 - endpoint_a_port: 3 + - ref: firewall___switch_3 + endpoint_a_ref: firewall + endpoint_a_port: 1 # external firewall port endpoint_b_ref: switch_3 endpoint_b_port: 8 - ref: external_computer___switch_3 diff --git a/tests/conftest.py b/tests/conftest.py index 5084c339..ada89026 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ # © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +from datetime import datetime from pathlib import Path from typing import Any, Dict, Tuple, Union import pytest import yaml +from _pytest.monkeypatch import MonkeyPatch from primaite import getLogger, PRIMAITE_PATHS from primaite.game.agent.actions import ActionManager @@ -12,6 +14,7 @@ from primaite.game.agent.observations import ICSObservation, ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.game.game import PrimaiteGame from primaite.session.session import PrimaiteSession +from primaite.simulator import SIM_OUTPUT from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -29,6 +32,7 @@ from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer +from tests import TEST_ASSETS_ROOT from tests.mock_and_patch.get_session_path_mock import temp_user_sessions_path ACTION_SPACE_NODE_VALUES = 1 @@ -37,6 +41,21 @@ ACTION_SPACE_NODE_ACTION_VALUES = 1 _LOGGER = getLogger(__name__) +@pytest.fixture(scope="function", autouse=True) +def set_syslog_output_to_true(): + """Will be run before each test.""" + monkeypatch = MonkeyPatch() + monkeypatch.setattr( + SIM_OUTPUT, + "path", + Path(TEST_ASSETS_ROOT.parent.parent / "simulation_output" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")), + ) + monkeypatch.setattr(SIM_OUTPUT, "save_pcap_logs", True) + monkeypatch.setattr(SIM_OUTPUT, "save_sys_logs", True) + + yield + + class TestService(Service): """Test Service class""" diff --git a/tests/integration_tests/configuration_file_parsing/__init__.py b/tests/integration_tests/configuration_file_parsing/__init__.py index e69de29b..1c8481d6 100644 --- a/tests/integration_tests/configuration_file_parsing/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/__init__.py @@ -0,0 +1,19 @@ +from pathlib import Path +from typing import Union + +import yaml + +from primaite.game.game import PrimaiteGame +from tests import TEST_ASSETS_ROOT + +BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" + +DMZ_NETWORK = TEST_ASSETS_ROOT / "configs/dmz_network.yaml" + + +def load_config(config_path: Union[str, Path]) -> PrimaiteGame: + """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" + with open(config_path, "r") as f: + cfg = yaml.safe_load(f) + + return PrimaiteGame.from_config(cfg) diff --git a/tests/integration_tests/configuration_file_parsing/nodes/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py b/tests/integration_tests/configuration_file_parsing/nodes/network/__init__.py new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..65fe8c6d --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_firewall_config.py @@ -0,0 +1,45 @@ +import pytest + +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.firewall import Firewall +from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config + + +@pytest.fixture(scope="function") +def dmz_config() -> Network: + game = load_config(DMZ_NETWORK) + return game.simulation.network + + +def test_firewall_is_in_configuration(dmz_config): + """Test that the firewall exists in the configuration file.""" + network: Network = dmz_config + + assert network.get_node_by_hostname("firewall") + + +def test_firewall_routes_are_correctly_added(dmz_config): + """Test that the firewall routes have been correctly added to and configured in the network.""" + network: Network = dmz_config + + firewall: Firewall = network.get_node_by_hostname("firewall") + client_1: Computer = network.get_node_by_hostname("client_1") + dmz_server: Server = network.get_node_by_hostname("dmz_server") + external_computer: Computer = network.get_node_by_hostname("external_computer") + external_server: Server = network.get_node_by_hostname("external_server") + + # there should be a route to client_1 + assert firewall.route_table.find_best_route(client_1.network_interface[1].ip_address) + assert dmz_server.ping(client_1.network_interface[1].ip_address) + assert external_computer.ping(client_1.network_interface[1].ip_address) + assert external_server.ping(client_1.network_interface[1].ip_address) + + +def test_firewall_acl_rules_correctly_added(): + """ + Test that makes sure that the firewall ACLs have been configured onto the firewall + node via configuration file. + """ + pass 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 new file mode 100644 index 00000000..d09d2e94 --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py @@ -0,0 +1,54 @@ +import pytest + +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.hardware.nodes.network.router import Router +from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config + + +@pytest.fixture(scope="function") +def dmz_config() -> Network: + game = load_config(DMZ_NETWORK) + return game.simulation.network + + +def test_router_is_in_configuration(dmz_config): + """Test that the router exists in the configuration file.""" + network: Network = dmz_config + + assert network.get_node_by_hostname("router_1") + + +def test_router_routes_are_correctly_added(dmz_config): + """Test that makes sure that router routes have been added from the configuration file.""" + network: Network = dmz_config + + router_1: Router = network.get_node_by_hostname("router_1") + client_1: Computer = network.get_node_by_hostname("client_1") + dmz_server: Server = network.get_node_by_hostname("dmz_server") + external_computer: Computer = network.get_node_by_hostname("external_computer") + external_server: Server = network.get_node_by_hostname("external_server") + + # there should be a route to dmz_server + assert router_1.route_table.find_best_route(dmz_server.network_interface[1].ip_address) + assert client_1.ping(dmz_server.network_interface[1].ip_address) + assert external_computer.ping(dmz_server.network_interface[1].ip_address) + assert external_server.ping(dmz_server.network_interface[1].ip_address) + + # there should be a route to external_computer + assert router_1.route_table.find_best_route(external_computer.network_interface[1].ip_address) + assert client_1.ping(external_computer.network_interface[1].ip_address) + assert dmz_server.ping(external_computer.network_interface[1].ip_address) + assert external_server.ping(external_computer.network_interface[1].ip_address) + + # there should be a route to external_server + assert router_1.route_table.find_best_route(external_server.network_interface[1].ip_address) + assert client_1.ping(external_server.network_interface[1].ip_address) + assert dmz_server.ping(external_server.network_interface[1].ip_address) + assert external_computer.ping(external_server.network_interface[1].ip_address) + + +def test_router_acl_rules_correctly_added(): + """Test that makes sure that the router ACLs have been configured onto the router node via configuration file.""" + pass 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 new file mode 100644 index 00000000..e222bfaf --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/nodes/test_node_config.py @@ -0,0 +1,26 @@ +from primaite.config.load import example_config_path +from primaite.simulator.network.container import Network +from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config + + +def test_example_config(): + """Test that the example config can be parsed properly.""" + game = load_config(example_config_path()) + network: Network = game.simulation.network + + assert len(network.nodes) == 10 # 10 nodes in example network + assert len(network.routers) == 1 # 1 router in network + assert len(network.switches) == 2 # 2 switches in network + assert len(network.servers) == 5 # 5 servers in network + + +def test_dmz_config(): + """Test that the DMZ network config can be parsed properly.""" + game = load_config(DMZ_NETWORK) + + network: Network = game.simulation.network + + assert len(network.nodes) == 9 # 9 nodes in network + assert len(network.routers) == 2 # 2 routers in network + assert len(network.switches) == 3 # 3 switches in network + assert len(network.servers) == 2 # 2 servers in network diff --git a/tests/integration_tests/configuration_file_parsing/router_game_configuration.py b/tests/integration_tests/configuration_file_parsing/router_game_configuration.py deleted file mode 100644 index 9d682dcc..00000000 --- a/tests/integration_tests/configuration_file_parsing/router_game_configuration.py +++ /dev/null @@ -1,76 +0,0 @@ -from pathlib import Path -from typing import Union - -import yaml - -from primaite.game.game import PrimaiteGame -from primaite.simulator.network.container import Network -from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.hardware.nodes.network.router import Router -from tests import TEST_ASSETS_ROOT - -DMZ_NETWORK = TEST_ASSETS_ROOT / "configs/dmz_network.yaml" - - -def load_config(config_path: Union[str, Path]) -> PrimaiteGame: - """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" - with open(config_path, "r") as f: - cfg = yaml.safe_load(f) - - return PrimaiteGame.from_config(cfg) - - -def test_dmz_config(): - """Test that the DMZ network config can be parsed properly.""" - game = load_config(DMZ_NETWORK) - - network: Network = game.simulation.network - - assert len(network.nodes) == 9 # 9 nodes in network - assert len(network.routers) == 2 # 2 routers in network - assert len(network.switches) == 3 # 3 switches in network - assert len(network.servers) == 2 # 2 servers in network - - -def test_router_routes_are_correctly_added(): - """Test that makes sure that router routes have been added from the configuration file.""" - game = load_config(DMZ_NETWORK) - - network: Network = game.simulation.network - - router_1: Router = network.get_node_by_hostname("router_1") - client_1: Computer = network.get_node_by_hostname("client_1") - dmz_server: Server = network.get_node_by_hostname("dmz_server") - external_computer: Computer = network.get_node_by_hostname("external_computer") - external_server: Server = network.get_node_by_hostname("external_server") - - # test that client_1 has a route to the DMZ and external nodes - they are on a second router - - # there should be a route to the dmz server - assert router_1.route_table.find_best_route(dmz_server.network_interface[1].ip_address) - # ping DMZ server - # assert client_1.ping(dmz_server.network_interface[1].ip_address) - - -def test_firewall_node_added_to_network(): - """Test that the firewall has been correctly added to and configured in the network.""" - pass - - -def test_router_acl_rules_correctly_added(): - """Test that makes sure that the router ACLs have been configured onto the router node via configuration file.""" - pass - - -def test_firewall_routes_are_correctly_added(): - """Test that the firewall routes have been correctly added to and configured in the network.""" - pass - - -def test_firewall_acl_rules_correctly_added(): - """ - Test that makes sure that the firewall ACLs have been configured onto the firewall - node via configuration file. - """ - pass 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 3bd870e3..54dca371 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,14 +1,6 @@ from ipaddress import IPv4Address -from pathlib import Path -from typing import Union -import yaml - -from primaite.config.load import example_config_path -from primaite.game.agent.data_manipulation_bot import DataManipulationAgent -from primaite.game.agent.interface import ProxyAgent, RandomAgent -from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING -from primaite.simulator.network.container import Network +from primaite.game.game import APPLICATION_TYPES_MAPPING, SERVICE_TYPES_MAPPING from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot @@ -22,47 +14,7 @@ from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.ntp.ntp_client import NTPClient from primaite.simulator.system.services.ntp.ntp_server import NTPServer from primaite.simulator.system.services.web_server.web_server import WebServer -from tests import TEST_ASSETS_ROOT - -BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" - - -def load_config(config_path: Union[str, Path]) -> PrimaiteGame: - """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" - with open(config_path, "r") as f: - cfg = yaml.safe_load(f) - - return PrimaiteGame.from_config(cfg) - - -def test_example_config(): - """Test that the example config can be parsed properly.""" - game = load_config(example_config_path()) - - assert len(game.agents) == 4 # red, blue and 2 green agents - - # green agent 1 - assert game.agents[0].agent_name == "client_2_green_user" - assert isinstance(game.agents[0], RandomAgent) - - # green agent 2 - assert game.agents[1].agent_name == "client_1_green_user" - assert isinstance(game.agents[1], RandomAgent) - - # red agent - assert game.agents[2].agent_name == "client_1_data_manipulation_red_bot" - assert isinstance(game.agents[2], DataManipulationAgent) - - # blue agent - assert game.agents[3].agent_name == "defender" - assert isinstance(game.agents[3], ProxyAgent) - - network: Network = game.simulation.network - - assert len(network.nodes) == 10 # 10 nodes in example network - assert len(network.routers) == 1 # 1 router in network - assert len(network.switches) == 2 # 2 switches in network - assert len(network.servers) == 5 # 5 servers in network +from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, load_config def test_node_software_install(): From e390d8385c9f5e54dd3c6a929c1557c4b6c38e44 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 15 Feb 2024 16:29:36 +0000 Subject: [PATCH 005/128] #2257: acl tests --- tests/assets/configs/dmz_network.yaml | 6 -- .../nodes/network/test_firewall_config.py | 61 ++++++++++++++++++- .../nodes/network/test_router_config.py | 17 +++++- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 1a099e41..971ed8cd 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -218,17 +218,11 @@ simulation: action: PERMIT src_port: ARP dst_port: ARP - 23: - action: PERMIT - protocol: ICMP external_outbound_acl: 22: action: PERMIT src_port: ARP dst_port: ARP - 23: - action: PERMIT - protocol: ICMP routes: - address: 192.168.0.10 # route to client_1 subnet_mask: 255.255.255.0 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 65fe8c6d..ae71809b 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 @@ -4,6 +4,9 @@ from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.firewall import Firewall +from primaite.simulator.network.hardware.nodes.network.router import ACLAction +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config @@ -37,9 +40,63 @@ def test_firewall_routes_are_correctly_added(dmz_config): assert external_server.ping(client_1.network_interface[1].ip_address) -def test_firewall_acl_rules_correctly_added(): +def test_firewall_acl_rules_correctly_added(dmz_config): """ Test that makes sure that the firewall ACLs have been configured onto the firewall node via configuration file. """ - pass + firewall: Firewall = dmz_config.get_node_by_hostname("firewall") + + # ICMP and ARP should be allowed internal_inbound + assert firewall.internal_inbound_acl.num_rules == 2 + assert firewall.internal_inbound_acl.acl[22].action == ACLAction.PERMIT + assert firewall.internal_inbound_acl.acl[22].src_port == Port.ARP + assert firewall.internal_inbound_acl.acl[22].dst_port == Port.ARP + assert firewall.internal_inbound_acl.acl[23].action == ACLAction.PERMIT + assert firewall.internal_inbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.internal_inbound_acl.implicit_action == ACLAction.DENY + + # ICMP and ARP should be allowed internal_outbound + assert firewall.internal_outbound_acl.num_rules == 2 + assert firewall.internal_outbound_acl.acl[22].action == ACLAction.PERMIT + assert firewall.internal_outbound_acl.acl[22].src_port == Port.ARP + assert firewall.internal_outbound_acl.acl[22].dst_port == Port.ARP + assert firewall.internal_outbound_acl.acl[23].action == ACLAction.PERMIT + assert firewall.internal_outbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.internal_outbound_acl.implicit_action == ACLAction.DENY + + # ICMP and ARP should be allowed dmz_inbound + assert firewall.dmz_inbound_acl.num_rules == 2 + assert firewall.dmz_inbound_acl.acl[22].action == ACLAction.PERMIT + assert firewall.dmz_inbound_acl.acl[22].src_port == Port.ARP + assert firewall.dmz_inbound_acl.acl[22].dst_port == Port.ARP + assert firewall.dmz_inbound_acl.acl[23].action == ACLAction.PERMIT + assert firewall.dmz_inbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.dmz_inbound_acl.implicit_action == ACLAction.DENY + + # ICMP and ARP should be allowed dmz_outbound + assert firewall.dmz_outbound_acl.num_rules == 2 + assert firewall.dmz_outbound_acl.acl[22].action == ACLAction.PERMIT + assert firewall.dmz_outbound_acl.acl[22].src_port == Port.ARP + assert firewall.dmz_outbound_acl.acl[22].dst_port == Port.ARP + assert firewall.dmz_outbound_acl.acl[23].action == ACLAction.PERMIT + assert firewall.dmz_outbound_acl.acl[23].protocol == IPProtocol.ICMP + assert firewall.dmz_outbound_acl.implicit_action == ACLAction.DENY + + # ICMP and ARP should be allowed external_inbound + assert firewall.external_inbound_acl.num_rules == 1 + assert firewall.external_inbound_acl.acl[22].action == ACLAction.PERMIT + assert firewall.external_inbound_acl.acl[22].src_port == Port.ARP + assert firewall.external_inbound_acl.acl[22].dst_port == Port.ARP + # external_inbound should have implicit action PERMIT + # ICMP does not have a provided ACL Rule but implicit action should allow anything + assert firewall.external_inbound_acl.implicit_action == ACLAction.PERMIT + + # ICMP and ARP should be allowed external_outbound + assert firewall.external_outbound_acl.num_rules == 1 + assert firewall.external_outbound_acl.acl[22].action == ACLAction.PERMIT + assert firewall.external_outbound_acl.acl[22].src_port == Port.ARP + assert firewall.external_outbound_acl.acl[22].dst_port == Port.ARP + # external_outbound should have implicit action PERMIT + # ICMP does not have a provided ACL Rule but implicit action should allow anything + assert firewall.external_outbound_acl.implicit_action == ACLAction.PERMIT diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index d09d2e94..fbaca12d 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 @@ -3,7 +3,9 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.hardware.nodes.network.router import Router +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config @@ -49,6 +51,15 @@ def test_router_routes_are_correctly_added(dmz_config): assert external_computer.ping(external_server.network_interface[1].ip_address) -def test_router_acl_rules_correctly_added(): +def test_router_acl_rules_correctly_added(dmz_config): """Test that makes sure that the router ACLs have been configured onto the router node via configuration file.""" - pass + router_1: Router = dmz_config.get_node_by_hostname("router_1") + + # ICMP and ARP should be allowed + assert router_1.acl.num_rules == 2 + assert router_1.acl.acl[22].action == ACLAction.PERMIT + assert router_1.acl.acl[22].src_port == Port.ARP + assert router_1.acl.acl[22].dst_port == Port.ARP + assert router_1.acl.acl[23].action == ACLAction.PERMIT + assert router_1.acl.acl[23].protocol == IPProtocol.ICMP + assert router_1.acl.implicit_action == ACLAction.DENY From 2e2d83c3e9775d1ba87f717abd0cba4937c37534 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 16 Feb 2024 16:14:36 +0000 Subject: [PATCH 006/128] #2257: update sphinx version + cleaning up some errors + splitting configuration page into multiple pages --- docs/api.rst | 2 + docs/conf.py | 11 +- docs/source/config.rst | 105 ++---------------- docs/source/configuration/agents.rst | 45 ++++++++ docs/source/configuration/game.rst | 8 ++ docs/source/configuration/io_settings.rst | 26 +++++ docs/source/configuration/simulation.rst | 27 +++++ docs/source/configuration/training_config.rst | 25 +++++ docs/source/dependencies.rst | 2 + docs/source/request_system.rst | 4 +- docs/source/simulation.rst | 2 +- .../network/base_hardware.rst | 59 ++++++++++ .../simulation_components/system/software.rst | 1 + docs/source/state_system.rst | 2 +- pyproject.toml | 4 +- 15 files changed, 220 insertions(+), 103 deletions(-) create mode 100644 docs/source/configuration/agents.rst create mode 100644 docs/source/configuration/game.rst create mode 100644 docs/source/configuration/io_settings.rst create mode 100644 docs/source/configuration/simulation.rst create mode 100644 docs/source/configuration/training_config.rst diff --git a/docs/api.rst b/docs/api.rst index aeaef4e2..13f3a1ec 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,3 +1,5 @@ +:orphan: + .. only:: comment © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK diff --git a/docs/conf.py b/docs/conf.py index efd60b49..6cdc0ac4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,6 @@ import furo # noqa sys.path.insert(0, os.path.abspath("../")) - # -- Project information ----------------------------------------------------- year = datetime.datetime.now().year project = "PrimAITE" @@ -45,13 +44,17 @@ extensions = [ "sphinx_copybutton", # Adds a copy button to code blocks ] - templates_path = ["_templates"] -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", +] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "furo" html_static_path = ["_static"] +html_theme_options = {"globaltoc_collapse": True, "globaltoc_maxdepth": 2} +html_copy_source = False diff --git a/docs/source/config.rst b/docs/source/config.rst index 575a3139..46631ab9 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,3 +1,7 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + Primaite v3 config ****************** @@ -5,98 +9,13 @@ PrimAITE uses a single configuration file to define everything needed to train a The entire config is used by the ``PrimaiteSession`` object for users who wish to let PrimAITE handle the agent definition and training. If you wish to define custom agents and control the training loop yourself, you can use the config with the ``PrimaiteGame``, and ``PrimaiteGymEnv`` objects instead. That way, only the network configuration and agent setup parts of the config are used, and the training section is ignored. Configurable items -================== +################## -``training_config`` -------------------- -This section allows selecting which training framework and algorithm to use, and set some training hyperparameters. +.. toctree:: + :maxdepth: 1 -``io_settings`` ---------------- -This section configures how PrimAITE saves data during simulation and training. - -**save_final_model**: Only used if training with PrimaiteSession, if true, the policy will be saved after the final training iteration. - -**save_checkpoints**: Only used if training with PrimaiteSession, if true, the policy will be saved periodically during training. - -**checkpoint_interval**: Only used if training with PrimaiteSession and if ``save_checkpoints`` is true. Defines how often to save the policy during training. - -**save_logs**: *currently unused*. - -**save_transactions**: *currently unused*. - -**save_tensorboard_logs**: *currently unused*. - -**save_step_metadata**: Whether to save the RL agents' action, environment state, and other data at every single step. - -**save_pcap_logs**: Whether to save pcap files of all network traffic during the simulation. - -**save_sys_logs**: Whether to save system logs from all nodes during the simulation. - -``game`` --------- -This section defines high-level settings that apply across the game, currently it's used to help shape the action and observation spaces by restricting which ports and internet protocols should be considered. Here, users can also set the maximum number of steps in an episode. - -``agents`` ----------- -Agents can be scripted (deterministic and stochastic), or controlled by a reinforcement learning algorithm. Not to be confused with an RL agent, the term agent here is used to refer to an entity that sends requests to the simulated network. In this part of the config, each agent's action space, observation space, and reward function can be defined. All three are defined in a modular way. - -**type**: Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``GreenWebBrowsingAgent`` generate their own behaviour. - -**team**: Specifies if the agent is malicious (RED), benign (GREEN), or defensive (BLUE). Currently this value is not used for anything. - -**observation space:** - * ``type``: selects which python class from the ``primaite.game.agent.observation`` module is used for the overall observation structure. - * ``options``: allows configuring the chosen observation type. The ``UC2BlueObservation`` should be used for RL Agents. - * ``num_services_per_node``, ``num_folders_per_node``, ``num_files_per_folder``, ``num_nics_per_node`` all define the shape of the observation space. The size and shape of the obs space must remain constant, but the number of files, folders, ACL rules, and other components can change within an episode. Therefore padding is performed and these options set the size of the obs space. - * ``nodes``: list of nodes that will be present in this agent's observation space. The ``node_ref`` relates to the human-readable unique reference defined later in the ``simulation`` part of the config. Each node can also be configured with services, and files that should be monitored. - * ``links``: list of links that will be present in this agent's observation space. The ``link_ref`` relates to the human-readable unique reference defined later in the ``simulation`` part of the config. - * ``acl``: configure how the agent reads the access control list on the router in the simulation. ``router_node_ref`` is for selecting which router's ACL table should be used. ``ip_address_order`` sets the encoding of ip addresses as integers within the observation space. - -**action space:** -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``. - -Description of configurable items: - * ``action_list``: a list of action modules. The options are listed in the ``primaite.game.agent.actions`` module. - * ``action_map``: (optional). Restricts the possible combinations of action type / action parameter values to reduce the overall size of the action space. By default, every possible combination of actions and parameters will be assigned an integer for the agent's ``MultiDiscrete`` action space. Instead, the ``action_map`` allows you to list the actions corresponding to each integer in the ``MultiDiscrete`` space. - * ``options``: Options that apply too all action components. - * ``nodes``: list the nodes that the agent can act on, the order of this list defines the mapping between nodes and ``node_id`` integers. - * ``max_folders_per_node``, ``max_files_per_folder``, ``max_services_per_node``, ``max_nics_per_node``, ``max_acl_rules`` all are used to define the size of the action space. - -**reward function:** -Similar to action space, this is defined as a list of components. - -Description of configurable items: - * ``reward_components`` a list of reward components from the ``primaite.game.agent.reward`` module. - * ``weight``: relative importance of this reward component. The total reward for a step is a weighted sum of all reward components. - * ``options``: list of options passed to the reward component during initialisation, the exact options required depend on the reward component. - -**agent_settings**: -Settings passed to the agent during initialisation. These depend on the agent class. - -Reinforcement learning agents use the ``ProxyAgent`` class, they accept these agent settings: - -**flatten_obs**: If true, gymnasium flattening will be performed on the observation space before sending to the agent. Set this to true if your agent does not support nested observation spaces. - -``simulation`` --------------- -In this section the network layout is defined. This part of the config follows a hierarchical structure. Almost every component defines a ``ref`` field which acts as a human-readable unique identifier, used by other parts of the config, such as agents. - -At the top level of the network are ``nodes`` and ``links``. - -**nodes:** - * ``type``: one of ``router``, ``switch``, ``computer``, or ``server``, this affects what other sub-options should be defined. - * ``hostname`` - a non-unique name used for logging and outputs. - * ``num_ports`` (optional, routers and switches only): number of network interfaces present on the device. - * ``ports`` (optional, routers and switches only): configuration for each network interface, including IP address and subnet mask. - * ``acl`` (Router only): Define the ACL rules at each index of the ACL on the router. the possible options are: ``action`` (PERMIT or DENY), ``src_port``, ``dst_port``, ``protocol``, ``src_ip``, ``dst_ip``. Any options left blank default to none which usually means that it will apply across all options. For example leaving ``src_ip`` blank will apply the rule to all IP addresses. - * ``services`` (computers and servers only): a list of services to install on the node. They must define a ``ref``, ``type``, and ``options`` that depend on which ``type`` was selected. - * ``applications`` (computer and servers only): Similar to services. A list of application to install on the node. - * ``network_interfaces`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``. - -**links:** - * ``ref``: unique identifier for this link - * ``endpoint_a_ref``: Reference to the node at the first end of the link - * ``endpoint_a_port``: The ethernet port or switch port index of the second node - * ``endpoint_b_ref``: Reference to the node at the second end of the link - * ``endpoint_b_port``: The ethernet port or switch port index on the second node + configuration/training_config.rst + configuration/io_settings.rst + configuration/game.rst + configuration/agents.rst + configuration/simulation.rst diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst new file mode 100644 index 00000000..4d81c89d --- /dev/null +++ b/docs/source/configuration/agents.rst @@ -0,0 +1,45 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + + +``agents`` +========== +Agents can be scripted (deterministic and stochastic), or controlled by a reinforcement learning algorithm. Not to be confused with an RL agent, the term agent here is used to refer to an entity that sends requests to the simulated network. In this part of the config, each agent's action space, observation space, and reward function can be defined. All three are defined in a modular way. + +**type**: Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``GreenWebBrowsingAgent`` generate their own behaviour. + +**team**: Specifies if the agent is malicious (RED), benign (GREEN), or defensive (BLUE). Currently this value is not used for anything. + +**observation space:** + * ``type``: selects which python class from the ``primaite.game.agent.observation`` module is used for the overall observation structure. + * ``options``: allows configuring the chosen observation type. The ``UC2BlueObservation`` should be used for RL Agents. + * ``num_services_per_node``, ``num_folders_per_node``, ``num_files_per_folder``, ``num_nics_per_node`` all define the shape of the observation space. The size and shape of the obs space must remain constant, but the number of files, folders, ACL rules, and other components can change within an episode. Therefore padding is performed and these options set the size of the obs space. + * ``nodes``: list of nodes that will be present in this agent's observation space. The ``node_ref`` relates to the human-readable unique reference defined later in the ``simulation`` part of the config. Each node can also be configured with services, and files that should be monitored. + * ``links``: list of links that will be present in this agent's observation space. The ``link_ref`` relates to the human-readable unique reference defined later in the ``simulation`` part of the config. + * ``acl``: configure how the agent reads the access control list on the router in the simulation. ``router_node_ref`` is for selecting which router's ACL table should be used. ``ip_address_order`` sets the encoding of ip addresses as integers within the observation space. + +**action space:** +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``. + +Description of configurable items: + * ``action_list``: a list of action modules. The options are listed in the ``primaite.game.agent.actions`` module. + * ``action_map``: (optional). Restricts the possible combinations of action type / action parameter values to reduce the overall size of the action space. By default, every possible combination of actions and parameters will be assigned an integer for the agent's ``MultiDiscrete`` action space. Instead, the ``action_map`` allows you to list the actions corresponding to each integer in the ``MultiDiscrete`` space. + * ``options``: Options that apply too all action components. + * ``nodes``: list the nodes that the agent can act on, the order of this list defines the mapping between nodes and ``node_id`` integers. + * ``max_folders_per_node``, ``max_files_per_folder``, ``max_services_per_node``, ``max_nics_per_node``, ``max_acl_rules`` all are used to define the size of the action space. + +**reward function:** +Similar to action space, this is defined as a list of components. + +Description of configurable items: + * ``reward_components`` a list of reward components from the ``primaite.game.agent.reward`` module. + * ``weight``: relative importance of this reward component. The total reward for a step is a weighted sum of all reward components. + * ``options``: list of options passed to the reward component during initialisation, the exact options required depend on the reward component. + +**agent_settings**: +Settings passed to the agent during initialisation. These depend on the agent class. + +Reinforcement learning agents use the ``ProxyAgent`` class, they accept these agent settings: + +**flatten_obs**: If true, gymnasium flattening will be performed on the observation space before sending to the agent. Set this to true if your agent does not support nested observation spaces. diff --git a/docs/source/configuration/game.rst b/docs/source/configuration/game.rst new file mode 100644 index 00000000..797c3813 --- /dev/null +++ b/docs/source/configuration/game.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + + +``game`` +======== +This section defines high-level settings that apply across the game, currently it's used to help shape the action and observation spaces by restricting which ports and internet protocols should be considered. Here, users can also set the maximum number of steps in an episode. diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst new file mode 100644 index 00000000..11d044bb --- /dev/null +++ b/docs/source/configuration/io_settings.rst @@ -0,0 +1,26 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + + +``io_settings`` +=============== +This section configures how PrimAITE saves data during simulation and training. + +**save_final_model**: Only used if training with PrimaiteSession, if true, the policy will be saved after the final training iteration. + +**save_checkpoints**: Only used if training with PrimaiteSession, if true, the policy will be saved periodically during training. + +**checkpoint_interval**: Only used if training with PrimaiteSession and if ``save_checkpoints`` is true. Defines how often to save the policy during training. + +**save_logs**: *currently unused*. + +**save_transactions**: *currently unused*. + +**save_tensorboard_logs**: *currently unused*. + +**save_step_metadata**: Whether to save the RL agents' action, environment state, and other data at every single step. + +**save_pcap_logs**: Whether to save pcap files of all network traffic during the simulation. + +**save_sys_logs**: Whether to save system logs from all nodes during the simulation. diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst new file mode 100644 index 00000000..eb13e2be --- /dev/null +++ b/docs/source/configuration/simulation.rst @@ -0,0 +1,27 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + + +``simulation`` +============== +In this section the network layout is defined. This part of the config follows a hierarchical structure. Almost every component defines a ``ref`` field which acts as a human-readable unique identifier, used by other parts of the config, such as agents. + +At the top level of the network are ``nodes`` and ``links``. + +**nodes:** + * ``type``: one of ``router``, ``switch``, ``computer``, or ``server``, this affects what other sub-options should be defined. + * ``hostname`` - a non-unique name used for logging and outputs. + * ``num_ports`` (optional, routers and switches only): number of network interfaces present on the device. + * ``ports`` (optional, routers and switches only): configuration for each network interface, including IP address and subnet mask. + * ``acl`` (Router only): Define the ACL rules at each index of the ACL on the router. the possible options are: ``action`` (PERMIT or DENY), ``src_port``, ``dst_port``, ``protocol``, ``src_ip``, ``dst_ip``. Any options left blank default to none which usually means that it will apply across all options. For example leaving ``src_ip`` blank will apply the rule to all IP addresses. + * ``services`` (computers and servers only): a list of services to install on the node. They must define a ``ref``, ``type``, and ``options`` that depend on which ``type`` was selected. + * ``applications`` (computer and servers only): Similar to services. A list of application to install on the node. + * ``network_interfaces`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``. + +**links:** + * ``ref``: unique identifier for this link + * ``endpoint_a_ref``: Reference to the node at the first end of the link + * ``endpoint_a_port``: The ethernet port or switch port index of the second node + * ``endpoint_b_ref``: Reference to the node at the second end of the link + * ``endpoint_b_port``: The ethernet port or switch port index on the second node diff --git a/docs/source/configuration/training_config.rst b/docs/source/configuration/training_config.rst new file mode 100644 index 00000000..cde6cf52 --- /dev/null +++ b/docs/source/configuration/training_config.rst @@ -0,0 +1,25 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``training_config`` +=================== + +``rl_framework`` +---------------- +The RL (Reinforcement Learning) Framework to use in the training session + +Options available are: + +- ``SB3`` (Stable Baselines 3) +- ``RLLIB_single_agent`` (Single Agent Ray RLLib) +- ``RLLIB_multi_agent`` (Multi Agent Ray RLLib) + +``rl_algorithm`` +---------------- +The Reinforcement Learning Algorithm to use in the training session + +Options available are: + +- ``PPO`` (Proximal Policy Optimisation) +- ``A2C`` (Advantage Actor Critic) diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst index 942ccfd8..ddea27fa 100644 --- a/docs/source/dependencies.rst +++ b/docs/source/dependencies.rst @@ -5,6 +5,8 @@ .. role:: raw-html(raw) :format: html +.. _Dependencies: + Dependencies ============ diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index 392bc792..e4c5584e 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -36,7 +36,7 @@ Technical Detail This system was achieved by implementing two classes, :py:class:`primaite.simulator.core.RequestType`, and :py:class:`primaite.simulator.core.RequestManager`. ``RequestType`` ------- +--------------- The ``RequestType`` object stores a reference to a method that executes the request, for example a node could have a request type that stores a reference to ``self.turn_on()``. Technically, this can be any callable that accepts `request, context` as it's parameters. In practice, this is often defined using ``lambda`` functions within a component's ``self._init_request_manager()`` method. Optionally, the ``RequestType`` object can also hold a validator that will permit/deny the request depending on context. @@ -60,7 +60,7 @@ A simple example without chaining can be seen in the :py:class:`primaite.simulat *ellipses (``...``) used to omit code impertinent to this explanation* Chaining RequestManagers ------------------------ +------------------------ A request function needs to be a callable that accepts ``request, context`` as parameters. Since the request manager resolves requests by invoking it with ``request, context`` as parameter, it is possible to use a ``RequestManager`` as a ``RequestType``. diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index c703b299..c4bf1bf0 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -22,9 +22,9 @@ Contents simulation_components/network/nodes/host_node simulation_components/network/nodes/network_node simulation_components/network/nodes/router + simulation_components/network/nodes/switch simulation_components/network/nodes/wireless_router simulation_components/network/nodes/firewall - simulation_components/network/switch simulation_components/network/network simulation_components/system/internal_frame_processing simulation_components/system/sys_log diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index c7545810..3aa6b073 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -41,6 +41,65 @@ Node Attributes - **session_manager**: Manages user sessions within the node. - **software_manager**: Controls the installation and management of software and services on the node. +.. _Node Start up and Shut down: + +Node Start up and Shut down +--------------------------- +Nodes are powered on and off over multiple timesteps. By default, the node ``start_up_duration`` and ``shut_down_duration`` is 3 timesteps. + +Example code where a node is turned on: + +.. code-block:: python + + from primaite.simulator.network.hardware.base import Node + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + + node = Node(hostname="pc_a") + + assert node.operating_state is NodeOperatingState.OFF # By default, node is instantiated in an OFF state + + node.power_on() # power on the node + + assert node.operating_state is NodeOperatingState.BOOTING # node is booting up + + for i in range(node.start_up_duration + 1): + # apply timestep until the node start up duration + node.apply_timestep(timestep=i) + + assert node.operating_state is NodeOperatingState.ON # node is in ON state + + +If the node needs to be instantiated in an on state: + + +.. code-block:: python + + from primaite.simulator.network.hardware.base import Node + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + + node = Node(hostname="pc_a", operating_state=NodeOperatingState.ON) + + assert node.operating_state is NodeOperatingState.ON # node is in ON state + +Setting ``start_up_duration`` and/or ``shut_down_duration`` to ``0`` will allow for the ``power_on`` and ``power_off`` methods to be completed instantly without applying timesteps: + +.. code-block:: python + + from primaite.simulator.network.hardware.base import Node + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + + node = Node(hostname="pc_a", start_up_duration=0, shut_down_duration=0) + + assert node.operating_state is NodeOperatingState.OFF # node is in OFF state + + node.power_on() + + assert node.operating_state is NodeOperatingState.ON # node is in ON state + + node.power_off() + + assert node.operating_state is NodeOperatingState.OFF # node is in OFF state + Node Behaviours/Functions ------------------------- diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index cd6b0aa3..7a1359f4 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -50,4 +50,5 @@ Services, Processes and Applications: data_manipulation_bot dns_client_server ftp_client_server + ntp_client_server web_browser_and_web_server_service diff --git a/docs/source/state_system.rst b/docs/source/state_system.rst index 860c9827..0bbbdd34 100644 --- a/docs/source/state_system.rst +++ b/docs/source/state_system.rst @@ -3,7 +3,7 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK Simulation State -============== +================ ``SimComponent`` objects in the simulation have a method called ``describe_state`` which return a dictionary of the state of the component. This is used to report pertinent data that could impact an agent's actions or rewards. For instance, the name and health status of a node is reported, which can be used by a reward function to punish corrupted or compromised nodes and reward healthy nodes. Each ``SimComponent`` object reports not only its own attributes in the state but also those of its child components. I.e. a computer node will report the state of its ``FileSystem`` and the ``FileSystem`` will report the state of its files and folders. This happens by recursively calling the childrens' own ``describe_state`` methods. diff --git a/pyproject.toml b/pyproject.toml index 3e5b959a..44ce75c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dev = [ "build==0.10.0", "flake8==6.0.0", "flake8-annotations", - "furo==2023.3.27", + "furo==2024.01.29", "gputil==1.4.0", "pip-licenses==4.3.0", "pre-commit==2.20.0", @@ -67,7 +67,7 @@ dev = [ "pytest-cov==4.0.0", "pytest-flake8==1.1.1", "setuptools==66", - "Sphinx==6.1.3", + "Sphinx==7.2.6", "sphinx-copybutton==0.5.2", "wheel==0.38.4" ] From 945db1341bb52af4cd1badbf60fe24972b99940c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 11:04:53 +0000 Subject: [PATCH 007/128] Make database client try to use most recent connection instead of generating new one --- .../simulator/system/applications/database_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index fbeefe6a..dfa5e445 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -193,7 +193,10 @@ class DatabaseClient(Application): return False if connection_id is None: - connection_id = str(uuid4()) + if self.connections: + connection_id = list(self.connections.keys())[-1] + else: + connection_id = str(uuid4()) if not self.connections.get(connection_id): if not self.connect(connection_id=connection_id): From 701781b23e58fbf9f7cf21b2ce7b2ef7682cdf95 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 11:05:09 +0000 Subject: [PATCH 008/128] Clear link load in new timestep --- src/primaite/simulator/network/hardware/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 5334021a..01dd736d 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -599,6 +599,11 @@ class Link(SimComponent): def __str__(self) -> str: return f"{self.endpoint_a}<-->{self.endpoint_b}" + def apply_timestep(self, timestep: int) -> None: + """Apply a timestep to the simulation.""" + super().apply_timestep(timestep) + self.current_load = 0.0 + class ARPCache: """ From 4a3c66bdc605e5ce0ee6863f30395a6d3a83ed84 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 12:04:07 +0000 Subject: [PATCH 009/128] Clear notebook code cells. --- src/primaite/notebooks/uc2_demo.ipynb | 680 +------------------------- 1 file changed, 21 insertions(+), 659 deletions(-) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index b37e69fc..48ca795a 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -333,7 +333,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -343,20 +343,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/cade/repos/PrimAITE/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n", - "2024-02-07 10:58:13,192\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", - "2024-02-07 10:58:17,136\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n" - ] - } - ], + "outputs": [], "source": [ "# Imports\n", "from primaite.config.load import example_config_path\n", @@ -377,134 +366,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resetting environment, episode 0, avg. reward: 0.0\n", - "env created successfully\n", - "{'ACL': {1: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 0,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 2: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 1,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 3: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 2,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 4: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 3,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 5: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 4,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 6: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 5,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 7: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 6,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 8: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 7,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 9: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 8,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 10: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 9,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0}},\n", - " 'ICS': 0,\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 0}},\n", - " 2: {'PROTOCOLS': {'ALL': 0}},\n", - " 3: {'PROTOCOLS': {'ALL': 0}},\n", - " 4: {'PROTOCOLS': {'ALL': 0}},\n", - " 5: {'PROTOCOLS': {'ALL': 0}},\n", - " 6: {'PROTOCOLS': {'ALL': 0}},\n", - " 7: {'PROTOCOLS': {'ALL': 0}},\n", - " 8: {'PROTOCOLS': {'ALL': 0}},\n", - " 9: {'PROTOCOLS': {'ALL': 0}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", - " 'health_status': 1}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}}\n" - ] - } - ], + "outputs": [], "source": [ "# create the env\n", "with open(example_config_path(), 'r') as f:\n", @@ -532,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -550,51 +414,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 1, Red action: DO NOTHING, Blue reward:0.34\n", - "step: 2, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 3, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 4, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 5, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 6, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 7, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 8, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 9, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 10, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 11, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 12, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 13, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 14, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 15, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 16, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 17, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 18, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 19, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 20, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 21, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 22, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 23, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 24, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 25, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 26, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 27, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 28, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 29, Red action: DO NOTHING, Blue reward:1.0\n", - "step: 30, Red action: ATTACK from client 1, Blue reward:0.32\n", - "step: 31, Red action: DO NOTHING, Blue reward:0.32\n", - "step: 32, Red action: DO NOTHING, Blue reward:0.32\n", - "step: 33, Red action: DO NOTHING, Blue reward:-1.0\n", - "step: 34, Red action: DO NOTHING, Blue reward:-1.0\n", - "step: 35, Red action: DO NOTHING, Blue reward:-1.0\n" - ] - } - ], + "outputs": [], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -610,44 +432,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}\n" - ] - } - ], + "outputs": [], "source": [ "pprint(obs['NODES'])" ] @@ -661,44 +448,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'nic_status': 1}, 2: {'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -722,21 +474,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 38\n", - "Red action: DONOTHING\n", - "Green action: NODE_APPLICATION_EXECUTE\n", - "Green action: NODE_APPLICATION_EXECUTE\n", - "Blue reward:-1.0\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -759,21 +499,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 39\n", - "Red action: DONOTHING\n", - "Green action: NODE_APPLICATION_EXECUTE\n", - "Green action: DONOTHING\n", - "Blue reward:-0.32\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -794,49 +522,9 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 139, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 140, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 141, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 142, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 143, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 144, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 145, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 146, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 147, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 148, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 149, Red action: NODE_APPLICATION_EXECUTE, Blue reward:-0.32\n", - "step: 150, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 151, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 152, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 153, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 154, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 155, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 156, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 157, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 158, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 159, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 160, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 161, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 162, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 163, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 164, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 165, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 166, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 167, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 168, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 169, Red action: NODE_APPLICATION_EXECUTE, Blue reward:-0.32\n", - "step: 170, Red action: DONOTHING, Blue reward:-0.32\n", - "step: 171, Red action: DONOTHING, Blue reward:-0.32\n" - ] - } - ], + "outputs": [], "source": [ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", @@ -868,345 +556,19 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 2,\n", - " 'source_node_id': 7,\n", - " 'source_port': 1,\n", - " 'dest_node_id': 4,\n", - " 'dest_port': 1,\n", - " 'protocol': 3},\n", - " 6: {'position': 5,\n", - " 'permission': 2,\n", - " 'source_node_id': 8,\n", - " 'source_port': 1,\n", - " 'dest_node_id': 4,\n", - " 'dest_port': 1,\n", - " 'protocol': 3},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0}}" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "obs['ACL']" ] }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "router = env.game.simulation.network.get_node_by_hostname('router_1')" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "+------------------------------------------------------------------------------------------------------------+\n", - "| router_1 Access Control List |\n", - "+-------+--------+----------+---------------+------------------------+--------------+------------------------+\n", - "| Index | Action | Protocol | Src IP | Src Port | Dst IP | Dst Port |\n", - "+-------+--------+----------+---------------+------------------------+--------------+------------------------+\n", - "| 5 | DENY | TCP | 192.168.10.21 | ANY | 192.168.1.14 | ANY |\n", - "| 6 | DENY | TCP | 192.168.10.22 | ANY | 192.168.1.14 | ANY |\n", - "| 18 | PERMIT | ANY | ANY | 5432 (POSTGRES_SERVER) | ANY | 5432 (POSTGRES_SERVER) |\n", - "| 19 | PERMIT | ANY | ANY | 53 (DNS) | ANY | 53 (DNS) |\n", - "| 20 | PERMIT | ANY | ANY | 21 (FTP) | ANY | 21 (FTP) |\n", - "| 21 | PERMIT | ANY | ANY | 80 (HTTP) | ANY | 80 (HTTP) |\n", - "| 22 | PERMIT | ANY | ANY | 219 (ARP) | ANY | 219 (ARP) |\n", - "| 23 | PERMIT | ICMP | ANY | ANY | ANY | ANY |\n", - "| 24 | DENY | ANY | ANY | ANY | ANY | ANY |\n", - "+-------+--------+----------+---------------+------------------------+--------------+------------------------+\n" - ] - } - ], - "source": [ - "router.acl.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[(, 0.34),\n", - " (,\n", - " 0.33),\n", - " (,\n", - " 0.33)]" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "env.agent.reward_function.reward_components" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "client_1 = env.game.simulation.network.get_node_by_hostname('client_1')\n", - "client_2 = env.game.simulation.network.get_node_by_hostname('client_2')" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "client_1_browser = client_1.software_manager.software.get(\"WebBrowser\")\n", - "client_2_browser = client_2.software_manager.software.get(\"WebBrowser\")" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=),\n", - " BrowserHistoryItem(url='http://arcd.com/users/', status=<_HistoryItemStatus.LOADED: 'LOADED'>, response_code=)]" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "client_2_browser.history" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "client_1_browser.get_webpage()" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [], - "source": [ - "database_server = env.game.simulation.network.get_node_by_hostname('database_server')" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={'e63e23dc-c443-4434-822d-3c2c01cbfe1e': File(uuid='e63e23dc-c443-4434-822d-3c2c01cbfe1e', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '646a198e-6ac1-4aea-b526-7ca5c2c30dfc': File(uuid='646a198e-6ac1-4aea-b526-7ca5c2c30dfc', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '23df7aba-074e-4ffa-939a-d87ad1fe7af1': File(uuid='23df7aba-074e-4ffa-939a-d87ad1fe7af1', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={'e63e23dc-c443-4434-822d-3c2c01cbfe1e': File(uuid='e63e23dc-c443-4434-822d-3c2c01cbfe1e', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '646a198e-6ac1-4aea-b526-7ca5c2c30dfc': File(uuid='646a198e-6ac1-4aea-b526-7ca5c2c30dfc', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e'])), '23df7aba-074e-4ffa-939a-d87ad1fe7af1': File(uuid='23df7aba-074e-4ffa-939a-d87ad1fe7af1', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=True, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={'390c399a-c2ab-4d84-b98f-d5fc1f9114d2': File(uuid='390c399a-c2ab-4d84-b98f-d5fc1f9114d2', name='database.db', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, folder_id='e602b9a6-ea1c-4c52-9025-8512d9116b1d', folder_name='database', file_type=, sim_size=15360000, real=False, sim_path=None, sim_root=PosixPath('/home/cade/primaite/3.0.0b6/sessions/2024-02-07/10-58-18/simulation_output/database_server/fs'), folder=Folder(uuid='e602b9a6-ea1c-4c52-9025-8512d9116b1d', name='database', health_status=, visible_health_status=, previous_hash=None, revealed_to_red=False, sys_log=, deleted=False, files={...}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, deleted_files={...}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))}, scan_duration=3, scan_countdown=-1, red_scan_duration=3, red_scan_countdown=-1, restore_duration=3, restore_countdown=-1, original_file_uuids=['e63e23dc-c443-4434-822d-3c2c01cbfe1e']))" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "database_server.file_system.get_file('database', 'database.db')" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "database_server." - ] + "source": [] } ], "metadata": { From 76db5dbaa234e32404ff9f89f1cd63b71137be10 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 12:05:02 +0000 Subject: [PATCH 010/128] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc39a2b9..79cce02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Changed the red agent in the data manipulation scenario to randomly choose client 1 or client 2 to start its attack. - Changed the data manipulation scenario to include a second green agent on client 1. - Refactored actions and observations to be configurable via object name, instead of UUID. - Fixed a bug where ACL rules were not resetting on episode reset. From f7c1da31185cafaf1c01223c8bb137057f29bde0 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 12:06:30 +0000 Subject: [PATCH 011/128] Update MARL config. --- .../config/_package_data/example_config_2_rl_agents.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 993b3283..6d5b3602 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -74,7 +74,10 @@ agents: nodes: - node_ref: client_1 applications: - - application_ref: data_manipulation_bot + - 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 From 88f8e9cb42322a236d3718136afb52adb87ae1be Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 12:09:32 +0000 Subject: [PATCH 012/128] Add todo comment. --- src/primaite/simulator/system/applications/database_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index dfa5e445..2e0f4e3f 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -195,6 +195,7 @@ class DatabaseClient(Application): if connection_id is None: if self.connections: connection_id = list(self.connections.keys())[-1] + # TODO: if the most recent connection dies, it should be automatically cleared. else: connection_id = str(uuid4()) From 64b9ba3ecf2e1865e902917bd80d05bac70ab0bd Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 16:21:03 +0000 Subject: [PATCH 013/128] Make environment reset reinstantiate the game --- src/primaite/game/game.py | 5 +--- .../notebooks/training_example_sb3.ipynb | 4 +-- src/primaite/session/environment.py | 27 ++++++++++++------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 1fd0dc8b..091438ce 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -67,9 +67,6 @@ class PrimaiteGame: self.step_counter: int = 0 """Current timestep within the episode.""" - self.episode_counter: int = 0 - """Current episode number.""" - self.options: PrimaiteGameOptions """Special options that apply for the entire game.""" @@ -163,7 +160,7 @@ class PrimaiteGame: return True return False - def reset(self) -> None: + def reset(self) -> None: # TODO: deprecated - remove me """Reset the game, this will reset the simulation.""" self.episode_counter += 1 self.step_counter = 0 diff --git a/src/primaite/notebooks/training_example_sb3.ipynb b/src/primaite/notebooks/training_example_sb3.ipynb index e5085c5e..164142b2 100644 --- a/src/primaite/notebooks/training_example_sb3.ipynb +++ b/src/primaite/notebooks/training_example_sb3.ipynb @@ -38,7 +38,7 @@ "metadata": {}, "outputs": [], "source": [ - "gym = PrimaiteGymEnv(game=game)" + "gym = PrimaiteGymEnv(game_config=cfg)" ] }, { @@ -65,7 +65,7 @@ "metadata": {}, "outputs": [], "source": [ - "model.learn(total_timesteps=1000)\n" + "model.learn(total_timesteps=10)\n" ] }, { diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index a3831bc1..ad770f8f 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -18,11 +18,18 @@ class PrimaiteGymEnv(gymnasium.Env): assumptions about the agent list always having a list of length 1. """ - def __init__(self, game: PrimaiteGame): + def __init__(self, game_config: Dict): """Initialise the environment.""" super().__init__() - self.game: "PrimaiteGame" = game + self.game_config: Dict = game_config + """PrimaiteGame definition. This can be changed between episodes to enable curriculum learning.""" + self.game: PrimaiteGame = PrimaiteGame.from_config(self.game_config) + """Current game.""" self.agent: ProxyAgent = self.game.rl_agents[0] + """The agent within the game that is controlled by the RL algorithm.""" + + self.episode_counter: int = 0 + """Current episode number.""" def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]: """Perform a step in the environment.""" @@ -45,13 +52,13 @@ class PrimaiteGymEnv(gymnasium.Env): return next_obs, reward, terminated, truncated, info def _write_step_metadata_json(self, action: int, state: Dict, reward: int): - output_dir = SIM_OUTPUT.path / f"episode_{self.game.episode_counter}" / "step_metadata" + output_dir = SIM_OUTPUT.path / f"episode_{self.episode_counter}" / "step_metadata" output_dir.mkdir(parents=True, exist_ok=True) path = output_dir / f"step_{self.game.step_counter}.json" data = { - "episode": self.game.episode_counter, + "episode": self.episode_counter, "step": self.game.step_counter, "action": int(action), "reward": int(reward), @@ -63,10 +70,12 @@ class PrimaiteGymEnv(gymnasium.Env): def reset(self, seed: Optional[int] = None) -> Tuple[ObsType, Dict[str, Any]]: """Reset the environment.""" print( - f"Resetting environment, episode {self.game.episode_counter}, " + f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {self.game.rl_agents[0].reward_function.total_reward}" ) - self.game.reset() + self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.game_config) + self.agent = self.game.rl_agents[0] + self.episode_counter += 1 state = self.game.get_sim_state() self.game.update_agents(state) next_obs = self._get_obs() @@ -107,7 +116,7 @@ class PrimaiteRayEnv(gymnasium.Env): :type env_config: Dict[str, PrimaiteGame] """ self.env = PrimaiteGymEnv(game=PrimaiteGame.from_config(env_config["cfg"])) - self.env.game.episode_counter -= 1 + self.env.episode_counter -= 1 self.action_space = self.env.action_space self.observation_space = self.env.observation_space @@ -194,13 +203,13 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): return next_obs, rewards, terminateds, truncateds, infos def _write_step_metadata_json(self, actions: Dict, state: Dict, rewards: Dict): - output_dir = SIM_OUTPUT.path / f"episode_{self.game.episode_counter}" / "step_metadata" + output_dir = SIM_OUTPUT.path / f"episode_{self.episode_counter}" / "step_metadata" output_dir.mkdir(parents=True, exist_ok=True) path = output_dir / f"step_{self.game.step_counter}.json" data = { - "episode": self.game.episode_counter, + "episode": self.episode_counter, "step": self.game.step_counter, "actions": {agent_name: int(action) for agent_name, action in actions.items()}, "reward": rewards, From f82506023bcd978ff28e28a25491eb6b6facdee7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 16:29:27 +0000 Subject: [PATCH 014/128] Delete set_original_state method definitions --- src/primaite/game/game.py | 2 - src/primaite/simulator/core.py | 10 +--- src/primaite/simulator/domain/account.py | 13 ----- src/primaite/simulator/file_system/file.py | 9 ---- .../simulator/file_system/file_system.py | 11 ---- .../file_system/file_system_item_abc.py | 5 -- src/primaite/simulator/file_system/folder.py | 17 ------- src/primaite/simulator/network/container.py | 7 --- .../simulator/network/hardware/base.py | 50 ------------------- .../network/hardware/nodes/router.py | 48 ------------------ src/primaite/simulator/sim_container.py | 4 -- .../system/applications/application.py | 6 --- .../system/applications/database_client.py | 8 --- .../red_applications/data_manipulation_bot.py | 15 ------ .../applications/red_applications/dos_bot.py | 16 ------ .../system/applications/web_browser.py | 8 --- .../simulator/system/processes/process.py | 6 --- .../services/database/database_service.py | 13 ----- .../system/services/dns/dns_client.py | 7 --- .../system/services/dns/dns_server.py | 7 --- .../system/services/ftp/ftp_client.py | 7 --- .../system/services/ftp/ftp_server.py | 7 --- .../simulator/system/services/service.py | 6 --- .../system/services/web_server/web_server.py | 7 --- src/primaite/simulator/system/software.py | 19 ------- .../_simulator/_domain/test_account.py | 2 - .../_file_system/test_file_system.py | 1 - .../_simulator/_network/test_container.py | 1 - .../_red_applications/test_dos_bot.py | 2 - 29 files changed, 1 insertion(+), 313 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 091438ce..bd7ed2cd 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -386,6 +386,4 @@ class PrimaiteGame: else: _LOGGER.warning(f"agent type {agent_type} not found") - game.simulation.set_original_state() - return game diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 98a7e8db..e21ce9eb 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -153,8 +153,6 @@ class SimComponent(BaseModel): uuid: str """The component UUID.""" - _original_state: Dict = {} - def __init__(self, **kwargs): if not kwargs.get("uuid"): kwargs["uuid"] = str(uuid4()) @@ -162,15 +160,9 @@ class SimComponent(BaseModel): self._request_manager: RequestManager = self._init_request_manager() self._parent: Optional["SimComponent"] = None - # @abstractmethod - def set_original_state(self): - """Sets the original state.""" - pass - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - for key, value in self._original_state.items(): - self.__setattr__(key, value) + pass def _init_request_manager(self) -> RequestManager: """ diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index d9dad06a..186caf5b 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -42,19 +42,6 @@ class Account(SimComponent): "Account Type, currently this can be service account (used by apps) or user account." enabled: bool = True - def set_original_state(self): - """Sets the original state.""" - vals_to_include = { - "num_logons", - "num_logoffs", - "num_group_changes", - "username", - "password", - "account_type", - "enabled", - } - self._original_state = self.model_dump(include=vals_to_include) - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index 608a1d78..4cd5cdbb 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -73,15 +73,6 @@ class File(FileSystemItemABC): self.sys_log.info(f"Created file /{self.path} (id: {self.uuid})") - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting File ({self.path}) original state on node {self.sys_log.hostname}") - super().set_original_state() - vals_to_include = {"folder_id", "folder_name", "file_type", "sim_size", "real", "sim_path", "sim_root"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting File ({self.path}) state on node {self.sys_log.hostname}") diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index ee80587d..a7252a2d 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -34,17 +34,6 @@ class FileSystem(SimComponent): if not self.folders: self.create_folder("root") - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting FileSystem original state on node {self.sys_log.hostname}") - for folder in self.folders.values(): - folder.set_original_state() - # Capture a list of all 'original' file uuids - original_keys = list(self.folders.keys()) - vals_to_include = {"sim_root"} - self._original_state.update(self.model_dump(include=vals_to_include)) - self._original_state["original_folder_uuids"] = original_keys - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting FileSystem state on node {self.sys_log.hostname}") 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 c3e1426b..fbe5f4b3 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -85,11 +85,6 @@ class FileSystemItemABC(SimComponent): deleted: bool = False "If true, the FileSystemItem was deleted." - def set_original_state(self): - """Sets the original state.""" - vals_to_keep = {"name", "health_status", "visible_health_status", "previous_hash", "revealed_to_red", "deleted"} - self._original_state = self.model_dump(include=vals_to_keep) - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 13fdc597..39c3dad8 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -49,23 +49,6 @@ class Folder(FileSystemItemABC): self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})") - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting Folder ({self.name}) original state on node {self.sys_log.hostname}") - for file in self.files.values(): - file.set_original_state() - super().set_original_state() - vals_to_include = { - "scan_duration", - "scan_countdown", - "red_scan_duration", - "red_scan_countdown", - "restore_duration", - "restore_countdown", - } - self._original_state.update(self.model_dump(include=vals_to_include)) - self._original_state["original_file_uuids"] = list(self.files.keys()) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting Folder ({self.name}) state on node {self.sys_log.hostname}") diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 8989a60f..48205bbd 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -45,13 +45,6 @@ class Network(SimComponent): self._nx_graph = MultiGraph() - def set_original_state(self): - """Sets the original state.""" - for node in self.nodes.values(): - node.set_original_state() - for link in self.links.values(): - link.set_original_state() - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" for node in self.nodes.values(): diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 01dd736d..68f3816d 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -123,13 +123,6 @@ class NIC(SimComponent): _LOGGER.error(msg) raise ValueError(msg) - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"ip_address", "subnet_mask", "mac_address", "speed", "mtu", "wake_on_lan", "enabled"} - self._original_state = self.model_dump(include=vals_to_include) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" super().reset_component_for_episode(episode) @@ -349,14 +342,6 @@ class SwitchPort(SimComponent): kwargs["mac_address"] = generate_mac_address() super().__init__(**kwargs) - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"port_num", "mac_address", "speed", "mtu", "enabled"} - self._original_state = self.model_dump(include=vals_to_include) - super().set_original_state() - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -506,14 +491,6 @@ class Link(SimComponent): self.endpoint_b.connect_link(self) self.endpoint_up() - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"bandwidth", "current_load"} - self._original_state = self.model_dump(include=vals_to_include) - super().set_original_state() - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -1033,33 +1010,6 @@ class Node(SimComponent): self.arp.nics = self.nics self.session_manager.software_manager = self.software_manager self._install_system_software() - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - for software in self.software_manager.software.values(): - software.set_original_state() - - self.file_system.set_original_state() - - for nic in self.nics.values(): - nic.set_original_state() - - vals_to_include = { - "hostname", - "default_gateway", - "operating_state", - "revealed_to_red", - "start_up_duration", - "start_up_countdown", - "shut_down_duration", - "shut_down_countdown", - "is_resetting", - "node_scan_duration", - "node_scan_countdown", - "red_scan_countdown", - } - self._original_state = self.model_dump(include=vals_to_include) def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 9a34be0b..4b379be0 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -53,11 +53,6 @@ class ACLRule(SimComponent): rule_strings.append(f"{key}={value}") return ", ".join(rule_strings) - def set_original_state(self): - """Sets the original state.""" - vals_to_keep = {"action", "protocol", "src_ip_address", "src_port", "dst_ip_address", "dst_port"} - self._original_state = self.model_dump(include=vals_to_keep, exclude_none=True) - def describe_state(self) -> Dict: """ Describes the current state of the ACLRule. @@ -101,28 +96,6 @@ class AccessControlList(SimComponent): super().__init__(**kwargs) self._acl = [None] * (self.max_acl_rules - 1) - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - self.implicit_rule.set_original_state() - vals_to_keep = {"implicit_action", "max_acl_rules", "acl"} - self._original_state = self.model_dump(include=vals_to_keep, exclude_none=True) - - for i, rule in enumerate(self._acl): - if not rule: - continue - self._default_config[i] = {"action": rule.action.name} - if rule.src_ip_address: - self._default_config[i]["src_ip"] = str(rule.src_ip_address) - if rule.dst_ip_address: - self._default_config[i]["dst_ip"] = str(rule.dst_ip_address) - if rule.src_port: - self._default_config[i]["src_port"] = rule.src_port.name - if rule.dst_port: - self._default_config[i]["dst_port"] = rule.dst_port.name - if rule.protocol: - self._default_config[i]["protocol"] = rule.protocol.name def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" @@ -389,11 +362,6 @@ class RouteEntry(SimComponent): metric: float = 0.0 "The cost metric for this route. Default is 0.0." - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"address", "subnet_mask", "next_hop_ip_address", "metric"} - self._original_values = self.model_dump(include=vals_to_include) - def describe_state(self) -> Dict: """ Describes the current state of the RouteEntry. @@ -426,11 +394,6 @@ class RouteTable(SimComponent): default_route: Optional[RouteEntry] = None sys_log: SysLog - def set_original_state(self): - """Sets the original state.""" - super().set_original_state() - self._original_state["routes_orig"] = self.routes - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" self.routes.clear() @@ -808,16 +771,6 @@ class Router(Node): self.arp.nics = self.nics self.icmp.arp = self.arp - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - self.acl.set_original_state() - self.route_table.set_original_state() - super().set_original_state() - vals_to_include = {"num_ports"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" self.arp.clear() @@ -987,7 +940,6 @@ class Router(Node): nic.ip_address = ip_address nic.subnet_mask = subnet_mask self.sys_log.info(f"Configured port {port} with ip_address={ip_address}/{nic.ip_network.prefixlen}") - self.set_original_state() def enable_port(self, port: int): """ diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 896861e6..18ed894c 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -21,10 +21,6 @@ class Simulation(SimComponent): super().__init__(**kwargs) - def set_original_state(self): - """Sets the original state.""" - self.network.set_original_state() - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" self.network.reset_component_for_episode(episode) diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 322ac808..513606a9 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -38,12 +38,6 @@ class Application(IOSoftware): def __init__(self, **kwargs): super().__init__(**kwargs) - def set_original_state(self): - """Sets the original state.""" - super().set_original_state() - vals_to_include = {"operating_state", "execution_control_status", "num_executions", "groups"} - self._original_state.update(self.model_dump(include=vals_to_include)) - @abstractmethod def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 2e0f4e3f..d05472d4 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -30,14 +30,6 @@ class DatabaseClient(Application): kwargs["port"] = Port.POSTGRES_SERVER kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) - self.set_original_state() - - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting DatabaseClient WebServer original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"server_ip_address", "server_password", "connected", "_query_success_tracker"} - self._original_state.update(self.model_dump(include=vals_to_include)) def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" 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 a844f059..bd4048c4 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 @@ -49,21 +49,6 @@ class DataManipulationBot(DatabaseClient): super().__init__(**kwargs) self.name = "DataManipulationBot" - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting DataManipulationBot original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = { - "server_ip_address", - "payload", - "server_password", - "port_scan_p_of_success", - "data_manipulation_p_of_success", - "attack_stage", - "repeat", - } - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting DataManipulationBot state on node {self.software_manager.node.hostname}") 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 dfc48dd3..d4ea1a20 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -57,22 +57,6 @@ class DoSBot(DatabaseClient, Application): self.name = "DoSBot" self.max_sessions = 1000 # override normal max sessions - def set_original_state(self): - """Set the original state of the Denial of Service Bot.""" - _LOGGER.debug(f"Setting {self.name} original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = { - "target_ip_address", - "target_port", - "payload", - "repeat", - "attack_stage", - "max_sessions", - "port_scan_p_of_success", - "dos_intensity", - } - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting {self.name} state on node {self.software_manager.node.hostname}") diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index eef0ed5d..f1dbe3ef 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -47,16 +47,8 @@ class WebBrowser(Application): kwargs["port"] = Port.HTTP super().__init__(**kwargs) - self.set_original_state() self.run() - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting WebBrowser original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"target_url", "domain_name_ip_address", "latest_response"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting WebBrowser state on node {self.software_manager.node.hostname}") diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index b753e3ad..458a6b5c 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -24,12 +24,6 @@ class Process(Software): operating_state: ProcessOperatingState "The current operating state of the Process." - def set_original_state(self): - """Sets the original state.""" - super().set_original_state() - vals_to_include = {"operating_state"} - self._original_state.update(self.model_dump(include=vals_to_include)) - @abstractmethod def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index d75b4424..4159c87c 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -40,19 +40,6 @@ class DatabaseService(Service): super().__init__(**kwargs) self._create_db_file() - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting DatabaseService original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = { - "password", - "connections", - "backup_server_ip", - "latest_backup_directory", - "latest_backup_file_name", - } - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug("Resetting DatabaseService original state on node {self.software_manager.node.hostname}") diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 2d3879ff..3c034705 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -29,13 +29,6 @@ class DNSClient(Service): super().__init__(**kwargs) self.start() - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting DNSClient original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"dns_server"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" self.dns_cache.clear() diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 8decf7e9..eab94766 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -28,13 +28,6 @@ class DNSServer(Service): super().__init__(**kwargs) self.start() - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting DNSServer original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"dns_table"} - self._original_state["dns_table_orig"] = self.model_dump(include=vals_to_include)["dns_table"] - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" self.dns_table.clear() diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 39bc57f0..457eaea9 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -27,13 +27,6 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting FTPClient original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"connected"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting FTPClient state on node {self.software_manager.node.hostname}") diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index a82b0919..9534a5e9 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -27,13 +27,6 @@ class FTPServer(FTPServiceABC): super().__init__(**kwargs) self.start() - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting FTPServer original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"server_password"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting FTPServer state on node {self.software_manager.node.hostname}") diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 162678a0..4102657c 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -78,12 +78,6 @@ class Service(IOSoftware): """ return super().receive(payload=payload, session_id=session_id, **kwargs) - def set_original_state(self): - """Sets the original state.""" - super().set_original_state() - vals_to_include = {"operating_state", "restart_duration", "restart_countdown"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request("scan", RequestType(func=lambda request, context: self.scan())) 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 eaea6bb1..5888e72a 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -23,13 +23,6 @@ class WebServer(Service): last_response_status_code: Optional[HttpStatusCode] = None - def set_original_state(self): - """Sets the original state.""" - _LOGGER.debug(f"Setting WebServer original state on node {self.software_manager.node.hostname}") - super().set_original_state() - vals_to_include = {"last_response_status_code"} - self._original_state.update(self.model_dump(include=vals_to_include)) - def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" _LOGGER.debug(f"Resetting WebServer state on node {self.software_manager.node.hostname}") diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 662db08e..1fb8c989 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -96,19 +96,6 @@ class Software(SimComponent): _patching_countdown: Optional[int] = None "Current number of ticks left to patch the software." - def set_original_state(self): - """Sets the original state.""" - vals_to_include = { - "name", - "health_state_actual", - "health_state_visible", - "criticality", - "patching_count", - "scanning_count", - "revealed_to_red", - } - self._original_state = self.model_dump(include=vals_to_include) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request( @@ -245,12 +232,6 @@ class IOSoftware(Software): _connections: Dict[str, Dict] = {} "Active connections." - def set_original_state(self): - """Sets the original state.""" - super().set_original_state() - vals_to_include = {"installing_count", "max_sessions", "tcp", "udp", "port"} - self._original_state.update(self.model_dump(include=vals_to_include)) - @abstractmethod def describe_state(self) -> Dict: """ diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py index 01ad3871..695b15dd 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py @@ -7,7 +7,6 @@ from primaite.simulator.domain.account import Account, AccountType @pytest.fixture(scope="function") def account() -> Account: acct = Account(username="Jake", password="totally_hashed_password", account_type=AccountType.USER) - acct.set_original_state() return acct @@ -39,7 +38,6 @@ def test_original_state(account): account.log_on() account.log_off() account.disable() - account.set_original_state() account.log_on() state = account.describe_state() 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 9366d173..2fe3f04c 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 @@ -189,7 +189,6 @@ def test_reset_file_system(file_system): # file and folder that existed originally file_system.create_file(file_name="test_file.zip") file_system.create_folder(folder_name="test_folder") - file_system.set_original_state() # create a new file file_system.create_file(file_name="new_file.txt") diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index 7667a59f..994e5a45 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -33,7 +33,6 @@ def network(example_network) -> Network: assert len(example_network.computers) is 2 assert len(example_network.servers) is 2 - example_network.set_original_state() example_network.show() return example_network 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 71489171..da29a439 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 @@ -22,7 +22,6 @@ def dos_bot() -> DoSBot: dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") dos_bot.configure(target_ip_address=IPv4Address("192.168.0.1")) - dos_bot.set_original_state() return dos_bot @@ -51,7 +50,6 @@ def test_dos_bot_reset(dos_bot): dos_bot.configure( target_ip_address=IPv4Address("192.168.1.1"), target_port=Port.HTTP, payload="payload", repeat=True ) - dos_bot.set_original_state() dos_bot.reset_component_for_episode(episode=1) # should reset to the configured value assert dos_bot.target_ip_address == IPv4Address("192.168.1.1") From 72f4cc0a5073e79f7b9734b27739b3264513c6f8 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 20 Feb 2024 16:56:25 +0000 Subject: [PATCH 015/128] Remove reset methods from most classes --- src/primaite/simulator/file_system/file.py | 5 --- .../simulator/file_system/file_system.py | 26 ----------- src/primaite/simulator/file_system/folder.py | 26 ----------- .../simulator/network/hardware/base.py | 9 ---- .../network/hardware/nodes/router.py | 45 ++++++------------- .../system/applications/database_client.py | 6 --- .../red_applications/data_manipulation_bot.py | 5 --- .../applications/red_applications/dos_bot.py | 5 --- .../system/applications/web_browser.py | 8 ---- .../services/database/database_service.py | 6 --- .../system/services/dns/dns_client.py | 5 --- .../system/services/dns/dns_server.py | 7 --- .../system/services/ftp/ftp_client.py | 5 --- .../system/services/ftp/ftp_server.py | 6 --- .../system/services/ntp/ntp_client.py | 13 +----- .../system/services/ntp/ntp_server.py | 10 ----- .../system/services/web_server/web_server.py | 5 --- 17 files changed, 16 insertions(+), 176 deletions(-) diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index 4cd5cdbb..d9b02e8e 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -73,11 +73,6 @@ class File(FileSystemItemABC): self.sys_log.info(f"Created file /{self.path} (id: {self.uuid})") - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting File ({self.path}) state on node {self.sys_log.hostname}") - super().reset_component_for_episode(episode) - @property def path(self) -> str: """ diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index a7252a2d..8fd4e5d7 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -34,32 +34,6 @@ class FileSystem(SimComponent): if not self.folders: self.create_folder("root") - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting FileSystem state on node {self.sys_log.hostname}") - # Move any 'original' folder that have been deleted back to folders - original_folder_uuids = self._original_state["original_folder_uuids"] - for uuid in original_folder_uuids: - if uuid in self.deleted_folders: - folder = self.deleted_folders[uuid] - self.deleted_folders.pop(uuid) - self.folders[uuid] = folder - - # Clear any other deleted folders that aren't original (have been created by agent) - self.deleted_folders.clear() - - # Now clear all non-original folders created by agent - current_folder_uuids = list(self.folders.keys()) - for uuid in current_folder_uuids: - if uuid not in original_folder_uuids: - folder = self.folders[uuid] - self.folders.pop(uuid) - - # Now reset all remaining folders - for folder in self.folders.values(): - folder.reset_component_for_episode(episode) - super().reset_component_for_episode(episode) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 39c3dad8..771dc7a0 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -49,32 +49,6 @@ class Folder(FileSystemItemABC): self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})") - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting Folder ({self.name}) state on node {self.sys_log.hostname}") - # Move any 'original' file that have been deleted back to files - original_file_uuids = self._original_state["original_file_uuids"] - for uuid in original_file_uuids: - if uuid in self.deleted_files: - file = self.deleted_files[uuid] - self.deleted_files.pop(uuid) - self.files[uuid] = file - - # Clear any other deleted files that aren't original (have been created by agent) - self.deleted_files.clear() - - # Now clear all non-original files created by agent - current_file_uuids = list(self.files.keys()) - for uuid in current_file_uuids: - if uuid not in original_file_uuids: - file = self.files[uuid] - self.files.pop(uuid) - - # Now reset all remaining files - for file in self.files.values(): - file.reset_component_for_episode(episode) - super().reset_component_for_episode(episode) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request( diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 68f3816d..67ac42c8 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1015,15 +1015,6 @@ class Node(SimComponent): """Reset the original state of the SimComponent.""" super().reset_component_for_episode(episode) - # Reset ARP Cache - self.arp.clear() - - # Reset ICMP - self.icmp.clear() - - # Reset Session Manager - self.session_manager.clear() - # Reset File System self.file_system.reset_component_for_episode(episode) diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 4b379be0..aa154ad9 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -84,9 +84,7 @@ class AccessControlList(SimComponent): implicit_action: ACLAction implicit_rule: ACLRule max_acl_rules: int = 25 - _acl: List[Optional[ACLRule]] = [None] * 24 - _default_config: Dict[int, dict] = {} - """Config dict describing how the ACL list should look at episode start""" + _acl: List[Optional[ACLRule]] = [None] * 24 # TODO: this ignores the max_acl_rules and assumes it's default def __init__(self, **kwargs) -> None: if not kwargs.get("implicit_action"): @@ -97,26 +95,6 @@ class AccessControlList(SimComponent): super().__init__(**kwargs) self._acl = [None] * (self.max_acl_rules - 1) - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - self.implicit_rule.reset_component_for_episode(episode) - super().reset_component_for_episode(episode) - self._reset_rules_to_default() - - def _reset_rules_to_default(self) -> None: - """Clear all ACL rules and set them to the default rules config.""" - self._acl = [None] * (self.max_acl_rules - 1) - for r_num, r_cfg in self._default_config.items(): - self.add_rule( - action=ACLAction[r_cfg["action"]], - src_port=None if not (p := r_cfg.get("src_port")) else Port[p], - dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], - protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], - src_ip_address=r_cfg.get("src_ip"), - dst_ip_address=r_cfg.get("dst_ip"), - position=r_num, - ) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() @@ -394,12 +372,6 @@ class RouteTable(SimComponent): default_route: Optional[RouteEntry] = None sys_log: SysLog - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - self.routes.clear() - self.routes = self._original_state["routes_orig"] - super().reset_component_for_episode(episode) - def describe_state(self) -> Dict: """ Describes the current state of the RouteTable. @@ -1040,7 +1012,18 @@ class Router(Node): ip_address=port_cfg["ip_address"], subnet_mask=port_cfg["subnet_mask"], ) + + # Add the router's default ACL rules from the config. if "acl" in cfg: - new.acl._default_config = cfg["acl"] # save the config to allow resetting - new.acl._reset_rules_to_default() # read the config and apply rules + for r_num, r_cfg in cfg["acl"].items(): + new.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) + return new diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index d05472d4..25730c38 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -31,12 +31,6 @@ class DatabaseClient(Application): kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting DataBaseClient state on node {self.software_manager.node.hostname}") - super().reset_component_for_episode(episode) - self._query_success_tracker.clear() - def describe_state(self) -> Dict: """ Describes the current state of the ACLRule. 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 bd4048c4..5fe951b7 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 @@ -49,11 +49,6 @@ class DataManipulationBot(DatabaseClient): super().__init__(**kwargs) self.name = "DataManipulationBot" - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting DataManipulationBot state on node {self.software_manager.node.hostname}") - super().reset_component_for_episode(episode) - def _init_request_manager(self) -> RequestManager: rm = super()._init_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 d4ea1a20..9dac6b25 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -57,11 +57,6 @@ class DoSBot(DatabaseClient, Application): self.name = "DoSBot" self.max_sessions = 1000 # override normal max sessions - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting {self.name} state on node {self.software_manager.node.hostname}") - super().reset_component_for_episode(episode) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index f1dbe3ef..6f2c479c 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -49,11 +49,6 @@ class WebBrowser(Application): super().__init__(**kwargs) self.run() - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting WebBrowser state on node {self.software_manager.node.hostname}") - super().reset_component_for_episode(episode) - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request( @@ -72,9 +67,6 @@ class WebBrowser(Application): state["history"] = [hist_item.state() for hist_item in self.history] return state - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - def get_webpage(self, url: Optional[str] = None) -> bool: """ Retrieve the webpage. diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 4159c87c..5425ce75 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -40,12 +40,6 @@ class DatabaseService(Service): super().__init__(**kwargs) self._create_db_file() - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug("Resetting DatabaseService original state on node {self.software_manager.node.hostname}") - self.clear_connections() - super().reset_component_for_episode(episode) - def configure_backup(self, backup_server: IPv4Address): """ Set up the database backup. diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 3c034705..967af6b2 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -29,11 +29,6 @@ class DNSClient(Service): super().__init__(**kwargs) self.start() - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - self.dns_cache.clear() - super().reset_component_for_episode(episode) - def describe_state(self) -> Dict: """ Describes the current state of the software. diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index eab94766..4d0ebbb8 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -28,13 +28,6 @@ class DNSServer(Service): super().__init__(**kwargs) self.start() - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - self.dns_table.clear() - for key, value in self._original_state["dns_table_orig"].items(): - self.dns_table[key] = value - super().reset_component_for_episode(episode) - def describe_state(self) -> Dict: """ Describes the current state of the software. diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 457eaea9..7c334ced 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -27,11 +27,6 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting FTPClient state on node {self.software_manager.node.hostname}") - super().reset_component_for_episode(episode) - def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ Process the command in the FTP Packet. diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 9534a5e9..c5330de2 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -27,12 +27,6 @@ class FTPServer(FTPServiceABC): super().__init__(**kwargs) self.start() - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting FTPServer state on node {self.software_manager.node.hostname}") - self.clear_connections() - super().reset_component_for_episode(episode) - def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: """ Process the command in the FTP Packet. diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index e8c3d0cb..5e4ae53a 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -1,6 +1,6 @@ from datetime import datetime from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Dict, List, Optional from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket @@ -49,21 +49,12 @@ class NTPClient(Service): state = super().describe_state() return state - def reset_component_for_episode(self, episode: int): - """ - Resets the Service component for a new episode. - - This method ensures the Service is ready for a new episode, including resetting any - stateful properties or statistics, and clearing any message queues. - """ - pass - def send( self, payload: NTPPacket, session_id: Optional[str] = None, dest_ip_address: IPv4Address = None, - dest_port: [Port] = Port.NTP, + dest_port: List[Port] = Port.NTP, **kwargs, ) -> bool: """Requests NTP data from NTP server. diff --git a/src/primaite/simulator/system/services/ntp/ntp_server.py b/src/primaite/simulator/system/services/ntp/ntp_server.py index 0a66384a..29a320f6 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_server.py +++ b/src/primaite/simulator/system/services/ntp/ntp_server.py @@ -34,16 +34,6 @@ class NTPServer(Service): state = super().describe_state() return state - def reset_component_for_episode(self, episode: int): - """ - Resets the Service component for a new episode. - - This method ensures the Service is ready for a new episode, including - resetting any stateful properties or statistics, and clearing any message - queues. - """ - pass - def receive( self, payload: NTPPacket, 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 5888e72a..5e4a6f6e 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -23,11 +23,6 @@ class WebServer(Service): last_response_status_code: Optional[HttpStatusCode] = None - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" - _LOGGER.debug(f"Resetting WebServer state on node {self.software_manager.node.hostname}") - super().reset_component_for_episode(episode) - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. From deb7a3aa9d066ae4b25aa995628a9ac7d33e3c34 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Wed, 21 Feb 2024 14:49:59 +0000 Subject: [PATCH 016/128] #2257: massive docs addition for config file --- docs/_static/firewall_acl.png | Bin 0 -> 36036 bytes docs/_static/switched_p2p_network.png | Bin 0 -> 9178 bytes docs/source/config.rst | 19 ++ docs/source/configuration/agents.rst | 181 ++++++++++-- docs/source/configuration/game.rst | 38 +++ docs/source/configuration/io_settings.rst | 81 +++++- docs/source/configuration/simulation.rst | 89 +++++- .../common/common_host_node_attributes.rst | 41 +++ .../common/common_network_node_attributes.rst | 49 ++++ .../nodes/common/common_node_attributes.rst | 13 + .../nodes/common/node_type_list.rst | 18 ++ .../simulation/nodes/computer.rst | 39 +++ .../simulation/nodes/firewall.rst | 258 ++++++++++++++++++ .../configuration/simulation/nodes/router.rst | 125 +++++++++ .../configuration/simulation/nodes/server.rst | 39 +++ .../configuration/simulation/nodes/switch.rst | 37 +++ .../simulation/software/applications.rst | 10 + .../simulation/software/services.rst | 10 + docs/source/configuration/training_config.rst | 50 ++++ .../config/_package_data/example_config.yaml | 20 +- src/primaite/game/agent/actions.py | 4 +- src/primaite/game/agent/rewards.py | 5 +- src/primaite/game/game.py | 10 +- .../hardware/nodes/network/firewall.py | 12 +- .../network/hardware/nodes/network/router.py | 8 +- .../network/transmission/network_layer.py | 9 +- .../network/transmission/transport_layer.py | 6 +- tests/assets/configs/dmz_network.yaml | 2 +- 28 files changed, 1101 insertions(+), 72 deletions(-) create mode 100644 docs/_static/firewall_acl.png create mode 100644 docs/_static/switched_p2p_network.png create mode 100644 docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst create mode 100644 docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst create mode 100644 docs/source/configuration/simulation/nodes/common/common_node_attributes.rst create mode 100644 docs/source/configuration/simulation/nodes/common/node_type_list.rst create mode 100644 docs/source/configuration/simulation/nodes/computer.rst create mode 100644 docs/source/configuration/simulation/nodes/firewall.rst create mode 100644 docs/source/configuration/simulation/nodes/router.rst create mode 100644 docs/source/configuration/simulation/nodes/server.rst create mode 100644 docs/source/configuration/simulation/nodes/switch.rst create mode 100644 docs/source/configuration/simulation/software/applications.rst create mode 100644 docs/source/configuration/simulation/software/services.rst diff --git a/docs/_static/firewall_acl.png b/docs/_static/firewall_acl.png new file mode 100644 index 0000000000000000000000000000000000000000..1cdd25263cf0817dde59ae02124ab17879197237 GIT binary patch literal 36036 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 for a list of ports. + +``protocols`` +------------- + +A list of protocols that the Reinforcement Learning agent(s) are able to see in the observation space. + +See :ref:`List of IPProtocols ` for a list of protocols. diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index 11d044bb..96cc28fe 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -7,20 +7,83 @@ =============== This section configures how PrimAITE saves data during simulation and training. -**save_final_model**: Only used if training with PrimaiteSession, if true, the policy will be saved after the final training iteration. +``io_settings`` hierarchy +------------------------- -**save_checkpoints**: Only used if training with PrimaiteSession, if true, the policy will be saved periodically during training. +.. code-block:: yaml -**checkpoint_interval**: Only used if training with PrimaiteSession and if ``save_checkpoints`` is true. Defines how often to save the policy during training. + io_settings: + save_final_model: True + save_checkpoints: False + checkpoint_interval: 10 + # save_logs: True + # save_transactions: False + # save_tensorboard_logs: False + save_step_metadata: False + save_pcap_logs: False + save_sys_logs: False -**save_logs**: *currently unused*. +``save_final_model`` +-------------------- -**save_transactions**: *currently unused*. +Optional. Default value is ``True``. -**save_tensorboard_logs**: *currently unused*. +Only used if training with PrimaiteSession. +If ``True``, the policy will be saved after the final training iteration. -**save_step_metadata**: Whether to save the RL agents' action, environment state, and other data at every single step. -**save_pcap_logs**: Whether to save pcap files of all network traffic during the simulation. +``save_checkpoints`` +-------------------- -**save_sys_logs**: Whether to save system logs from all nodes during the simulation. +Optional. Default value is ``False``. + +Only used if training with PrimaiteSession. +If ``True``, the policy will be saved periodically during training. + + +``checkpoint_interval`` +----------------------- + +Optional. Default value is ``10``. + +Only used if training with PrimaiteSession and if ``save_checkpoints`` is ``True``. +Defines how often to save the policy during training. + + +``save_logs`` +------------- + +*currently unused*. + +``save_transactions`` +--------------------- + +*currently unused*. + +``save_tensorboard_logs`` +------------------------- + +*currently unused*. + +``save_step_metadata`` +---------------------- + +Optional. Default value is ``False``. + +If ``True``, The RL agent(s) actions, environment states and other data will be saved at every single step. + + +``save_pcap_logs`` +------------------ + +Optional. Default value is ``False``. + +If ``True``, then the pcap files which contain all network traffic during the simulation will be saved. + + +``save_sys_logs`` +----------------- + +Optional. Default value is ``False``. + +If ``True``, then the log files which contain all node actions during the simulation will be saved. diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index eb13e2be..d8497212 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -9,6 +9,17 @@ In this section the network layout is defined. This part of the config follows a At the top level of the network are ``nodes`` and ``links``. +e.g. + +.. code-block:: yaml + + simulation: + network: + nodes: + ... + links: + ... + **nodes:** * ``type``: one of ``router``, ``switch``, ``computer``, or ``server``, this affects what other sub-options should be defined. * ``hostname`` - a non-unique name used for logging and outputs. @@ -19,9 +30,75 @@ At the top level of the network are ``nodes`` and ``links``. * ``applications`` (computer and servers only): Similar to services. A list of application to install on the node. * ``network_interfaces`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``. -**links:** - * ``ref``: unique identifier for this link - * ``endpoint_a_ref``: Reference to the node at the first end of the link - * ``endpoint_a_port``: The ethernet port or switch port index of the second node - * ``endpoint_b_ref``: Reference to the node at the second end of the link - * ``endpoint_b_port``: The ethernet port or switch port index on the second node +``nodes`` +--------- + +This is where the list of nodes are defined. Some items will differ according to the node type, however, there will be common items such as a node's reference (which is used by the agent), the node's ``type`` and ``hostname`` + +To see the configuration for these nodes, refer to the following: + +.. toctree:: + :maxdepth: 1 + + simulation/nodes/computer.rst + simulation/nodes/firewall.rst + simulation/nodes/router.rst + simulation/nodes/server.rst + simulation/nodes/switch.rst + +``links`` +--------- + +This is where the links between the nodes are formed. + +e.g. + +In order to recreate the network below, we will need to create 2 links: + +- a link from computer_1 to the switch +- a link from computer_2 to the switch + +.. image:: ../../_static/switched_p2p_network.png + +this results in: + +.. code-block:: yaml + + links: + - ref: computer_1___switch + endpoint_a_ref: computer_1 + endpoint_a_port: 1 # port 1 on computer_1 + endpoint_b_ref: switch + endpoint_b_port: 1 # port 1 on switch + - ref: computer_2___switch + endpoint_a_ref: computer_2 + endpoint_a_port: 1 # port 1 on computer_2 + endpoint_b_ref: switch + endpoint_b_port: 2 # port 2 on switch + +``ref`` +^^^^^^^ + +The human readable name for the link. Not used in code, however is useful for a human to understand what the link is for. + +``endpoint_a_ref`` +^^^^^^^^^^^^^^^^^^ + +The name of the node which must be connected. + +``endpoint_a_port`` +^^^^^^^^^^^^^^^^^^^ + +The port on ``endpoint_a_ref`` which is to be connected to ``endpoint_b_port``. +This accepts an integer value e.g. if port 1 is to be connected, the configuration should be ``endpoint_a_port: 1`` + +``endpoint_b_ref`` +^^^^^^^^^^^^^^^^^^ + +The name of the node which must be connected. + +``endpoint_b_port`` +^^^^^^^^^^^^^^^^^^^ + +The port on ``endpoint_b_ref`` which is to be connected to ``endpoint_a_port``. +This accepts an integer value e.g. if port 1 is to be connected, the configuration should be ``endpoint_b_port: 1`` 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 new file mode 100644 index 00000000..265c7106 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/common/common_host_node_attributes.rst @@ -0,0 +1,41 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``ip_address`` +^^^^^^^^^^^^^^ + +The IP address of the |NODE| in the network. + +``subnet_mask`` +^^^^^^^^^^^^^^^ + +Optional. Default value is ``255.255.255.0``. + +The subnet mask for the |NODE| to use. + +``default_gateway`` +^^^^^^^^^^^^^^^^^^^ + +The IP address that the |NODE| will use as the default gateway. Typically, this is the IP address of the closest router that the |NODE| is connected to. + +``dns_server`` +^^^^^^^^^^^^^^ + +Optional. Default value is ``None`` + +The IP address of the node which holds an instance of the DNS server. Some applications may use a domain name e.g. the WebBrowser (TODO: WebBrowser page) + +``applications`` +^^^^^^^^^^^^^^^^ + +A list of applications which are not considered system software that need to be installed on the |NODE|. + +See :ref:`Applications ` + +``services`` +^^^^^^^^^^^^ + +A list of services which are not considered system software that need to be installed on the |NODE|. + +See :ref:`Services ` 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 new file mode 100644 index 00000000..83007145 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/common/common_network_node_attributes.rst @@ -0,0 +1,49 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``routes`` +---------- + +A list of routes which tells the |NODE| where to forward the packet to depending on the target IP address. + +e.g. + +.. code-block:: yaml + + nodes: + - ref: node + ... + routes: + - address: 192.168.0.10 + subnet_mask: 255.255.255.0 + next_hop_ip_address: 192.168.1.1 + metric: 0 + +``address`` +""""""""""" + +The target IP address for the route. If the packet destination IP address matches this, the router will route the packet according to the ``next_hop_ip_address``. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``subnet_mask`` +""""""""""""""" + +Optional. Default value is ``255.255.255.0``. + +The subnet mask setting for the route. + +``next_hop_ip_address`` +""""""""""""""""""""""" + +The IP address of the next hop IP address that the packet will follow if the address matches the packet's destination IP address. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``metric`` +"""""""""" + +Optional. Default value is ``0``. This value accepts floats. + +The cost or distance of a route. The higher the value, the more cost or distance is attributed to the route. diff --git a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst new file mode 100644 index 00000000..c1523518 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -0,0 +1,13 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``ref`` +------- + +Human readable name used as reference for the |NODE|. Not used in code. + +``hostname`` +------------ + +The hostname of the |NODE|. This will be used to reference the |NODE|. diff --git a/docs/source/configuration/simulation/nodes/common/node_type_list.rst b/docs/source/configuration/simulation/nodes/common/node_type_list.rst new file mode 100644 index 00000000..ceee8207 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/common/node_type_list.rst @@ -0,0 +1,18 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``type`` +-------- + +The type of node to add. + +Available options are: + +- ``computer`` +- ``firewall`` +- ``router`` +- ``server`` +- ``switch`` + +To create a |NODE|, type must be |NODE_TYPE|. diff --git a/docs/source/configuration/simulation/nodes/computer.rst b/docs/source/configuration/simulation/nodes/computer.rst new file mode 100644 index 00000000..bbdf087d --- /dev/null +++ b/docs/source/configuration/simulation/nodes/computer.rst @@ -0,0 +1,39 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _computer_configuration: + +``computer`` +============ + +A basic representation of a computer within the simulation. + +See :py:mod:`primaite.simulator.network.hardware.nodes.host.computer.Computer` + +example computer +---------------- + +.. code-block:: yaml + + nodes: + - ref: client_1 + hostname: client_1 + type: computer + ip_address: 192.168.0.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.0.1 + dns_server: 192.168.1.10 + applications: + ... + services: + ... + +.. include:: common/common_node_attributes.rst + +.. include:: common/node_type_list.rst + +.. include:: common/common_host_node_attributes.rst + +.. |NODE| replace:: computer +.. |NODE_TYPE| replace:: ``computer`` diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst new file mode 100644 index 00000000..b1e4e5e1 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -0,0 +1,258 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _firewall_configuration: + +``firewall`` +============ + +A basic representation of a network router within the simulation. + +The firewall is similar to how :ref:`Router ` works, with the difference being how firewall has specific ACL rules for inbound and outbound traffic as well as firewall being limited to 3 ports. + +See :py:mod:`primaite.simulator.network.hardware.nodes.network.firewall.Firewall` + +example firewall +---------------- + +.. code-block:: yaml + + nodes: + - ref: firewall + hostname: firewall + type: firewall + start_up_duration: 0 + shut_down_duration: 0 + ports: + external_port: # port 1 + ip_address: 192.168.20.1 + subnet_mask: 255.255.255.0 + internal_port: # port 2 + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + dmz_port: # port 3 + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + acl: + internal_inbound_acl: + ... + internal_outbound_acl: + ... + dmz_inbound_acl: + ... + dmz_outbound_acl: + ... + external_inbound_acl: + ... + external_outbound_acl: + ... + routes: + ... + +.. include:: common/common_node_attributes.rst + +.. include:: common/node_type_list.rst + +``ports`` +--------- + +The firewall node only has 3 ports. These specifically are: + +- ``external_port`` (port 1) +- ``internal_port`` (port 2) +- ``dmz_port`` (port 3) (can be optional) + +The ports should be defined with an ip address and subnet mask e.g. + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + ports: + external_port: # port 1 + ip_address: 192.168.20.1 + subnet_mask: 255.255.255.0 + internal_port: # port 2 + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + dmz_port: # port 3 + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + +``ip_address`` +"""""""""""""" + +The IP address for the given port. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``subnet_mask`` +""""""""""""""" + +Optional. Default value is ``255.255.255.0``. + +The subnet mask setting for the port. + +``acl`` +------- + +There are 6 ACLs that can be defined for a firewall + +- ``internal_inbound_acl`` for traffic going towards the internal network +- ``internal_outbound_acl`` for traffic coming from the internal network +- ``dmz_inbound_acl`` for traffic going towards the dmz network +- ``dmz_outbound_acl`` for traffic coming from the dmz network +- ``external_inbound_acl`` for traffic coming from the external network +- ``external_outbound_acl`` for traffic going towards the external network + +.. image:: ../../../../_static/firewall_acl.png + +By default, ``external_inbound_acl`` and ``external_outbound_acl`` will permit any traffic through. + +See :py:mod:`primaite.simulator.network.hardware.nodes.network.router.AccessControlList` + +See :ref:`List of Ports ` for a list of ports. + +``internal_inbound_acl`` +"""""""""""""""""""""""" + +ACL rules for packets that have a destination IP address in what is considered the internal network. + +example: + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + acl: + internal_inbound_acl: + 22: # position 22 on ACL list + action: PERMIT # allow packets that + src_port: ARP # are emitted from the ARP port + dst_port: ARP # are going towards an ARP port + 23: # position 23 on ACL list + action: PERMIT # allow packets that + protocol: ICMP # are ICMP + +``internal_outbound_acl`` +""""""""""""""""""""""""" + +ACL rules for packets that have a source IP address in what is considered the internal network and is going towards the DMZ network or the external network. + +example: + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + acl: + internal_outbound_acl: + 22: # position 22 on ACL list + action: PERMIT # allow packets that + src_port: ARP # are emitted from the ARP port + dst_port: ARP # are going towards an ARP port + 23: # position 23 on ACL list + action: PERMIT # allow packets that + protocol: ICMP # are ICMP + + +``dmz_inbound_acl`` +""""""""""""""""""" + +ACL rules for packets that have a destination IP address in what is considered the DMZ network. + +example: + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + acl: + dmz_inbound_acl: + 22: # position 22 on ACL list + action: PERMIT # allow packets that + src_port: ARP # are emitted from the ARP port + dst_port: ARP # are going towards an ARP port + 23: # position 23 on ACL list + action: PERMIT # allow packets that + protocol: ICMP # are ICMP + +``dmz_outbound_acl`` +"""""""""""""""""""" + +ACL rules for packets that have a source IP address in what is considered the DMZ network and is going towards the internal network or the external network. + +example: + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + acl: + dmz_outbound_acl: + 22: # position 22 on ACL list + action: PERMIT # allow packets that + src_port: ARP # are emitted from the ARP port + dst_port: ARP # are going towards an ARP port + 23: # position 23 on ACL list + action: PERMIT # allow packets that + protocol: ICMP # are ICMP + + + +``external_inbound_acl`` +"""""""""""""""""""""""" + +Optional. By default, this will allow any traffic through. + +ACL rules for packets that have a destination IP address in what is considered the external network. + +example: + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + acl: + external_inbound_acl: + 22: # position 22 on ACL list + action: PERMIT # allow packets that + src_port: ARP # are emitted from the ARP port + dst_port: ARP # are going towards an ARP port + 23: # position 23 on ACL list + action: PERMIT # allow packets that + protocol: ICMP # are ICMP + +``external_outbound_acl`` +""""""""""""""""""""""""" + +Optional. By default, this will allow any traffic through. + +ACL rules for packets that have a source IP address in what is considered the external network and is going towards the DMZ network or the internal network. + +example: + +.. code-block:: yaml + + nodes: + - ref: firewall + ... + acl: + external_outbound_acl: + 22: # position 22 on ACL list + action: PERMIT # allow packets that + src_port: ARP # are emitted from the ARP port + dst_port: ARP # are going towards an ARP port + 23: # position 23 on ACL list + action: PERMIT # allow packets that + protocol: ICMP # are ICMP + +.. include:: common/common_network_node_attributes.rst + +.. |NODE| replace:: firewall +.. |NODE_TYPE| replace:: ``firewall`` diff --git a/docs/source/configuration/simulation/nodes/router.rst b/docs/source/configuration/simulation/nodes/router.rst new file mode 100644 index 00000000..8a8efc06 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/router.rst @@ -0,0 +1,125 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _router_configuration: + +``router`` +========== + +A basic representation of a network router within the simulation. + +See :py:mod:`primaite.simulator.network.hardware.nodes.network.router.Router` + +example router +-------------- + +.. code-block:: yaml + + nodes: + - ref: router_1 + hostname: router_1 + type: router + num_ports: 5 + ports: + ... + acl: + ... + +.. include:: common/common_node_attributes.rst + +.. include:: common/node_type_list.rst + +``num_ports`` +------------- + +Optional. Default value is ``5``. + +The number of ports the router will have. + +``ports`` +--------- + +Sets up the router's ports with an IP address and a subnet mask. + +Example of setting ports for a router with 2 ports: + +.. code-block:: yaml + + nodes: + - ref: router_1 + ... + ports: + 1: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + +``ip_address`` +"""""""""""""" + +The IP address for the given port. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``subnet_mask`` +""""""""""""""" + +Optional. Default value is ``255.255.255.0``. + +The subnet mask setting for the port. + +``acl`` +------- + +Sets up the ACL rules for the router. + +e.g. + +.. code-block:: yaml + + nodes: + - ref: router_1 + ... + acl: + 1: + action: PERMIT + src_port: ARP + dst_port: ARP + 2: + action: PERMIT + protocol: ICMP + +See :py:mod:`primaite.simulator.network.hardware.nodes.network.router.AccessControlList` + +See :ref:`List of Ports ` for a list of ports. + +``action`` +"""""""""" + +Available options are + +- ``PERMIT`` : Allows the specified ``protocol`` or ``src_port`` and ``dst_port`` pairs +- ``DENY`` : Blocks the specified ``protocol`` or ``src_port`` and ``dst_port`` pairs + +``src_port`` +"""""""""""" + +Is used alongside ``dst_port``. Specifies the port where a packet originates. Used by the ACL Rule to determine if a packet with a specific source port is allowed to pass through the network node. + +``dst_port`` +"""""""""""" + +Is used alongside ``src_port``. Specifies the port where a packet is destined to arrive. Used by the ACL Rule to determine if a packet with a specific destination port is allowed to pass through the network node. + +``protocol`` +"""""""""""" + +Specifies which protocols are allowed by the ACL Rule to pass through the network node. + +See :ref:`List of IPProtocols ` for a list of protocols. + +.. include:: common/common_network_node_attributes.rst + +.. |NODE| replace:: router +.. |NODE_TYPE| replace:: ``router`` diff --git a/docs/source/configuration/simulation/nodes/server.rst b/docs/source/configuration/simulation/nodes/server.rst new file mode 100644 index 00000000..7f51eaf2 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/server.rst @@ -0,0 +1,39 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _server_configuration: + +``server`` +========== + +A basic representation of a server within the simulation. + +See :py:mod:`primaite.simulator.network.hardware.nodes.host.server.Server` + +example server +-------------- + +.. code-block:: yaml + + nodes: + - ref: server_1 + hostname: server_1 + type: server + ip_address: 192.168.10.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + ... + services: + ... + +.. include:: common/common_node_attributes.rst + +.. include:: common/node_type_list.rst + +.. include:: common/common_host_node_attributes.rst + +.. |NODE| replace:: server +.. |NODE_TYPE| replace:: ``server`` diff --git a/docs/source/configuration/simulation/nodes/switch.rst b/docs/source/configuration/simulation/nodes/switch.rst new file mode 100644 index 00000000..4d57f76e --- /dev/null +++ b/docs/source/configuration/simulation/nodes/switch.rst @@ -0,0 +1,37 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _switch_configuration: + +``switch`` +========== + +A basic representation of a network switch within the simulation. + +See :py:mod:`primaite.simulator.network.hardware.nodes.network.switch.Switch` + +example switch +-------------- + +.. code-block:: yaml + + nodes: + - ref: switch_1 + hostname: switch_1 + type: switch + num_ports: 8 + +.. include:: common/common_node_attributes.rst + +.. include:: common/node_type_list.rst + +``num_ports`` +------------- + +Optional. Default value is ``8``. + +The number of ports the switch will have. + +.. |NODE| replace:: switch +.. |NODE_TYPE| replace:: ``switch`` diff --git a/docs/source/configuration/simulation/software/applications.rst b/docs/source/configuration/simulation/software/applications.rst new file mode 100644 index 00000000..75e0c64c --- /dev/null +++ b/docs/source/configuration/simulation/software/applications.rst @@ -0,0 +1,10 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _applications_config: + +``applications`` +================ + +apps diff --git a/docs/source/configuration/simulation/software/services.rst b/docs/source/configuration/simulation/software/services.rst new file mode 100644 index 00000000..5f1783af --- /dev/null +++ b/docs/source/configuration/simulation/software/services.rst @@ -0,0 +1,10 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _services_config: + +``services`` +============ + +services diff --git a/docs/source/configuration/training_config.rst b/docs/source/configuration/training_config.rst index cde6cf52..3e63f69b 100644 --- a/docs/source/configuration/training_config.rst +++ b/docs/source/configuration/training_config.rst @@ -4,6 +4,22 @@ ``training_config`` =================== +Configuration items relevant to how the Reinforcement Learning agent(s) will be trained. + +``training_config`` hierarchy +----------------------------- + +.. code-block:: yaml + + training_config: + rl_framework: SB3 # or RLLIB_single_agent or RLLIB_multi_agent + rl_algorithm: PPO # or A2C + n_learn_episodes: 5 + max_steps_per_episode: 200 + n_eval_episodes: 1 + deterministic_eval: True + seed: 123 + ``rl_framework`` ---------------- @@ -23,3 +39,37 @@ Options available are: - ``PPO`` (Proximal Policy Optimisation) - ``A2C`` (Advantage Actor Critic) + +``n_learn_episodes`` +-------------------- +The number of episodes to train the agent(s). +This should be an integer value above ``0`` + +``max_steps_per_episode`` +------------------------- +The number of steps each episode will last for. +This should be an integer value above ``0``. + + +``n_eval_episodes`` +------------------- +Optional. Default value is ``0``. + +The number of evaluation episodes to run the trained agent for. +This should be an integer value above ``0``. + +``deterministic_eval`` +---------------------- +Optional. By default this value is ``False``. + +If this is set to ``True``, the agents will act deterministically instead of stochastically. + + + +``seed`` +-------- +Optional. + +The seed is used (alongside ``deterministic_eval``) to reproduce a previous instance of training and evaluation of an RL agent. +The seed should be an integer value. +Useful for debugging. diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 6eab6c54..ae248f23 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -583,8 +583,8 @@ simulation: nodes: - ref: router_1 - type: router hostname: router_1 + type: router num_ports: 5 ports: 1: @@ -619,18 +619,18 @@ simulation: protocol: ICMP - ref: switch_1 - type: switch hostname: switch_1 + type: switch num_ports: 8 - ref: switch_2 - type: switch hostname: switch_2 + type: switch num_ports: 8 - ref: domain_controller - type: server hostname: domain_controller + type: server ip_address: 192.168.1.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -642,8 +642,8 @@ simulation: arcd.com: 192.168.1.12 # web server - ref: web_server - type: server hostname: web_server + type: server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -658,8 +658,8 @@ simulation: - ref: database_server - type: server hostname: database_server + type: server ip_address: 192.168.1.14 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -673,8 +673,8 @@ simulation: type: FTPClient - ref: backup_server - type: server hostname: backup_server + type: server ip_address: 192.168.1.16 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -684,8 +684,8 @@ simulation: type: FTPServer - ref: security_suite - type: server hostname: security_suite + type: server ip_address: 192.168.1.110 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -696,8 +696,8 @@ simulation: subnet_mask: 255.255.255.0 - ref: client_1 - type: computer hostname: client_1 + type: computer ip_address: 192.168.10.21 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 @@ -719,8 +719,8 @@ simulation: type: DNSClient - ref: client_2 - type: computer hostname: client_2 + type: computer ip_address: 192.168.10.22 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 1793d420..b85cf86c 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -572,7 +572,7 @@ class NetworkNICDisableAction(NetworkNICAbstractAction): class ActionManager: """Class which manages the action space for an agent.""" - _act_class_identifiers: Dict[str, type] = { + act_class_identifiers: Dict[str, type] = { "DONOTHING": DoNothingAction, "NODE_SERVICE_SCAN": NodeServiceScanAction, "NODE_SERVICE_STOP": NodeServiceStopAction, @@ -753,7 +753,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/rewards.py b/src/primaite/game/agent/rewards.py index b5d5f998..27c39b65 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -245,12 +245,13 @@ class WebpageUnavailablePenalty(AbstractReward): class RewardFunction: """Manages the reward function for the agent.""" - __rew_class_identifiers: Dict[str, Type[AbstractReward]] = { + rew_class_identifiers: Dict[str, Type[AbstractReward]] = { "DUMMY": DummyReward, "DATABASE_FILE_INTEGRITY": DatabaseFileIntegrity, "WEB_SERVER_404_PENALTY": WebServer404Penalty, "WEBPAGE_UNAVAILABLE_PENALTY": WebpageUnavailablePenalty, } + """List of reward class identifiers.""" def __init__(self): """Initialise the reward function object.""" @@ -297,7 +298,7 @@ class RewardFunction: for rew_component_cfg in config["reward_components"]: rew_type = rew_component_cfg["type"] weight = rew_component_cfg.get("weight", 1.0) - rew_class = cls.__rew_class_identifiers[rew_type] + rew_class = cls.rew_class_identifiers[rew_type] rew_instance = rew_class.from_config(config=rew_component_cfg.get("options", {})) new.register_component(component=rew_instance, weight=weight) return new diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index b860fb2a..909b27a4 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -231,24 +231,24 @@ class PrimaiteGame: new_node = Computer( hostname=node_cfg["hostname"], ip_address=node_cfg["ip_address"], - subnet_mask=node_cfg["subnet_mask"], + subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), default_gateway=node_cfg["default_gateway"], - dns_server=node_cfg["dns_server"], + dns_server=node_cfg.get("dns_server", None), operating_state=NodeOperatingState.ON, ) elif n_type == "server": new_node = Server( hostname=node_cfg["hostname"], ip_address=node_cfg["ip_address"], - subnet_mask=node_cfg["subnet_mask"], + subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), default_gateway=node_cfg["default_gateway"], - dns_server=node_cfg.get("dns_server"), + dns_server=node_cfg.get("dns_server", None), operating_state=NodeOperatingState.ON, ) elif n_type == "switch": new_node = Switch( hostname=node_cfg["hostname"], - num_ports=node_cfg.get("num_ports"), + num_ports=int(node_cfg.get("num_ports", "8")), operating_state=NodeOperatingState.ON, ) elif n_type == "router": diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index f48d0561..903ce3f3 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -506,22 +506,24 @@ class Firewall(Router): # configure internal port new.configure_internal_port( ip_address=IPV4Address(internal_port.get("ip_address")), - subnet_mask=IPV4Address(internal_port.get("subnet_mask")), + subnet_mask=IPV4Address(internal_port.get("subnet_mask", "255.255.255.0")), ) # configure external port new.configure_external_port( ip_address=IPV4Address(external_port.get("ip_address")), - subnet_mask=IPV4Address(external_port.get("subnet_mask")), + subnet_mask=IPV4Address(external_port.get("subnet_mask", "255.255.255.0")), ) # configure dmz port new.configure_dmz_port( - ip_address=IPV4Address(dmz_port.get("ip_address")), subnet_mask=IPV4Address(dmz_port.get("subnet_mask")) + ip_address=IPV4Address(dmz_port.get("ip_address")), + subnet_mask=IPV4Address(dmz_port.get("subnet_mask", "255.255.255.0")), ) if "acl" in cfg: # acl rules for internal_inbound_acl if cfg["acl"]["internal_inbound_acl"]: + new.internal_inbound_acl.max_acl_rules new.internal_inbound_acl._default_config = cfg["acl"]["internal_inbound_acl"] new.internal_inbound_acl._reset_rules_to_default() @@ -553,8 +555,8 @@ class Firewall(Router): for route in cfg.get("routes"): new.route_table.add_route( address=IPv4Address(route.get("address")), - subnet_mask=IPv4Address(route.get("subnet_mask")), + subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), - metric=float(route.get("metric")), + metric=float(route.get("metric", 0)), ) return new diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index d52028a8..b3d7f7bf 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1482,7 +1482,7 @@ class Router(NetworkNode): """ new = Router( hostname=cfg["hostname"], - num_ports=cfg.get("num_ports"), + num_ports=int(cfg.get("num_ports", "5")), operating_state=NodeOperatingState.ON, ) if "ports" in cfg: @@ -1490,7 +1490,7 @@ class Router(NetworkNode): new.configure_port( port=port_num, ip_address=port_cfg["ip_address"], - subnet_mask=port_cfg["subnet_mask"], + subnet_mask=IPv4Address(port_cfg.get("subnet_mask", "255.255.255.0")), ) if "acl" in cfg: new.acl._default_config = cfg["acl"] # save the config to allow resetting @@ -1499,8 +1499,8 @@ class Router(NetworkNode): for route in cfg.get("routes"): new.route_table.add_route( address=IPv4Address(route.get("address")), - subnet_mask=IPv4Address(route.get("subnet_mask")), + subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), - metric=float(route.get("metric")), + metric=float(route.get("metric", 0)), ) return new diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index bdf4babc..dc848ade 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -9,11 +9,18 @@ _LOGGER = getLogger(__name__) class IPProtocol(Enum): - """Enum representing transport layer protocols in IP header.""" + """ + Enum representing transport layer protocols in IP header. + + .. _List of IPProtocols: + """ TCP = "tcp" + """Transmission Control Protocol.""" UDP = "udp" + """User Datagram Protocol.""" ICMP = "icmp" + """Internet Control Message Protocol.""" class Precedence(Enum): diff --git a/src/primaite/simulator/network/transmission/transport_layer.py b/src/primaite/simulator/network/transmission/transport_layer.py index 7c7509ab..c73e451a 100644 --- a/src/primaite/simulator/network/transmission/transport_layer.py +++ b/src/primaite/simulator/network/transmission/transport_layer.py @@ -5,7 +5,11 @@ from pydantic import BaseModel class Port(Enum): - """Enumeration of common known TCP/UDP ports used by protocols for operation of network applications.""" + """ + Enumeration of common known TCP/UDP ports used by protocols for operation of network applications. + + .. _List of Ports: + """ NONE = 0 "Place holder for a non-port." diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 971ed8cd..880735d9 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -20,7 +20,7 @@ # . ---------------- -------------- -------------- . # . | dmz_server |------| switch_2 |------| firewall | . # . ---------------- -------------- -------------- . -# . (Computer) | . +# . (Server) | . # ........................................................|................... # | # External Network | From 98fb28cbbc97cb164378507dd2bac120ea67f391 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Wed, 21 Feb 2024 18:19:16 +0000 Subject: [PATCH 017/128] #2257: setting up application and service docs --- docs/source/configuration/simulation.rst | 10 ------ .../common/common_host_node_attributes.rst | 22 ++++-------- .../simulation/software/applications.rst | 29 ++++++++++++--- .../applications/data_manipulation_bot.rst | 8 +++++ .../software/applications/database_client.rst | 8 +++++ .../software/applications/dos_bot.rst | 8 +++++ .../software/applications/web_browser.rst | 8 +++++ .../software/common/system_software.rst | 12 +++++++ .../simulation/software/services.rst | 35 ++++++++++++++++--- .../software/services/database_service.rst | 8 +++++ .../software/services/dns_client.rst | 8 +++++ .../software/services/dns_server.rst | 8 +++++ .../software/services/ftp_client.rst | 8 +++++ .../software/services/ftp_server.rst | 8 +++++ .../software/services/ntp_client.rst | 8 +++++ .../software/services/ntp_server.rst | 8 +++++ .../software/services/web_server.rst | 8 +++++ src/primaite/game/game.py | 2 ++ .../network/hardware/nodes/host/host_node.py | 30 +++++++--------- 19 files changed, 185 insertions(+), 51 deletions(-) create mode 100644 docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst create mode 100644 docs/source/configuration/simulation/software/applications/database_client.rst create mode 100644 docs/source/configuration/simulation/software/applications/dos_bot.rst create mode 100644 docs/source/configuration/simulation/software/applications/web_browser.rst create mode 100644 docs/source/configuration/simulation/software/common/system_software.rst create mode 100644 docs/source/configuration/simulation/software/services/database_service.rst create mode 100644 docs/source/configuration/simulation/software/services/dns_client.rst create mode 100644 docs/source/configuration/simulation/software/services/dns_server.rst create mode 100644 docs/source/configuration/simulation/software/services/ftp_client.rst create mode 100644 docs/source/configuration/simulation/software/services/ftp_server.rst create mode 100644 docs/source/configuration/simulation/software/services/ntp_client.rst create mode 100644 docs/source/configuration/simulation/software/services/ntp_server.rst create mode 100644 docs/source/configuration/simulation/software/services/web_server.rst diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index d8497212..7bb079e9 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -20,16 +20,6 @@ e.g. links: ... -**nodes:** - * ``type``: one of ``router``, ``switch``, ``computer``, or ``server``, this affects what other sub-options should be defined. - * ``hostname`` - a non-unique name used for logging and outputs. - * ``num_ports`` (optional, routers and switches only): number of network interfaces present on the device. - * ``ports`` (optional, routers and switches only): configuration for each network interface, including IP address and subnet mask. - * ``acl`` (Router only): Define the ACL rules at each index of the ACL on the router. the possible options are: ``action`` (PERMIT or DENY), ``src_port``, ``dst_port``, ``protocol``, ``src_ip``, ``dst_ip``. Any options left blank default to none which usually means that it will apply across all options. For example leaving ``src_ip`` blank will apply the rule to all IP addresses. - * ``services`` (computers and servers only): a list of services to install on the node. They must define a ``ref``, ``type``, and ``options`` that depend on which ``type`` was selected. - * ``applications`` (computer and servers only): Similar to services. A list of application to install on the node. - * ``network_interfaces`` (computers and servers only): If the node has multiple networking devices, the second, third, fourth, etc... must be defined here with an ``ip_address`` and ``subnet_mask``. - ``nodes`` --------- 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 265c7106..a95f98d4 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 @@ -3,39 +3,29 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK ``ip_address`` -^^^^^^^^^^^^^^ +-------------- The IP address of the |NODE| in the network. ``subnet_mask`` -^^^^^^^^^^^^^^^ +--------------- Optional. Default value is ``255.255.255.0``. The subnet mask for the |NODE| to use. ``default_gateway`` -^^^^^^^^^^^^^^^^^^^ +------------------- The IP address that the |NODE| will use as the default gateway. Typically, this is the IP address of the closest router that the |NODE| is connected to. ``dns_server`` -^^^^^^^^^^^^^^ +-------------- Optional. Default value is ``None`` The IP address of the node which holds an instance of the DNS server. Some applications may use a domain name e.g. the WebBrowser (TODO: WebBrowser page) -``applications`` -^^^^^^^^^^^^^^^^ +.. include:: ../software/applications.rst -A list of applications which are not considered system software that need to be installed on the |NODE|. - -See :ref:`Applications ` - -``services`` -^^^^^^^^^^^^ - -A list of services which are not considered system software that need to be installed on the |NODE|. - -See :ref:`Services ` +.. include:: ../software/services.rst diff --git a/docs/source/configuration/simulation/software/applications.rst b/docs/source/configuration/simulation/software/applications.rst index 75e0c64c..7acde817 100644 --- a/docs/source/configuration/simulation/software/applications.rst +++ b/docs/source/configuration/simulation/software/applications.rst @@ -2,9 +2,30 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -.. _applications_config: - ``applications`` -================ +---------------- -apps +List of available applications that can be installed on a |NODE|: + +.. toctree:: + :maxdepth: 1 + + ../software/applications/data_manipulation_bot.rst + ../software/applications/database_client.rst + ../software/applications/dos_bot.rst + ../software/applications/web_browser.rst + +More info :py:mod:`primaite.game.game.APPLICATION_TYPES_MAPPING` + +.. include:: ../software/common/system_software.rst + + +.. toctree:: + :maxdepth: 1 + + ../software/applications/web_browser.rst + +More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` + +.. |SOFTWARE_TYPE| replace:: application +.. |SOFTWARE_TYPES| replace:: applications diff --git a/docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst b/docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst new file mode 100644 index 00000000..6b650cf7 --- /dev/null +++ b/docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``DataManipulationBot`` +----------------------- + +test diff --git a/docs/source/configuration/simulation/software/applications/database_client.rst b/docs/source/configuration/simulation/software/applications/database_client.rst new file mode 100644 index 00000000..81e827bc --- /dev/null +++ b/docs/source/configuration/simulation/software/applications/database_client.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``DatabaseClient`` +------------------ + +test diff --git a/docs/source/configuration/simulation/software/applications/dos_bot.rst b/docs/source/configuration/simulation/software/applications/dos_bot.rst new file mode 100644 index 00000000..98939e5b --- /dev/null +++ b/docs/source/configuration/simulation/software/applications/dos_bot.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``DoSBot`` +---------- + +test diff --git a/docs/source/configuration/simulation/software/applications/web_browser.rst b/docs/source/configuration/simulation/software/applications/web_browser.rst new file mode 100644 index 00000000..4af0d7b7 --- /dev/null +++ b/docs/source/configuration/simulation/software/applications/web_browser.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``WebBrowser`` +-------------- + +test diff --git a/docs/source/configuration/simulation/software/common/system_software.rst b/docs/source/configuration/simulation/software/common/system_software.rst new file mode 100644 index 00000000..64248272 --- /dev/null +++ b/docs/source/configuration/simulation/software/common/system_software.rst @@ -0,0 +1,12 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``system software`` +""""""""""""""""""" + +Some |SOFTWARE_TYPES| are pre installed on nodes - this is similar to how some |SOFTWARE_TYPES| are included with the Operating System. + +The |SOFTWARE_TYPE| may not be configured as needed, in which case, follow the steps above to configure them. + +The list of |SOFTWARE_TYPES| that are considered system software are: diff --git a/docs/source/configuration/simulation/software/services.rst b/docs/source/configuration/simulation/software/services.rst index 5f1783af..383f9de4 100644 --- a/docs/source/configuration/simulation/software/services.rst +++ b/docs/source/configuration/simulation/software/services.rst @@ -2,9 +2,36 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -.. _services_config: - ``services`` -============ +------------ -services +List of available services that can be installed on a |NODE|: + +.. toctree:: + :maxdepth: 1 + + ../software/services/database_service.rst + ../software/services/dns_client.rst + ../software/services/dns_server.rst + ../software/services/ftp_client.rst + ../software/services/ftp_server.rst + ../software/services/ntp_client.rst + ../software/services/ntp_server.rst + ../software/services/web_server.rst + +More info :py:mod:`primaite.game.game.SERVICE_TYPES_MAPPING` + +.. include:: ../software/common/system_software.rst + + +.. toctree:: + :maxdepth: 1 + + ../software/services/dns_client.rst + ../software/services/ftp_client.rst + ../software/services/ntp_client.rst + +More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` + +.. |SOFTWARE_TYPE| replace:: service +.. |SOFTWARE_TYPES| replace:: services diff --git a/docs/source/configuration/simulation/software/services/database_service.rst b/docs/source/configuration/simulation/software/services/database_service.rst new file mode 100644 index 00000000..f03fde70 --- /dev/null +++ b/docs/source/configuration/simulation/software/services/database_service.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``DatabaseService`` +------------------- + +test diff --git a/docs/source/configuration/simulation/software/services/dns_client.rst b/docs/source/configuration/simulation/software/services/dns_client.rst new file mode 100644 index 00000000..d9b8008d --- /dev/null +++ b/docs/source/configuration/simulation/software/services/dns_client.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``DNSClient`` +------------- + +test diff --git a/docs/source/configuration/simulation/software/services/dns_server.rst b/docs/source/configuration/simulation/software/services/dns_server.rst new file mode 100644 index 00000000..a342967f --- /dev/null +++ b/docs/source/configuration/simulation/software/services/dns_server.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``DNSServer`` +------------- + +test diff --git a/docs/source/configuration/simulation/software/services/ftp_client.rst b/docs/source/configuration/simulation/software/services/ftp_client.rst new file mode 100644 index 00000000..d51a3dc1 --- /dev/null +++ b/docs/source/configuration/simulation/software/services/ftp_client.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``FTPClient`` +------------- + +test diff --git a/docs/source/configuration/simulation/software/services/ftp_server.rst b/docs/source/configuration/simulation/software/services/ftp_server.rst new file mode 100644 index 00000000..c7f92340 --- /dev/null +++ b/docs/source/configuration/simulation/software/services/ftp_server.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``FTPServer`` +------------- + +test diff --git a/docs/source/configuration/simulation/software/services/ntp_client.rst b/docs/source/configuration/simulation/software/services/ntp_client.rst new file mode 100644 index 00000000..51b2e061 --- /dev/null +++ b/docs/source/configuration/simulation/software/services/ntp_client.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``NTPClient`` +------------- + +test diff --git a/docs/source/configuration/simulation/software/services/ntp_server.rst b/docs/source/configuration/simulation/software/services/ntp_server.rst new file mode 100644 index 00000000..2efbdf1a --- /dev/null +++ b/docs/source/configuration/simulation/software/services/ntp_server.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``NTPServer`` +------------- + +test diff --git a/docs/source/configuration/simulation/software/services/web_server.rst b/docs/source/configuration/simulation/software/services/web_server.rst new file mode 100644 index 00000000..4fab660d --- /dev/null +++ b/docs/source/configuration/simulation/software/services/web_server.rst @@ -0,0 +1,8 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``WebServer`` +------------- + +test diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 909b27a4..7a17a03d 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -41,6 +41,7 @@ APPLICATION_TYPES_MAPPING = { "DataManipulationBot": DataManipulationBot, "DoSBot": DoSBot, } +"""List of available applications that can be installed on nodes in the PrimAITE Simulation.""" SERVICE_TYPES_MAPPING = { "DNSClient": DNSClient, @@ -52,6 +53,7 @@ SERVICE_TYPES_MAPPING = { "NTPClient": NTPClient, "NTPServer": NTPServer, } +"""List of available services that can be installed on nodes in the PrimAITE Simulation.""" class PrimaiteGameOptions(BaseModel): 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 3f34f736..6db1e036 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -261,6 +261,17 @@ class NIC(IPWiredNetworkInterface): return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}" +SYSTEM_SOFTWARE = { + "HostARP": HostARP, + "ICMP": ICMP, + "DNSClient": DNSClient, + "FTPClient": FTPClient, + "NTPClient": NTPClient, + "WebBrowser": WebBrowser, +} +"""List of system software that is automatically installed on nodes.""" + + class HostNode(Node): """ Represents a host node in the network. @@ -321,23 +332,8 @@ class HostNode(Node): This method equips the host with essential network services and applications, preparing it for various network-related tasks and operations. """ - # ARP Service - self.software_manager.install(HostARP) - - # ICMP Service - self.software_manager.install(ICMP) - - # DNS Client - self.software_manager.install(DNSClient) - - # FTP Client - self.software_manager.install(FTPClient) - - # NTP Client - self.software_manager.install(NTPClient) - - # Web Browser - self.software_manager.install(WebBrowser) + for _, software_class in SYSTEM_SOFTWARE.items(): + self.software_manager.install(software_class) super()._install_system_software() From 771a68dccba056428caee4fec18a9c0a4e4c2648 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Thu, 22 Feb 2024 22:43:14 +0000 Subject: [PATCH 018/128] #2238 - Implement NMNE detection and logging in NetworkInterface. - Enhance NicObservation for detailed NMNE event monitoring. - Add nmne_config options to simulation settings for customizable NMNE capturing. - Update documentation and tests for new NMNE features and simulation config. --- CHANGELOG.md | 6 +- .../network/network_interfaces.rst | 11 +- .../config/_package_data/example_config.yaml | 4 + .../example_config_2_rl_agents.yaml | 4 + src/primaite/game/agent/observations.py | 52 +++++++- src/primaite/game/game.py | 4 + .../simulator/network/hardware/base.py | 102 +++++++++++++-- .../network/hardware/nodes/host/host_node.py | 1 + src/primaite/simulator/network/nmne.py | 46 +++++++ .../network/test_capture_nmne.py | 120 ++++++++++++++++++ 10 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 src/primaite/simulator/network/nmne.py create mode 100644 tests/integration_tests/network/test_capture_nmne.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e45d2e..40ac6535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,7 +82,8 @@ SessionManager. - `AirSpace` class to simulate wireless communications, managing wireless interfaces and facilitating the transmission of frames within specified frequencies. - `AirSpaceFrequency` enum for defining standard wireless frequencies, including 2.4 GHz and 5 GHz bands, to support realistic wireless network simulations. - `WirelessRouter` class, extending the `Router` class, to incorporate wireless networking capabilities alongside traditional wired connections. This class allows the configuration of wireless access points with specific IP settings and operating frequencies. - +- NMNE capturing capabilities to `NetworkInterface` class for detecting and logging Malicious Network Events. +- New `nmne_config` settings in the simulation configuration to enable NMNE capturing and specify keywords such as "DELETE". ### Changed - Integrated the RouteTable into the Routers frame processing. @@ -94,7 +95,8 @@ SessionManager. - Refactored all tests to utilise new `Node` subclasses (`Computer`, `Server`, `Router`, `Switch`) instead of creating generic `Node` instances and manually adding network interfaces. This change aligns test setups more closely with the intended use cases and hierarchies within the network simulation framework. - Updated all tests to employ the `Network()` class for managing nodes and their connections, ensuring a consistent and structured approach to setting up network topologies in testing scenarios. - **ACLRule Wildcard Masking**: Updated the `ACLRule` class to support IP ranges using wildcard masking. This enhancement allows for more flexible and granular control over traffic filtering, enabling the specification of broader or more specific IP address ranges in ACL rules. - +- Updated `NetworkInterface` documentation to reflect the new NMNE capturing features and how to use them. +- Integration of NMNE capturing functionality within the `NicObservation` class. ### Removed - Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol` diff --git a/docs/source/simulation_components/network/network_interfaces.rst b/docs/source/simulation_components/network/network_interfaces.rst index 9e1ad80a..c74b54ae 100644 --- a/docs/source/simulation_components/network/network_interfaces.rst +++ b/docs/source/simulation_components/network/network_interfaces.rst @@ -65,9 +65,14 @@ Network Interface Classes **NetworkInterface (Base Layer)** -Abstract base class defining core interface properties like MAC address, speed, MTU. -Requires subclasses implement key methods like send/receive frames, enable/disable interface. -Establishes universal network interface capabilities. +- Abstract base class defining core interface properties like MAC address, speed, MTU. +- Requires subclasses implement key methods like send/receive frames, enable/disable interface. +- Establishes universal network interface capabilities. +- Malicious Network Events Monitoring: + + * Enhances network interfaces with the capability to monitor and capture Malicious Network Events (MNEs) based on predefined criteria such as specific keywords or traffic patterns. + * Integrates NMNE detection functionalities, leveraging configurable settings like ``capture_nmne``, `nmne_capture_keywords``, and observation mechanisms such as ``NicObservation`` to classify and record network anomalies. + * Offers an additional layer of security and data analysis, crucial for identifying and mitigating malicious activities within the network infrastructure. Provides vital information for network security analysis and reinforcement learning algorithms. **WiredNetworkInterface (Connection Type Layer)** diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index f85baf10..a72ebeca 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -583,6 +583,10 @@ agents: simulation: network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE nodes: - ref: router_1 diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 93019c9d..12461547 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -963,6 +963,10 @@ agents: simulation: network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE nodes: - ref: router_1 diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index dfee2543..1d8799fd 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -8,6 +8,7 @@ from gymnasium.core import ObsType from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.simulator.network.nmne import CAPTURE_NMNE _LOGGER = getLogger(__name__) @@ -346,7 +347,14 @@ class FolderObservation(AbstractObservation): class NicObservation(AbstractObservation): """Observation of a Network Interface Card (NIC) in the network.""" - default_observation: spaces.Space = {"nic_status": 0} + @property + def default_observation(self) -> Dict: + """The default NIC observation dict.""" + data = {"nic_status": 0} + + if CAPTURE_NMNE: + data.update({"nmne": {"inbound": 0, "outbound": 0}}) + return data def __init__(self, where: Optional[Tuple[str]] = None) -> None: """Initialise NIC observation. @@ -360,6 +368,29 @@ class NicObservation(AbstractObservation): super().__init__() self.where: Optional[Tuple[str]] = where + def _categorise_mne_count(self, nmne_count: int) -> int: + """ + Categorise the number of Malicious Network Events (NMNEs) into discrete bins. + + This helps in classifying the severity or volume of MNEs into manageable levels for the agent. + + Bins are defined as follows: + - 0: No MNEs detected (0 events). + - 1: Low number of MNEs (1-5 events). + - 2: Moderate number of MNEs (6-10 events). + - 3: High number of MNEs (more than 10 events). + + :param nmne_count: Number of MNEs detected. + :return: Bin number corresponding to the number of MNEs. Returns 0, 1, 2, or 3 based on the detected MNE count. + """ + if nmne_count > 10: + return 3 + elif nmne_count > 5: + return 2 + elif nmne_count > 0: + return 1 + return 0 + def observe(self, state: Dict) -> Dict: """Generate observation based on the current state of the simulation. @@ -371,15 +402,30 @@ class NicObservation(AbstractObservation): if self.where is None: return self.default_observation nic_state = access_from_nested_dict(state, self.where) + if nic_state is NOT_PRESENT_IN_STATE: return self.default_observation else: - return {"nic_status": 1 if nic_state["enabled"] else 2} + obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2, "nmne": {}} + if CAPTURE_NMNE: + direction_dict = nic_state["nmne"].get("direction", {}) + inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) + inbound_count = inbound_keywords.get("*", 0) + outbound_keywords = direction_dict.get("outbound", {}).get("keywords", {}) + outbound_count = outbound_keywords.get("*", 0) + obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count) + obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count) + return obs_dict @property def space(self) -> spaces.Space: """Gymnasium space object describing the observation space shape.""" - return spaces.Dict({"nic_status": spaces.Discrete(3)}) + return spaces.Dict( + { + "nic_status": spaces.Discrete(3), + "nmne": spaces.Dict({"inbound": spaces.Discrete(6), "outbound": spaces.Discrete(6)}), + } + ) @classmethod def from_config(cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]]) -> "NicObservation": diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index ed98accd..1f5dc8fa 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -17,6 +17,7 @@ from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import Router from primaite.simulator.network.hardware.nodes.network.switch import Switch +from primaite.simulator.network.nmne import set_nmne_config from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.database_client import DatabaseClient @@ -426,4 +427,7 @@ class PrimaiteGame: game.simulation.set_original_state() + # Set the NMNE capture config + set_nmne_config(cfg["simulation"]["network"].get("nmne_config", {})) + return game diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index fa135674..c0e69e60 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -17,6 +17,15 @@ from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.nmne import ( + CAPTURE_BY_DIRECTION, + CAPTURE_BY_IP_ADDRESS, + CAPTURE_BY_KEYWORD, + CAPTURE_BY_PORT, + CAPTURE_BY_PROTOCOL, + CAPTURE_NMNE, + NMNE_CAPTURE_KEYWORDS, +) from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.packet_capture import PacketCapture @@ -88,6 +97,8 @@ class NetworkInterface(SimComponent, ABC): pcap: Optional[PacketCapture] = None "A PacketCapture instance for capturing and analysing packets passing through this interface." + nmne: Dict = Field(default_factory=lambda: {}) + def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() @@ -111,27 +122,99 @@ class NetworkInterface(SimComponent, ABC): "enabled": self.enabled, } ) + state.update({"nmne": self.nmne}) return state def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" super().reset_component_for_episode(episode) + self.nmne = {} if episode and self.pcap: self.pcap.current_episode = episode self.pcap.setup_logger() self.enable() - @abstractmethod + # @abstractmethod def enable(self): """Enable the interface.""" pass - @abstractmethod + # @abstractmethod def disable(self): """Disable the interface.""" pass - @abstractmethod + def _capture_nmne(self, frame: Frame, inbound: bool = True): + """ + Processes and captures network frame data based on predefined global NMNE settings. + + This method updates the NMNE structure with counts of malicious network events based on the frame content and + direction. The structure is dynamically adjusted according to the enabled capture settings. + + :param frame: The network frame to process, containing IP, TCP/UDP, and payload information. + :param inbound: Boolean indicating if the frame direction is inbound. Defaults to True. + """ + # Exit function if NMNE capturing is disabled + if not CAPTURE_NMNE: + return + + # Initialise basic frame data variables + direction = "inbound" if inbound else "outbound" # Direction of the traffic + ip_address = str(frame.ip.src_ip_address if inbound else frame.ip.dst_ip_address) # Source or destination IP + protocol = frame.ip.protocol.name # Network protocol used in the frame + + # Initialise port variable; will be determined based on protocol type + port = None + + # Determine the source or destination port based on the protocol (TCP/UDP) + if frame.tcp: + port = frame.tcp.src_port.value if inbound else frame.tcp.dst_port.value + elif frame.udp: + port = frame.udp.src_port.value if inbound else frame.udp.dst_port.value + + # Convert frame payload to string for keyword checking + frame_str = str(frame.payload) + + # Proceed only if any NMNE keyword is present in the frame payload + if any(keyword in frame_str for keyword in NMNE_CAPTURE_KEYWORDS): + # Start with the root of the NMNE capture structure + current_level = self.nmne + + # Update NMNE structure based on enabled settings + if CAPTURE_BY_DIRECTION: + # Set or get the dictionary for the current direction + current_level = current_level.setdefault("direction", {}) + current_level = current_level.setdefault(direction, {}) + + if CAPTURE_BY_IP_ADDRESS: + # Set or get the dictionary for the current IP address + current_level = current_level.setdefault("ip_address", {}) + current_level = current_level.setdefault(ip_address, {}) + + if CAPTURE_BY_PROTOCOL: + # Set or get the dictionary for the current protocol + current_level = current_level.setdefault("protocol", {}) + current_level = current_level.setdefault(protocol, {}) + + if CAPTURE_BY_PORT: + # Set or get the dictionary for the current port + current_level = current_level.setdefault("port", {}) + current_level = current_level.setdefault(port, {}) + + # Ensure 'KEYWORD' level is present in the structure + keyword_level = current_level.setdefault("keywords", {}) + + # Increment the count for detected keywords in the payload + if CAPTURE_BY_KEYWORD: + for keyword in NMNE_CAPTURE_KEYWORDS: + if keyword in frame_str: + # Update the count for each keyword found + keyword_level[keyword] = keyword_level.get(keyword, 0) + 1 + else: + # Increment a generic counter if keyword capturing is not enabled + keyword_level["*"] = keyword_level.get("*", 0) + 1 + + # @abstractmethod def send_frame(self, frame: Frame) -> bool: """ Attempts to send a network frame through the interface. @@ -139,9 +222,9 @@ class NetworkInterface(SimComponent, ABC): :param frame: The network frame to be sent. :return: A boolean indicating whether the frame was successfully sent. """ - pass + self._capture_nmne(frame, inbound=False) - @abstractmethod + # @abstractmethod def receive_frame(self, frame: Frame) -> bool: """ Receives a network frame on the interface. @@ -149,7 +232,7 @@ class NetworkInterface(SimComponent, ABC): :param frame: The network frame being received. :return: A boolean indicating whether the frame was successfully received. """ - pass + self._capture_nmne(frame, inbound=True) def __str__(self) -> str: """ @@ -263,6 +346,7 @@ class WiredNetworkInterface(NetworkInterface, ABC): :param frame: The network frame to be sent. :return: True if the frame is sent, False if the Network Interface is disabled or not connected to a link. """ + super().send_frame(frame) if self.enabled: frame.set_sent_timestamp() self.pcap.capture_outbound(frame) @@ -279,7 +363,7 @@ class WiredNetworkInterface(NetworkInterface, ABC): :param frame: The network frame being received. :return: A boolean indicating whether the frame was successfully received. """ - pass + return super().receive_frame(frame) class Layer3Interface(BaseModel, ABC): @@ -409,7 +493,7 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): except AttributeError: pass - # @abstractmethod + @abstractmethod def receive_frame(self, frame: Frame) -> bool: """ Receives a network frame on the network interface. @@ -417,7 +501,7 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): :param frame: The network frame being received. :return: A boolean indicating whether the frame was successfully received. """ - pass + return super().receive_frame(frame) class Link(SimComponent): 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 3f34f736..6ecd6733 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -248,6 +248,7 @@ class NIC(IPWiredNetworkInterface): accept_frame = True if accept_frame: + super().receive_frame(frame) self._connected_node.receive_frame(frame=frame, from_network_interface=self) return True return False diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py new file mode 100644 index 00000000..d4c40631 --- /dev/null +++ b/src/primaite/simulator/network/nmne.py @@ -0,0 +1,46 @@ +from typing import Dict, Final, List + +CAPTURE_NMNE: bool = True +"""Indicates whether Malicious Network Events (MNEs) should be captured. Default is True.""" + +NMNE_CAPTURE_KEYWORDS: List[str] = [] +"""List of keywords to identify malicious network events.""" + +CAPTURE_BY_DIRECTION: Final[bool] = True +"""Flag to determine if captures should be organized by traffic direction (inbound/outbound).""" +CAPTURE_BY_IP_ADDRESS: Final[bool] = False +"""Flag to determine if captures should be organized by source or destination IP address.""" +CAPTURE_BY_PROTOCOL: Final[bool] = False +"""Flag to determine if captures should be organized by network protocol (e.g., TCP, UDP).""" +CAPTURE_BY_PORT: Final[bool] = False +"""Flag to determine if captures should be organized by source or destination port.""" +CAPTURE_BY_KEYWORD: Final[bool] = False +"""Flag to determine if captures should be filtered and categorised based on specific keywords.""" + + +def set_nmne_config(nmne_config: Dict): + """ + Sets the configuration for capturing Malicious Network Events (MNEs) based on a provided dictionary. + + This function updates global settings related to NMNE capture, including whether to capture NMNEs and what + keywords to use for identifying NMNEs. + + The function ensures that the settings are updated only if they are provided in the `nmne_config` dictionary, + and maintains type integrity by checking the types of the provided values. + + :param nmne_config: A dictionary containing the NMNE configuration settings. Possible keys include: + "capture_nmne" (bool) to indicate whether NMNEs should be captured, "nmne_capture_keywords" (list of strings) + to specify keywords for NMNE identification. + """ + global NMNE_CAPTURE_KEYWORDS + global CAPTURE_NMNE + + # Update the NMNE capture flag, defaulting to False if not specified or if the type is incorrect + CAPTURE_NMNE = nmne_config.get("capture_nmne", False) + if not isinstance(CAPTURE_NMNE, bool): + CAPTURE_NMNE = True # Revert to default True if the provided value is not a boolean + + # Update the NMNE capture keywords, appending new keywords if provided + NMNE_CAPTURE_KEYWORDS += nmne_config.get("nmne_capture_keywords", []) + if not isinstance(NMNE_CAPTURE_KEYWORDS, list): + NMNE_CAPTURE_KEYWORDS = [] # Reset to empty list if the provided value is not a list diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py new file mode 100644 index 00000000..85ac23e8 --- /dev/null +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -0,0 +1,120 @@ +from primaite.game.agent.observations import NicObservation +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.network.nmne import set_nmne_config +from primaite.simulator.sim_container import Simulation +from primaite.simulator.system.applications.database_client import DatabaseClient + + +def test_capture_nmne(uc2_network): + """ + Conducts a test to verify that Malicious Network Events (MNEs) are correctly captured. + + This test involves a web server querying a database server and checks if the MNEs are captured + based on predefined keywords in the network configuration. Specifically, it checks the capture + of the "DELETE" SQL command as a malicious network event. + """ + web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa + db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa + db_client.connect() + + db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa + + web_server_nic = web_server.network_interface[1] + db_server_nic = db_server.network_interface[1] + + # Set the NMNE configuration to capture DELETE queries as MNEs + nmne_config = { + "capture_nmne": True, # Enable the capture of MNEs + "nmne_capture_keywords": ["DELETE"], # Specify "DELETE" SQL command as a keyword for MNE detection + } + + # Apply the NMNE configuration settings + set_nmne_config(nmne_config) + + # Assert that initially, there are no captured MNEs on both web and database servers + assert web_server_nic.describe_state()["nmne"] == {} + assert db_server_nic.describe_state()["nmne"] == {} + + # Perform a "SELECT" query + db_client.query("SELECT") + + # Check that it does not trigger an MNE capture. + assert web_server_nic.describe_state()["nmne"] == {} + assert db_server_nic.describe_state()["nmne"] == {} + + # Perform a "DELETE" query + db_client.query("DELETE") + + # Check that the web server's outbound interface and the database server's inbound interface register the MNE + assert web_server_nic.describe_state()["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic.describe_state()["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} + + # Perform another "SELECT" query + db_client.query("SELECT") + + # Check that no additional MNEs are captured + assert web_server_nic.describe_state()["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic.describe_state()["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} + + # Perform another "DELETE" query + db_client.query("DELETE") + + # Check that the web server and database server interfaces register an additional MNE + assert web_server_nic.describe_state()["nmne"] == {"direction": {"outbound": {"keywords": {"*": 2}}}} + assert db_server_nic.describe_state()["nmne"] == {"direction": {"inbound": {"keywords": {"*": 2}}}} + + +def test_capture_nmne_observations(uc2_network): + """ + Tests the NicObservation class's functionality within a simulated network environment. + + This test ensures the observation space, as defined by instances of NicObservation, accurately reflects the + number of MNEs detected based on network activities over multiple iterations. + + The test employs a series of "DELETE" SQL operations, considered as MNEs, to validate the dynamic update + and accuracy of the observation space related to network interface conditions. It confirms that the + observed NIC states match expected MNE activity levels. + """ + # Initialise a new Simulation instance and assign the test network to it. + sim = Simulation() + sim.network = uc2_network + + web_server: Server = uc2_network.get_node_by_hostname("web_server") + db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client.connect() + + # Set the NMNE configuration to capture DELETE queries as MNEs + nmne_config = { + "capture_nmne": True, # Enable the capture of MNEs + "nmne_capture_keywords": ["DELETE"], # Specify "DELETE" SQL command as a keyword for MNE detection + } + + # Apply the NMNE configuration settings + set_nmne_config(nmne_config) + + # Define observations for the NICs of the database and web servers + db_server_nic_obs = NicObservation(where=["network", "nodes", "database_server", "NICs", 1]) + web_server_nic_obs = NicObservation(where=["network", "nodes", "web_server", "NICs", 1]) + + # Iterate through a set of test cases to simulate multiple DELETE queries + for i in range(1, 20): + # Perform a "DELETE" query each iteration + db_client.query("DELETE") + + # Observe the current state of NMNEs from the NICs of both the database and web servers + db_nic_obs = db_server_nic_obs.observe(sim.describe_state())["nmne"] + web_nic_obs = web_server_nic_obs.observe(sim.describe_state())["nmne"] + + # Define expected NMNE values based on the iteration count + if i > 10: + expected_nmne = 3 # High level of detected MNEs after 10 iterations + elif i > 5: + expected_nmne = 2 # Moderate level after more than 5 iterations + elif i > 0: + expected_nmne = 1 # Low level detected after just starting + else: + expected_nmne = 0 # No MNEs detected + + # Assert that the observed NMNEs match the expected values for both NICs + assert web_nic_obs["outbound"] == expected_nmne + assert db_nic_obs["inbound"] == expected_nmne From 5836ea68e339d59ffc9c6f56f472bb8dea2477cf Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 23 Feb 2024 08:55:32 +0000 Subject: [PATCH 019/128] #2257: rearrange software pages + creating a list of applications and services which is hopefully a single point that should be referred to --- docs/conf.py | 5 ++ docs/source/config.rst | 4 +- docs/source/configuration/simulation.rst | 2 + .../simulation/nodes/firewall.rst | 4 ++ .../simulation/software/applications.rst | 34 ++++----- .../applications/data_manipulation_bot.rst | 8 --- .../software/applications/database_client.rst | 8 --- .../software/applications/web_browser.rst | 8 --- .../software/common/system_software.rst | 12 ---- .../simulation/software/services.rst | 40 ++++------- .../software/services/database_service.rst | 8 --- .../software/services/dns_client.rst | 8 --- .../software/services/dns_server.rst | 8 --- .../software/services/ftp_client.rst | 8 --- .../software/services/ftp_server.rst | 8 --- .../software/services/ntp_client.rst | 8 --- .../software/services/ntp_server.rst | 8 --- .../software/services/web_server.rst | 8 --- docs/source/game_layer.rst | 2 +- .../network/network_interfaces.rst | 2 + .../network/nodes/firewall.rst | 2 +- .../network/nodes/network_node.rst | 2 +- .../data_manipulation_bot.rst | 1 + .../system/applications/database_client.rst | 38 ++++++++++ .../system}/applications/dos_bot.rst | 4 +- .../web_browser.rst} | 30 +------- .../system/database_client_server.rst | 71 ------------------- .../system/list_of_applications.rst | 11 +++ .../system/list_of_services.rst | 15 ++++ .../system/list_of_system_applications.rst | 19 +++++ .../system/list_of_system_services.rst | 21 ++++++ .../system/services/database_service.rst | 33 +++++++++ .../dns_client.rst} | 30 +------- .../system/services/dns_server.rst | 26 +++++++ .../ftp_client.rst} | 30 +------- .../system/services/ftp_server.rst | 27 +++++++ .../system/services/ntp_client.rst | 26 +++++++ .../ntp_server.rst} | 30 +------- .../system/services/web_server.rst | 27 +++++++ .../system/session_and_software_manager.rst | 2 + .../simulation_components/system/software.rst | 31 +++++--- docs/source/simulation_structure.rst | 11 +-- 42 files changed, 329 insertions(+), 351 deletions(-) delete mode 100644 docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst delete mode 100644 docs/source/configuration/simulation/software/applications/database_client.rst delete mode 100644 docs/source/configuration/simulation/software/applications/web_browser.rst delete mode 100644 docs/source/configuration/simulation/software/common/system_software.rst delete mode 100644 docs/source/configuration/simulation/software/services/database_service.rst delete mode 100644 docs/source/configuration/simulation/software/services/dns_client.rst delete mode 100644 docs/source/configuration/simulation/software/services/dns_server.rst delete mode 100644 docs/source/configuration/simulation/software/services/ftp_client.rst delete mode 100644 docs/source/configuration/simulation/software/services/ftp_server.rst delete mode 100644 docs/source/configuration/simulation/software/services/ntp_client.rst delete mode 100644 docs/source/configuration/simulation/software/services/ntp_server.rst delete mode 100644 docs/source/configuration/simulation/software/services/web_server.rst rename docs/source/simulation_components/system/{ => applications}/data_manipulation_bot.rst (99%) create mode 100644 docs/source/simulation_components/system/applications/database_client.rst rename docs/source/{configuration/simulation/software => simulation_components/system}/applications/dos_bot.rst (82%) rename docs/source/simulation_components/system/{web_browser_and_web_server_service.rst => applications/web_browser.rst} (72%) delete mode 100644 docs/source/simulation_components/system/database_client_server.rst create mode 100644 docs/source/simulation_components/system/list_of_applications.rst create mode 100644 docs/source/simulation_components/system/list_of_services.rst create mode 100644 docs/source/simulation_components/system/list_of_system_applications.rst create mode 100644 docs/source/simulation_components/system/list_of_system_services.rst create mode 100644 docs/source/simulation_components/system/services/database_service.rst rename docs/source/simulation_components/system/{dns_client_server.rst => services/dns_client.rst} (52%) create mode 100644 docs/source/simulation_components/system/services/dns_server.rst rename docs/source/simulation_components/system/{ftp_client_server.rst => services/ftp_client.rst} (78%) create mode 100644 docs/source/simulation_components/system/services/ftp_server.rst create mode 100644 docs/source/simulation_components/system/services/ntp_client.rst rename docs/source/simulation_components/system/{ntp_client_server.rst => services/ntp_server.rst} (56%) create mode 100644 docs/source/simulation_components/system/services/web_server.rst diff --git a/docs/conf.py b/docs/conf.py index 6cdc0ac4..d246afe5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,6 +27,11 @@ with open("../src/primaite/VERSION", "r") as file: # The full version, including alpha/beta/rc tags release = version +# set global variables +rst_prolog = f""" +.. |VERSION| replace:: {release} +""" + html_title = f"{project} v{release} docs" # -- General configuration --------------------------------------------------- diff --git a/docs/source/config.rst b/docs/source/config.rst index b7bce731..89181a24 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -2,8 +2,8 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -Primaite v3 config -****************** +PrimAITE |VERSION| Configuration +******************************** PrimAITE uses a single configuration file to define everything needed to train and evaluate an RL policy in a custom cybersecurity scenario. This includes the configuration of the network, the scripted or trained agents that interact with the network, as well as settings that define how to perform training in Stable Baselines 3 or Ray RLLib. The entire config is used by the ``PrimaiteSession`` object for users who wish to let PrimAITE handle the agent definition and training. If you wish to define custom agents and control the training loop yourself, you can use the config with the ``PrimaiteGame``, and ``PrimaiteGymEnv`` objects instead. That way, only the network configuration and agent setup parts of the config are used, and the training section is ignored. diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index 7bb079e9..f24cc41d 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -49,6 +49,8 @@ In order to recreate the network below, we will need to create 2 links: - a link from computer_2 to the switch .. image:: ../../_static/switched_p2p_network.png + :width: 500 + :align: center this results in: diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst index b1e4e5e1..c8a21a02 100644 --- a/docs/source/configuration/simulation/nodes/firewall.rst +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -106,9 +106,13 @@ There are 6 ACLs that can be defined for a firewall - ``external_outbound_acl`` for traffic going towards the external network .. image:: ../../../../_static/firewall_acl.png + :width: 500 + :align: center By default, ``external_inbound_acl`` and ``external_outbound_acl`` will permit any traffic through. +``internal_inbound_acl``, ``internal_outbound_acl``, ``dmz_inbound_acl`` and ``dmz_outbound_acl`` will deny any traffic by default, so must be configured to allow defined ``src_port`` and ``dst_port`` or ``protocol``. + See :py:mod:`primaite.simulator.network.hardware.nodes.network.router.AccessControlList` See :ref:`List of Ports ` for a list of ports. diff --git a/docs/source/configuration/simulation/software/applications.rst b/docs/source/configuration/simulation/software/applications.rst index 7acde817..90ae3ec1 100644 --- a/docs/source/configuration/simulation/software/applications.rst +++ b/docs/source/configuration/simulation/software/applications.rst @@ -5,27 +5,21 @@ ``applications`` ---------------- -List of available applications that can be installed on a |NODE|: +List of available applications that can be installed on a |NODE| can be found in :ref:`List of Applications ` -.. toctree:: - :maxdepth: 1 +application in configuration +"""""""""""""""""""""""""""" - ../software/applications/data_manipulation_bot.rst - ../software/applications/database_client.rst - ../software/applications/dos_bot.rst - ../software/applications/web_browser.rst +Applications takes a list of applications as shown in the example below. -More info :py:mod:`primaite.game.game.APPLICATION_TYPES_MAPPING` +.. code-block:: yaml -.. include:: ../software/common/system_software.rst - - -.. toctree:: - :maxdepth: 1 - - ../software/applications/web_browser.rst - -More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` - -.. |SOFTWARE_TYPE| replace:: application -.. |SOFTWARE_TYPES| replace:: applications + - ref: client_1 + hostname: client_1 + type: computer + ... + applications: + - ref: example_application + type: example_application_type + options: + # this section is different for each application diff --git a/docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst b/docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst deleted file mode 100644 index 6b650cf7..00000000 --- a/docs/source/configuration/simulation/software/applications/data_manipulation_bot.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``DataManipulationBot`` ------------------------ - -test diff --git a/docs/source/configuration/simulation/software/applications/database_client.rst b/docs/source/configuration/simulation/software/applications/database_client.rst deleted file mode 100644 index 81e827bc..00000000 --- a/docs/source/configuration/simulation/software/applications/database_client.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``DatabaseClient`` ------------------- - -test diff --git a/docs/source/configuration/simulation/software/applications/web_browser.rst b/docs/source/configuration/simulation/software/applications/web_browser.rst deleted file mode 100644 index 4af0d7b7..00000000 --- a/docs/source/configuration/simulation/software/applications/web_browser.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``WebBrowser`` --------------- - -test diff --git a/docs/source/configuration/simulation/software/common/system_software.rst b/docs/source/configuration/simulation/software/common/system_software.rst deleted file mode 100644 index 64248272..00000000 --- a/docs/source/configuration/simulation/software/common/system_software.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``system software`` -""""""""""""""""""" - -Some |SOFTWARE_TYPES| are pre installed on nodes - this is similar to how some |SOFTWARE_TYPES| are included with the Operating System. - -The |SOFTWARE_TYPE| may not be configured as needed, in which case, follow the steps above to configure them. - -The list of |SOFTWARE_TYPES| that are considered system software are: diff --git a/docs/source/configuration/simulation/software/services.rst b/docs/source/configuration/simulation/software/services.rst index 383f9de4..88957001 100644 --- a/docs/source/configuration/simulation/software/services.rst +++ b/docs/source/configuration/simulation/software/services.rst @@ -5,33 +5,21 @@ ``services`` ------------ -List of available services that can be installed on a |NODE|: +List of available services that can be installed on a |NODE| can be found in :ref:`List of Services ` -.. toctree:: - :maxdepth: 1 +services in configuration +""""""""""""""""""""""""" - ../software/services/database_service.rst - ../software/services/dns_client.rst - ../software/services/dns_server.rst - ../software/services/ftp_client.rst - ../software/services/ftp_server.rst - ../software/services/ntp_client.rst - ../software/services/ntp_server.rst - ../software/services/web_server.rst +Services takes a list of services as shown in the example below. -More info :py:mod:`primaite.game.game.SERVICE_TYPES_MAPPING` +.. code-block:: yaml -.. include:: ../software/common/system_software.rst - - -.. toctree:: - :maxdepth: 1 - - ../software/services/dns_client.rst - ../software/services/ftp_client.rst - ../software/services/ntp_client.rst - -More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` - -.. |SOFTWARE_TYPE| replace:: service -.. |SOFTWARE_TYPES| replace:: services + - ref: client_1 + hostname: client_1 + type: computer + ... + applications: + - ref: example_service + type: example_service_type + options: + # this section is different for each service diff --git a/docs/source/configuration/simulation/software/services/database_service.rst b/docs/source/configuration/simulation/software/services/database_service.rst deleted file mode 100644 index f03fde70..00000000 --- a/docs/source/configuration/simulation/software/services/database_service.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``DatabaseService`` -------------------- - -test diff --git a/docs/source/configuration/simulation/software/services/dns_client.rst b/docs/source/configuration/simulation/software/services/dns_client.rst deleted file mode 100644 index d9b8008d..00000000 --- a/docs/source/configuration/simulation/software/services/dns_client.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``DNSClient`` -------------- - -test diff --git a/docs/source/configuration/simulation/software/services/dns_server.rst b/docs/source/configuration/simulation/software/services/dns_server.rst deleted file mode 100644 index a342967f..00000000 --- a/docs/source/configuration/simulation/software/services/dns_server.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``DNSServer`` -------------- - -test diff --git a/docs/source/configuration/simulation/software/services/ftp_client.rst b/docs/source/configuration/simulation/software/services/ftp_client.rst deleted file mode 100644 index d51a3dc1..00000000 --- a/docs/source/configuration/simulation/software/services/ftp_client.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``FTPClient`` -------------- - -test diff --git a/docs/source/configuration/simulation/software/services/ftp_server.rst b/docs/source/configuration/simulation/software/services/ftp_server.rst deleted file mode 100644 index c7f92340..00000000 --- a/docs/source/configuration/simulation/software/services/ftp_server.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``FTPServer`` -------------- - -test diff --git a/docs/source/configuration/simulation/software/services/ntp_client.rst b/docs/source/configuration/simulation/software/services/ntp_client.rst deleted file mode 100644 index 51b2e061..00000000 --- a/docs/source/configuration/simulation/software/services/ntp_client.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``NTPClient`` -------------- - -test diff --git a/docs/source/configuration/simulation/software/services/ntp_server.rst b/docs/source/configuration/simulation/software/services/ntp_server.rst deleted file mode 100644 index 2efbdf1a..00000000 --- a/docs/source/configuration/simulation/software/services/ntp_server.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``NTPServer`` -------------- - -test diff --git a/docs/source/configuration/simulation/software/services/web_server.rst b/docs/source/configuration/simulation/software/services/web_server.rst deleted file mode 100644 index 4fab660d..00000000 --- a/docs/source/configuration/simulation/software/services/web_server.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -``WebServer`` -------------- - -test diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index cdae17dd..eb9b17c3 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -18,7 +18,7 @@ Game layer The game layer is responsible for managing agents and getting them to interface with the simulator correctly. It consists of several components: PrimAITE Session -^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^ ``PrimaiteSession`` is the main entry point into Primaite and it allows the simultaneous coordination of a simulation and agents that interact with it. ``PrimaiteSession`` keeps track of multiple agents of different types. diff --git a/docs/source/simulation_components/network/network_interfaces.rst b/docs/source/simulation_components/network/network_interfaces.rst index 9e1ad80a..f3d4d373 100644 --- a/docs/source/simulation_components/network/network_interfaces.rst +++ b/docs/source/simulation_components/network/network_interfaces.rst @@ -13,6 +13,8 @@ facilitates modular development, enhances maintainability, and supports scalabil allowing for focused enhancements within each layer. .. image:: primaite_network_interface_model.png + :width: 500 + :align: center Layer Descriptions ================== diff --git a/docs/source/simulation_components/network/nodes/firewall.rst b/docs/source/simulation_components/network/nodes/firewall.rst index 73168517..2f948081 100644 --- a/docs/source/simulation_components/network/nodes/firewall.rst +++ b/docs/source/simulation_components/network/nodes/firewall.rst @@ -229,7 +229,7 @@ To limit database server access to selected external IP addresses: position=7 ) -**Permitting DMZ Web Server Access while Blocking Specific Threats* +**Permitting DMZ Web Server Access while Blocking Specific Threats** To authorize HTTP/HTTPS access to a DMZ-hosted web server, excluding known malicious IPs: diff --git a/docs/source/simulation_components/network/nodes/network_node.rst b/docs/source/simulation_components/network/nodes/network_node.rst index eb9997ba..33bcea5b 100644 --- a/docs/source/simulation_components/network/nodes/network_node.rst +++ b/docs/source/simulation_components/network/nodes/network_node.rst @@ -27,7 +27,7 @@ in the transmission and routing of data within the simulated environment. **Key Features:** - **Frame Processing:** Central to the class is the ability to receive and process network frames, facilitating the -simulation of data flow through network devices. + simulation of data flow through network devices. - **Abstract Methods:** Includes abstract methods such as ``receive_frame``, which subclasses must implement to specify how devices handle incoming traffic. diff --git a/docs/source/simulation_components/system/data_manipulation_bot.rst b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst similarity index 99% rename from docs/source/simulation_components/system/data_manipulation_bot.rst rename to docs/source/simulation_components/system/applications/data_manipulation_bot.rst index 1fd5e5c8..8c326b56 100644 --- a/docs/source/simulation_components/system/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -16,6 +16,7 @@ The bot is intended to simulate a malicious actor carrying out attacks like: - Dropping tables - Deleting records - Modifying data + on a database server by abusing an application's trusted database connectivity. The bot performs attacks in the following stages to simulate the real pattern of an attack: diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst new file mode 100644 index 00000000..47690cb6 --- /dev/null +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -0,0 +1,38 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + + +DatabaseClient +=============== + +The DatabaseClient provides a client interface for connecting to the ``DatabaseService``. + +Key features +^^^^^^^^^^^^ + +- Connects to the ``DatabaseService`` via the ``SoftwareManager``. +- Handles connecting and disconnecting. +- Executes SQL queries and retrieves result sets. + +Usage +^^^^^ + +- Initialise with server IP address and optional password. +- Connect to the ``DatabaseService`` with ``connect``. +- Retrieve results in a dictionary. +- Disconnect when finished. + +To create database backups: + +- Configure the backup server on the ``DatabaseService`` by providing the Backup server ``IPv4Address`` with ``configure_backup`` +- Create a backup using ``backup_database``. This fails if the backup server is not configured. +- Restore a backup using ``restore_backup``. By default, this uses the database created via ``backup_database``. + +Implementation +^^^^^^^^^^^^^^ + +- Leverages ``SoftwareManager`` for sending payloads over the network. +- Connect and disconnect methods manage sessions. +- Payloads serialised as dictionaries for transmission. +- Extends base Application class. diff --git a/docs/source/configuration/simulation/software/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst similarity index 82% rename from docs/source/configuration/simulation/software/applications/dos_bot.rst rename to docs/source/simulation_components/system/applications/dos_bot.rst index 98939e5b..6aa849a7 100644 --- a/docs/source/configuration/simulation/software/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -2,7 +2,7 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -``DoSBot`` ----------- +DoSBot +------ test diff --git a/docs/source/simulation_components/system/web_browser_and_web_server_service.rst b/docs/source/simulation_components/system/applications/web_browser.rst similarity index 72% rename from docs/source/simulation_components/system/web_browser_and_web_server_service.rst rename to docs/source/simulation_components/system/applications/web_browser.rst index 538baa58..ee4e8b94 100644 --- a/docs/source/simulation_components/system/web_browser_and_web_server_service.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -2,35 +2,9 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -Web Browser and Web Server Service -================================== -Web Server Service ------------------- -Provides a Web Server simulation by extending the base Service class. - -Key capabilities -^^^^^^^^^^^^^^^^ - -- Simulates a web server with the capability to also request data from a database -- Allows the emulation of HTTP requests between client (e.g. a web browser) and server - - GET request sends a get all users request to the database server and returns an HTTP 200 status if the database is responsive -- Leverages the Service base class for install/uninstall, status tracking, etc. - -Usage -^^^^^ -- Install on a Node via the ``SoftwareManager`` to start the `WebServer`. -- Service runs on HTTP port 80 by default. (TODO: HTTPS) - -Implementation -^^^^^^^^^^^^^^ - -- HTTP request uses a ``HttpRequestPacket`` object -- HTTP response uses a ``HttpResponsePacket`` object -- Extends Service class for integration with ``SoftwareManager``. - -Web Browser (Web Client) ------------------------- +WebBrowser +========== The ``WebBrowser`` provides a client interface for connecting to the ``WebServer``. diff --git a/docs/source/simulation_components/system/database_client_server.rst b/docs/source/simulation_components/system/database_client_server.rst deleted file mode 100644 index 07912f3e..00000000 --- a/docs/source/simulation_components/system/database_client_server.rst +++ /dev/null @@ -1,71 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - - -Database Client Server -====================== - -Database Service ----------------- - -The ``DatabaseService`` provides a SQL database server simulation by extending the base Service class. - -Key capabilities -^^^^^^^^^^^^^^^^ - -- Creates a database file in the ``Node`` 's ``FileSystem`` upon creation. -- Handles connecting clients by maintaining a dictionary of connections mapped to session IDs. -- Authenticates connections using a configurable password. -- Simulates ``SELECT``, ``DELETE`` and ``INSERT`` SQL queries. -- Returns query results and status codes back to clients. -- Leverages the Service base class for install/uninstall, status tracking, etc. - -Usage -^^^^^ -- Install on a Node via the ``SoftwareManager`` to start the database service. -- Clients connect, execute queries, and disconnect. -- Service runs on TCP port 5432 by default. - -Implementation -^^^^^^^^^^^^^^ - -- Creates the database file within the node's file system. -- Manages client connections in a dictionary by session ID. -- Processes SQL queries. -- Returns results and status codes in a standard dictionary format. -- Extends Service class for integration with ``SoftwareManager``. - -Database Client ---------------- - -The DatabaseClient provides a client interface for connecting to the ``DatabaseService``. - -Key features -^^^^^^^^^^^^ - -- Connects to the ``DatabaseService`` via the ``SoftwareManager``. -- Handles connecting and disconnecting. -- Executes SQL queries and retrieves result sets. - -Usage -^^^^^ - -- Initialise with server IP address and optional password. -- Connect to the ``DatabaseService`` with ``connect``. -- Retrieve results in a dictionary. -- Disconnect when finished. - -To create database backups: - -- Configure the backup server on the ``DatabaseService`` by providing the Backup server ``IPv4Address`` with ``configure_backup`` -- Create a backup using ``backup_database``. This fails if the backup server is not configured. -- Restore a backup using ``restore_backup``. By default, this uses the database created via ``backup_database``. - -Implementation -^^^^^^^^^^^^^^ - -- Leverages ``SoftwareManager`` for sending payloads over the network. -- Connect and disconnect methods manage sessions. -- Payloads serialised as dictionaries for transmission. -- Extends base Application class. diff --git a/docs/source/simulation_components/system/list_of_applications.rst b/docs/source/simulation_components/system/list_of_applications.rst new file mode 100644 index 00000000..9aac23de --- /dev/null +++ b/docs/source/simulation_components/system/list_of_applications.rst @@ -0,0 +1,11 @@ +.. toctree:: + :maxdepth: 1 + + applications/data_manipulation_bot.rst + applications/database_client.rst + applications/dos_bot.rst + applications/web_browser.rst + +More info :py:mod:`primaite.game.game.APPLICATION_TYPES_MAPPING` + +.. include:: list_of_system_applications.rst diff --git a/docs/source/simulation_components/system/list_of_services.rst b/docs/source/simulation_components/system/list_of_services.rst new file mode 100644 index 00000000..07bc25ee --- /dev/null +++ b/docs/source/simulation_components/system/list_of_services.rst @@ -0,0 +1,15 @@ +.. toctree:: + :maxdepth: 1 + + services/database_service.rst + services/dns_client.rst + services/dns_server.rst + services/ftp_client.rst + services/ftp_server.rst + services/ntp_client.rst + services/ntp_server.rst + services/web_server.rst + +More info :py:mod:`primaite.game.game.SERVICE_TYPES_MAPPING` + +.. include:: list_of_system_services.rst diff --git a/docs/source/simulation_components/system/list_of_system_applications.rst b/docs/source/simulation_components/system/list_of_system_applications.rst new file mode 100644 index 00000000..ca5a7457 --- /dev/null +++ b/docs/source/simulation_components/system/list_of_system_applications.rst @@ -0,0 +1,19 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``system applications`` +""""""""""""""""""""""" + +Some applications are pre installed on nodes - this is similar to how some applications are included with the Operating System. + +The application may not be configured as needed, in which case, see the relevant application page. + +The list of applications that are considered system software are: + +.. toctree:: + :maxdepth: 1 + + applications/web_browser.rst + +More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` diff --git a/docs/source/simulation_components/system/list_of_system_services.rst b/docs/source/simulation_components/system/list_of_system_services.rst new file mode 100644 index 00000000..657faa52 --- /dev/null +++ b/docs/source/simulation_components/system/list_of_system_services.rst @@ -0,0 +1,21 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +``system services`` +""""""""""""""""""" + +Some services are pre installed on nodes - this is similar to how some services are included with the Operating System. + +The service may not be configured as needed, in which case, see the relevant service page. + +The list of services that are considered system software are: + +.. toctree:: + :maxdepth: 1 + + services/dns_client.rst + services/ftp_client.rst + services/ntp_client.rst + +More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst new file mode 100644 index 00000000..a4591d15 --- /dev/null +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -0,0 +1,33 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +DatabaseService +=============== + +The ``DatabaseService`` provides a SQL database server simulation by extending the base Service class. + +Key capabilities +^^^^^^^^^^^^^^^^ + +- Creates a database file in the ``Node`` 's ``FileSystem`` upon creation. +- Handles connecting clients by maintaining a dictionary of connections mapped to session IDs. +- Authenticates connections using a configurable password. +- Simulates ``SELECT``, ``DELETE`` and ``INSERT`` SQL queries. +- Returns query results and status codes back to clients. +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ +- Install on a Node via the ``SoftwareManager`` to start the database service. +- Clients connect, execute queries, and disconnect. +- Service runs on TCP port 5432 by default. + +Implementation +^^^^^^^^^^^^^^ + +- Creates the database file within the node's file system. +- Manages client connections in a dictionary by session ID. +- Processes SQL queries. +- Returns results and status codes in a standard dictionary format. +- Extends Service class for integration with ``SoftwareManager``. diff --git a/docs/source/simulation_components/system/dns_client_server.rst b/docs/source/simulation_components/system/services/dns_client.rst similarity index 52% rename from docs/source/simulation_components/system/dns_client_server.rst rename to docs/source/simulation_components/system/services/dns_client.rst index f57f903b..f961ece3 100644 --- a/docs/source/simulation_components/system/dns_client_server.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -2,34 +2,8 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -DNS Client Server -================= - -DNS Server ----------- -Also known as a DNS Resolver, the ``DNSServer`` provides a DNS Server simulation by extending the base Service class. - -Key capabilities -^^^^^^^^^^^^^^^^ - -- Simulates DNS requests and DNSPacket transfer across a network -- Registers domain names and the IP Address linked to the domain name -- Returns the IP address for a given domain name within a DNS Packet that a DNS Client can read -- Leverages the Service base class for install/uninstall, status tracking, etc. - -Usage -^^^^^ -- Install on a Node via the ``SoftwareManager`` to start the database service. -- Service runs on TCP port 53 by default. (TODO: TCP for now, should be UDP in future) - -Implementation -^^^^^^^^^^^^^^ - -- DNS request and responses use a ``DNSPacket`` object -- Extends Service class for integration with ``SoftwareManager``. - -DNS Client ----------- +DNSClient +========= The DNSClient provides a client interface for connecting to the ``DNSServer``. diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst new file mode 100644 index 00000000..ef463d9a --- /dev/null +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -0,0 +1,26 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +DNSServer +========= +Also known as a DNS Resolver, the ``DNSServer`` provides a DNS Server simulation by extending the base Service class. + +Key capabilities +^^^^^^^^^^^^^^^^ + +- Simulates DNS requests and DNSPacket transfer across a network +- Registers domain names and the IP Address linked to the domain name +- Returns the IP address for a given domain name within a DNS Packet that a DNS Client can read +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ +- Install on a Node via the ``SoftwareManager`` to start the database service. +- Service runs on TCP port 53 by default. (TODO: TCP for now, should be UDP in future) + +Implementation +^^^^^^^^^^^^^^ + +- DNS request and responses use a ``DNSPacket`` object +- Extends Service class for integration with ``SoftwareManager``. diff --git a/docs/source/simulation_components/system/ftp_client_server.rst b/docs/source/simulation_components/system/services/ftp_client.rst similarity index 78% rename from docs/source/simulation_components/system/ftp_client_server.rst rename to docs/source/simulation_components/system/services/ftp_client.rst index a544b4c8..77111938 100644 --- a/docs/source/simulation_components/system/ftp_client_server.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -2,35 +2,9 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -FTP Client Server -================= -FTP Server ----------- -Provides a FTP Client-Server simulation by extending the base Service class. - -Key capabilities -^^^^^^^^^^^^^^^^ - -- Simulates FTP requests and FTPPacket transfer across a network -- Allows the emulation of FTP commands between an FTP client and server: - - STOR: stores a file from client to server - - RETR: retrieves a file from the FTP server -- Leverages the Service base class for install/uninstall, status tracking, etc. - -Usage -^^^^^ -- Install on a Node via the ``SoftwareManager`` to start the FTP server service. -- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) - -Implementation -^^^^^^^^^^^^^^ - -- FTP request and responses use a ``FTPPacket`` object -- Extends Service class for integration with ``SoftwareManager``. - -FTP Client ----------- +FTPClient +========= The ``FTPClient`` provides a client interface for connecting to the ``FTPServer``. diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst new file mode 100644 index 00000000..81f51e6b --- /dev/null +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -0,0 +1,27 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +FTPServer +========= +Provides a FTP Client-Server simulation by extending the base Service class. + +Key capabilities +^^^^^^^^^^^^^^^^ + +- Simulates FTP requests and FTPPacket transfer across a network +- Allows the emulation of FTP commands between an FTP client and server: + - STOR: stores a file from client to server + - RETR: retrieves a file from the FTP server +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ +- Install on a Node via the ``SoftwareManager`` to start the FTP server service. +- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) + +Implementation +^^^^^^^^^^^^^^ + +- FTP request and responses use a ``FTPPacket`` object +- Extends Service class for integration with ``SoftwareManager``. diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst new file mode 100644 index 00000000..27cd27e4 --- /dev/null +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -0,0 +1,26 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +NTPClient +========= + +The NTPClient provides a client interface for connecting to the ``NTPServer``. + +Key features +^^^^^^^^^^^^ + +- Connects to the ``NTPServer`` via the ``SoftwareManager``. + +Usage +^^^^^ + +- Install on a Node via the ``SoftwareManager`` to start the database service. +- Service runs on UDP port 123 by default. + +Implementation +^^^^^^^^^^^^^^ + +- Leverages ``SoftwareManager`` for sending payloads over the network. +- Provides easy interface for Nodes to find IP addresses via domain names. +- Extends base Service class. diff --git a/docs/source/simulation_components/system/ntp_client_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst similarity index 56% rename from docs/source/simulation_components/system/ntp_client_server.rst rename to docs/source/simulation_components/system/services/ntp_server.rst index b6d57c13..066ad5ac 100644 --- a/docs/source/simulation_components/system/ntp_client_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -2,11 +2,8 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -NTP Client Server -================= - -NTP Server ----------- +NTPServer +========= The ``NTPServer`` provides a NTP Server simulation by extending the base Service class. NTP Client @@ -29,26 +26,3 @@ Implementation - NTP request and responses use a ``NTPPacket`` object - Extends Service class for integration with ``SoftwareManager``. - -NTP Client ----------- - -The NTPClient provides a client interface for connecting to the ``NTPServer``. - -Key features -^^^^^^^^^^^^ - -- Connects to the ``NTPServer`` via the ``SoftwareManager``. - -Usage -^^^^^ - -- Install on a Node via the ``SoftwareManager`` to start the database service. -- Service runs on UDP port 123 by default. - -Implementation -^^^^^^^^^^^^^^ - -- Leverages ``SoftwareManager`` for sending payloads over the network. -- Provides easy interface for Nodes to find IP addresses via domain names. -- Extends base Service class. diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst new file mode 100644 index 00000000..ae3f32e6 --- /dev/null +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -0,0 +1,27 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +WebServer +========= +Provides a Web Server simulation by extending the base Service class. + +Key capabilities +^^^^^^^^^^^^^^^^ + +- Simulates a web server with the capability to also request data from a database +- Allows the emulation of HTTP requests between client (e.g. a web browser) and server + - GET request sends a get all users request to the database server and returns an HTTP 200 status if the database is responsive +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ +- Install on a Node via the ``SoftwareManager`` to start the `WebServer`. +- Service runs on HTTP port 80 by default. (TODO: HTTPS) + +Implementation +^^^^^^^^^^^^^^ + +- HTTP request uses a ``HttpRequestPacket`` object +- HTTP response uses a ``HttpResponsePacket`` object +- Extends Service class for integration with ``SoftwareManager``. 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 a550faf1..8af96e87 100644 --- a/docs/source/simulation_components/system/session_and_software_manager.rst +++ b/docs/source/simulation_components/system/session_and_software_manager.rst @@ -16,6 +16,8 @@ ARP, ICMP, or the Web Client. This pathway exemplifies the structured processing each frame reaches its intended target within the simulated environment. .. image:: node_session_software_model_example.png + :width: 500 + :align: center Session Manager --------------- diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index 7a1359f4..459064f0 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -39,16 +39,27 @@ See :ref:`Node Start up and Shut down` assert node.operating_state is NodeOperatingState.ON assert web_server.operating_state is ServiceOperatingState.RUNNING # service turned back on when node is powered on +.. _List of Applications: -Services, Processes and Applications: -##################################### +Applications +############ -.. toctree:: - :maxdepth: 2 +These are a list of applications that are currently available in PrimAITE: - database_client_server - data_manipulation_bot - dns_client_server - ftp_client_server - ntp_client_server - web_browser_and_web_server_service +.. include:: list_of_applications.rst + +.. _List of Services: + +Services +######## + +These are a list of services that are currently available in PrimAITE: + +.. include:: list_of_services.rst + +.. _List of Processes: + +Processes +######### + +`To be implemented` diff --git a/docs/source/simulation_structure.rst b/docs/source/simulation_structure.rst index 6e0ab5ce..f9a69b26 100644 --- a/docs/source/simulation_structure.rst +++ b/docs/source/simulation_structure.rst @@ -12,14 +12,15 @@ and a domain controller for managing software and users. Each node of the simulation 'tree' has responsibility for creating, deleting, and updating its direct descendants. Also, when a component's ``describe_state()`` method is called, it will include the state of its descendants. The -``apply_request()`` method can be used to act on a component or one of its descendatnts. The diagram below shows the +``apply_request()`` method can be used to act on a component or one of its descendants. The diagram below shows the relationship between components. -.. image:: _static/component_relationship.png +.. image:: ../../_static/component_relationship.png :width: 500 - :alt: The top level simulation object owns a NetworkContainer and a DomainController. The DomainController has a - list of accounts. The network container has links and nodes. Nodes can own switchports, NICs, FileSystem, - Application, Service, and Process. + :align: center + :alt: :: The top level simulation object owns a NetworkContainer and a DomainController. The DomainController has a + list of accounts. The network container has links and nodes. Nodes can own switchports, NICs, FileSystem, + Application, Service, and Process. Actions From f933341df521feaca5e494bf739833b86d75ab28 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 23 Feb 2024 10:06:48 +0000 Subject: [PATCH 020/128] eod commit --- src/primaite/game/game.py | 10 ---------- src/primaite/simulator/core.py | 8 ++++++-- src/primaite/simulator/network/container.py | 8 ++++---- src/primaite/simulator/network/hardware/base.py | 14 +++++++------- .../simulator/network/hardware/nodes/router.py | 10 +++++----- src/primaite/simulator/sim_container.py | 4 ++-- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index bd7ed2cd..72ad01e7 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -160,16 +160,6 @@ class PrimaiteGame: return True return False - def reset(self) -> None: # TODO: deprecated - remove me - """Reset the game, this will reset the simulation.""" - self.episode_counter += 1 - self.step_counter = 0 - _LOGGER.debug(f"Resetting primaite game, episode = {self.episode_counter}") - self.simulation.reset_component_for_episode(episode=self.episode_counter) - for agent in self.agents: - agent.reward_function.total_reward = 0.0 - agent.reset_agent_for_episode() - def close(self) -> None: """Close the game, this will close the simulation.""" return NotImplemented diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index e21ce9eb..b9188bf0 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -160,8 +160,12 @@ class SimComponent(BaseModel): self._request_manager: RequestManager = self._init_request_manager() self._parent: Optional["SimComponent"] = None - def reset_component_for_episode(self, episode: int): - """Reset the original state of the SimComponent.""" + def setup_for_episode(self, episode: int): + """ + Perform any additional setup on this component that can't happen during __init__. + + For instance, some components may require for the entire network to exist before some configuration can be set. + """ pass def _init_request_manager(self) -> RequestManager: diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 48205bbd..080a1004 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -45,12 +45,12 @@ class Network(SimComponent): self._nx_graph = MultiGraph() - def reset_component_for_episode(self, episode: int): + def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" for node in self.nodes.values(): - node.reset_component_for_episode(episode) + node.setup_for_episode(episode) for link in self.links.values(): - link.reset_component_for_episode(episode) + link.setup_for_episode(episode) for node in self.nodes.values(): node.power_on() @@ -171,7 +171,7 @@ class Network(SimComponent): def clear_links(self): """Clear all the links in the network by resetting their component state for the episode.""" for link in self.links.values(): - link.reset_component_for_episode() + link.setup_for_episode() def draw(self, seed: int = 123): """ diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 67ac42c8..e2a90db1 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -123,9 +123,9 @@ class NIC(SimComponent): _LOGGER.error(msg) raise ValueError(msg) - def reset_component_for_episode(self, episode: int): + def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - super().reset_component_for_episode(episode) + super().setup_for_episode(episode) if episode and self.pcap: self.pcap.current_episode = episode self.pcap.setup_logger() @@ -1011,19 +1011,19 @@ class Node(SimComponent): self.session_manager.software_manager = self.software_manager self._install_system_software() - def reset_component_for_episode(self, episode: int): + def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - super().reset_component_for_episode(episode) + super().setup_for_episode(episode) # Reset File System - self.file_system.reset_component_for_episode(episode) + self.file_system.setup_for_episode(episode) # Reset all Nics for nic in self.nics.values(): - nic.reset_component_for_episode(episode) + nic.setup_for_episode(episode) for software in self.software_manager.software.values(): - software.reset_component_for_episode(episode) + software.setup_for_episode(episode) if episode and self.sys_log: self.sys_log.current_episode = episode diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index aa154ad9..887bc9be 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -743,16 +743,16 @@ class Router(Node): self.arp.nics = self.nics self.icmp.arp = self.arp - def reset_component_for_episode(self, episode: int): + def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" self.arp.clear() - self.acl.reset_component_for_episode(episode) - self.route_table.reset_component_for_episode(episode) + self.acl.setup_for_episode(episode) + self.route_table.setup_for_episode(episode) for i, nic in self.ethernet_ports.items(): - nic.reset_component_for_episode(episode) + nic.setup_for_episode(episode) self.enable_port(i) - super().reset_component_for_episode(episode) + super().setup_for_episode(episode) def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 18ed894c..bb6132a8 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -21,9 +21,9 @@ class Simulation(SimComponent): super().__init__(**kwargs) - def reset_component_for_episode(self, episode: int): + def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - self.network.reset_component_for_episode(episode) + self.network.setup_for_episode(episode) def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() From 52677538a89f9f5c5ffa88b72e0b0e0415f85cc6 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 23 Feb 2024 15:12:46 +0000 Subject: [PATCH 021/128] #2238 - Tidied up code, added more docstrings, and implemented suggestions from PR. --- .../network/network_interfaces.rst | 2 +- src/primaite/game/agent/observations.py | 4 +--- .../simulator/network/hardware/base.py | 18 ++++++++++++------ .../network/hardware/nodes/host/host_node.py | 6 +----- src/primaite/simulator/network/nmne.py | 1 + 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/source/simulation_components/network/network_interfaces.rst b/docs/source/simulation_components/network/network_interfaces.rst index c74b54ae..2bb8dda4 100644 --- a/docs/source/simulation_components/network/network_interfaces.rst +++ b/docs/source/simulation_components/network/network_interfaces.rst @@ -71,7 +71,7 @@ Network Interface Classes - Malicious Network Events Monitoring: * Enhances network interfaces with the capability to monitor and capture Malicious Network Events (MNEs) based on predefined criteria such as specific keywords or traffic patterns. - * Integrates NMNE detection functionalities, leveraging configurable settings like ``capture_nmne``, `nmne_capture_keywords``, and observation mechanisms such as ``NicObservation`` to classify and record network anomalies. + * Integrates Number of Malicious Network Events (NMNE) detection functionalities, leveraging configurable settings like ``capture_nmne``, `nmne_capture_keywords``, and observation mechanisms such as ``NicObservation`` to classify and record network anomalies. * Offers an additional layer of security and data analysis, crucial for identifying and mitigating malicious activities within the network infrastructure. Provides vital information for network security analysis and reinforcement learning algorithms. **WiredNetworkInterface (Connection Type Layer)** diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index 1d8799fd..7ccc3f11 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -352,8 +352,6 @@ class NicObservation(AbstractObservation): """The default NIC observation dict.""" data = {"nic_status": 0} - if CAPTURE_NMNE: - data.update({"nmne": {"inbound": 0, "outbound": 0}}) return data def __init__(self, where: Optional[Tuple[str]] = None) -> None: @@ -407,7 +405,7 @@ class NicObservation(AbstractObservation): return self.default_observation else: obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2, "nmne": {}} - if CAPTURE_NMNE: + if CAPTURE_NMNE and nic_state.get("nmne"): direction_dict = nic_state["nmne"].get("direction", {}) inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) inbound_count = inbound_keywords.get("*", 0) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index c0e69e60..b22bea25 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -98,6 +98,7 @@ class NetworkInterface(SimComponent, ABC): "A PacketCapture instance for capturing and analysing packets passing through this interface." nmne: Dict = Field(default_factory=lambda: {}) + "A dict containing details of the number of malicious network events captured." def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() @@ -122,7 +123,6 @@ class NetworkInterface(SimComponent, ABC): "enabled": self.enabled, } ) - state.update({"nmne": self.nmne}) return state def reset_component_for_episode(self, episode: int): @@ -134,23 +134,29 @@ class NetworkInterface(SimComponent, ABC): self.pcap.setup_logger() self.enable() - # @abstractmethod + @abstractmethod def enable(self): """Enable the interface.""" pass - # @abstractmethod + @abstractmethod def disable(self): """Disable the interface.""" pass - def _capture_nmne(self, frame: Frame, inbound: bool = True): + def _capture_nmne(self, frame: Frame, inbound: bool = True) -> None: """ Processes and captures network frame data based on predefined global NMNE settings. This method updates the NMNE structure with counts of malicious network events based on the frame content and direction. The structure is dynamically adjusted according to the enabled capture settings. + .. note:: + While there is a lot of logic in this code that defines a multi-level hierarchical NMNE structure, + most of it is unused for now as a result of all `CAPTURE_BY_<>` variables in + ``primaite.simulator.network.nmne`` being hardcoded and set as final. Once they're 'released' and made + configurable, this function will be updated to properly explain the dynamic data structure. + :param frame: The network frame to process, containing IP, TCP/UDP, and payload information. :param inbound: Boolean indicating if the frame direction is inbound. Defaults to True. """ @@ -214,7 +220,7 @@ class NetworkInterface(SimComponent, ABC): # Increment a generic counter if keyword capturing is not enabled keyword_level["*"] = keyword_level.get("*", 0) + 1 - # @abstractmethod + @abstractmethod def send_frame(self, frame: Frame) -> bool: """ Attempts to send a network frame through the interface. @@ -224,7 +230,7 @@ class NetworkInterface(SimComponent, ABC): """ self._capture_nmne(frame, inbound=False) - # @abstractmethod + @abstractmethod def receive_frame(self, frame: Frame) -> bool: """ Receives a network frame on the interface. 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 6ecd6733..8e104924 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -205,11 +205,7 @@ class NIC(IPWiredNetworkInterface): state = super().describe_state() # Update the state with NIC-specific information - state.update( - { - "wake_on_lan": self.wake_on_lan, - } - ) + state.update({"wake_on_lan": self.wake_on_lan, "nmne": self.nmne}) return state diff --git a/src/primaite/simulator/network/nmne.py b/src/primaite/simulator/network/nmne.py index d4c40631..87839712 100644 --- a/src/primaite/simulator/network/nmne.py +++ b/src/primaite/simulator/network/nmne.py @@ -6,6 +6,7 @@ CAPTURE_NMNE: bool = True NMNE_CAPTURE_KEYWORDS: List[str] = [] """List of keywords to identify malicious network events.""" +# TODO: Remove final and make configurable after example layout when the NicObservation creates nmne structure dynamically CAPTURE_BY_DIRECTION: Final[bool] = True """Flag to determine if captures should be organized by traffic direction (inbound/outbound).""" CAPTURE_BY_IP_ADDRESS: Final[bool] = False From fb148dc4fb100ae50545913094537bc5b7dfa3b2 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 23 Feb 2024 16:49:01 +0000 Subject: [PATCH 022/128] #2257: applications and services docs --- docs/source/configuration/simulation.rst | 7 +- .../simulation_components/network/network.rst | 10 +- .../applications/data_manipulation_bot.rst | 92 +++++++++-- .../system/applications/database_client.rst | 90 +++++++++- .../system/applications/dos_bot.rst | 156 +++++++++++++++++- .../system/applications/web_browser.rst | 103 +++++++----- .../system/common/common_configuration.rst | 14 ++ .../system/list_of_applications.rst | 8 +- .../system/list_of_services.rst | 12 +- .../system/list_of_system_applications.rst | 5 +- .../system/list_of_system_services.rst | 9 +- .../system/services/database_service.rst | 84 +++++++++- .../system/services/dns_client.rst | 83 +++++++++- .../system/services/dns_server.rst | 80 ++++++++- .../system/services/ftp_client.rst | 103 +++++------- .../system/services/ftp_server.rst | 74 ++++++++- .../system/services/ntp_client.rst | 77 ++++++++- .../system/services/ntp_server.rst | 68 +++++++- .../system/services/web_server.rst | 67 +++++++- src/primaite/game/game.py | 3 +- .../services/database/database_service.py | 3 + 21 files changed, 956 insertions(+), 192 deletions(-) create mode 100644 docs/source/simulation_components/system/common/common_configuration.rst diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index f24cc41d..89c1669b 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -29,12 +29,9 @@ To see the configuration for these nodes, refer to the following: .. toctree:: :maxdepth: 1 + :glob: - simulation/nodes/computer.rst - simulation/nodes/firewall.rst - simulation/nodes/router.rst - simulation/nodes/server.rst - simulation/nodes/switch.rst + simulation/nodes/* ``links`` --------- diff --git a/docs/source/simulation_components/network/network.rst b/docs/source/simulation_components/network/network.rst index 533a15f2..36e8ee48 100644 --- a/docs/source/simulation_components/network/network.rst +++ b/docs/source/simulation_components/network/network.rst @@ -30,11 +30,11 @@ we'll use the following Network that has a client, server, two switches, and a r .. code-block:: python from primaite.simulator.network.container import Network - from primaite.simulator.network.hardware.base import NIC - from primaite.simulator.network.hardware.nodes.computer import Computer - from primaite.simulator.network.hardware.nodes.router import Router, ACLAction - from primaite.simulator.network.hardware.nodes.server import Server - from primaite.simulator.network.hardware.nodes.switch import Switch + from primaite.simulator.network.hardware.base import NetworkInterface + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.network.hardware.nodes.network.router import Router, ACLAction + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port 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 8c326b56..209cdcbd 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -2,14 +2,15 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _DataManipulationBot: DataManipulationBot -=================== +################### -The ``DataManipulationBot`` class provides functionality to connect to a ``DatabaseService`` and execute malicious SQL statements. +The ``DataManipulationBot`` class provides functionality to connect to a :ref:`DatabaseService` and execute malicious SQL statements. Overview --------- +======== The bot is intended to simulate a malicious actor carrying out attacks like: @@ -28,7 +29,7 @@ The bot performs attacks in the following stages to simulate the real pattern of Each of these stages has a random, configurable probability of succeeding (by default 10%). The bot can also be configured to repeat the attack once complete. Usage ------ +===== - Create an instance and call ``configure`` to set: - Target database server IP @@ -41,16 +42,35 @@ The bot handles connecting, executing the statement, and disconnecting. In a simulation, the bot can be controlled by using ``DataManipulationAgent`` which calls ``run`` on the bot at configured timesteps. -Example -------- +Implementation +============== + +The bot extends :ref:`DatabaseClient` and leverages its connectivity. + +- Uses the Application base class for lifecycle management. +- Credentials, target IP and other options set via ``configure``. +- ``run`` handles connecting, executing statement, and disconnecting. +- SQL payload executed via ``query`` method. +- Results in malicious SQL being executed on remote database server. + + +Examples +======== + +Python +"""""" .. code-block:: python + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot + client_1 = Computer( hostname="client_1", ip_address="192.168.10.21", subnet_mask="255.255.255.0", - default_gateway="192.168.10.1" + default_gateway="192.168.10.1", operating_state=NodeOperatingState.ON # initialise the computer in an ON state ) network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) @@ -62,13 +82,13 @@ Example This would connect to the database service at 192.168.1.14, authenticate, and execute the SQL statement to drop the 'users' table. Example with ``DataManipulationAgent`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +"""""""""""""""""""""""""""""""""""""" If not using the data manipulation bot manually, it needs to be used with a data manipulation agent. Below is an example section of configuration file for setting up a simulation with data manipulation bot and agent. .. code-block:: yaml - game_config: + game: # ... agents: - ref: data_manipulation_red_bot @@ -129,13 +149,51 @@ If not using the data manipulation bot manually, it needs to be used with a data payload: "DELETE" server_ip: 192.168.1.14 -Implementation --------------- +Configuration +============= -The bot extends ``DatabaseClient`` and leverages its connectivity. +.. include:: ../common/common_configuration.rst -- Uses the Application base class for lifecycle management. -- Credentials, target IP and other options set via ``configure``. -- ``run`` handles connecting, executing statement, and disconnecting. -- SQL payload executed via ``query`` method. -- Results in malicious SQL being executed on remote database server. +.. |SOFTWARE_NAME| replace:: DataManipulationBot +.. |SOFTWARE_NAME_BACKTICK| replace:: ``DataManipulationBot`` + +``server_ip`` +""""""""""""" + +IP address of the :ref:`DatabaseService` which the ``DataManipulationBot`` will try to attack. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``server_password`` +""""""""""""""""""" + +Optional. Default value is ``None``. + +The password that the ``DataManipulationBot`` will use to access the :ref:`DatabaseService`. + +``payload`` +""""""""""" + +Optional. Default value is ``DELETE``. + +The payload that the ``DataManipulationBot`` will send to the :ref:`DatabaseService`. + +See :ref:`Database Payload List` + +``port_scan_p_of_success`` +"""""""""""""""""""""""""" + +Optional. Default value is ``0.1``. + +The chance of the ``DataManipulationBot`` to succeed with a port scan (and therefore continue the attack). + +This must be a float value between ``0`` and ``1``. + +``data_manipulation_p_of_success`` +"""""""""""""""""""""""""""""""""" + +Optional. Default value is ``0.1``. + +The chance of the ``DataManipulationBot`` to succeed with a data manipulation attack. + +This must be a float value between ``0`` and ``1``. diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 47690cb6..61d955f2 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -2,37 +2,111 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _DatabaseClient: DatabaseClient -=============== +############## -The DatabaseClient provides a client interface for connecting to the ``DatabaseService``. +The ``DatabaseClient`` provides a client interface for connecting to the :ref:`DatabaseService`. Key features -^^^^^^^^^^^^ +============ -- Connects to the ``DatabaseService`` via the ``SoftwareManager``. +- Connects to the :ref:`DatabaseService` via the ``SoftwareManager``. - Handles connecting and disconnecting. - Executes SQL queries and retrieves result sets. Usage -^^^^^ +===== - Initialise with server IP address and optional password. -- Connect to the ``DatabaseService`` with ``connect``. +- Connect to the :ref:`DatabaseService` with ``connect``. - Retrieve results in a dictionary. - Disconnect when finished. To create database backups: -- Configure the backup server on the ``DatabaseService`` by providing the Backup server ``IPv4Address`` with ``configure_backup`` +- Configure the backup server on the :ref:`DatabaseService` by providing the Backup server ``IPv4Address`` with ``configure_backup`` - Create a backup using ``backup_database``. This fails if the backup server is not configured. - Restore a backup using ``restore_backup``. By default, this uses the database created via ``backup_database``. Implementation -^^^^^^^^^^^^^^ +============== - Leverages ``SoftwareManager`` for sending payloads over the network. - Connect and disconnect methods manage sessions. - Payloads serialised as dictionaries for transmission. - Extends base Application class. + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.applications.database_client import DatabaseClient + + client = Computer( + hostname="client", + ip_address="192.168.10.21", + subnet_mask="255.255.255.0", + default_gateway="192.168.10.1", + operating_state=NodeOperatingState.ON # initialise the computer in an ON state + ) + + # install DatabaseClient + client.software_manager.install(DatabaseClient) + + database_client: DatabaseClient = client.software_manager.software.get("DatabaseClient") + + # Configure the DatabaseClient + database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) # address of the DatabaseService + database_client.run() + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_computer + hostname: example_computer + type: computer + ... + applications: + - ref: database_client + type: DatabaseClient + options: + db_server_ip: 192.168.0.1 + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: DatabaseClient +.. |SOFTWARE_NAME_BACKTICK| replace:: ``DatabaseClient`` + + +``db_server_ip`` +"""""""""""""""" + +IP address of the :ref:`DatabaseService` that the ``DatabaseClient`` will connect to + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``server_password`` +""""""""""""""""""" + +Optional. Default value is ``None``. + +The password that the ``DatabaseClient`` will use to access the :ref:`DatabaseService`. diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index 6aa849a7..fcf3f207 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -2,7 +2,157 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -DoSBot ------- +.. _DoSBot: -test +DoSBot +###### + +The ``DoSBot`` is an implementation of a Denial of Service attack within the PrimAITE simulation. This specifically simulates a `Slow Loris attack `. + +Key features +============ + +- Connects to the :ref:`DatabaseService` via the ``SoftwareManager``. +- Makes many connections to the :ref:`DatabaseService` which ends up using up the available connections. + +Usage +===== + +- Configure with target IP address and optional password. +- use ``run`` to run the application_loop of DoSBot to begin attacks +- DoSBot runs through different actions at each timestep + +Implementation +============== + +- Leverages :ref:`DatabaseClient` to create connections with :ref`DatabaseServer`. +- Extends base Application class. + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.nodes.host.computer import Computer + from primaite.simulator.system.applications.red_applications.dos_bot import DoSBot + + # Create Computer + computer = Computer( + hostname="computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + computer.power_on() + + # Install DoSBot on computer + computer.software_manager.install(DoSBot) + dos_bot: DoSBot = computer.software_manager.software.get("DoSBot") + + # Configure the DoSBot + dos_bot.configure( + target_ip_address=IPv4Address("192.168.0.10"), + payload="SPOOF DATA", + repeat=False, + port_scan_p_of_success=0.8, + dos_intensity=1.0, + max_sessions=1000 + ) + + # run DoSBot + dos_bot.run() + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_computer + hostname: example_computer + type: computer + ... + applications: + - ref: dos_bot + type: DoSBot + options: + target_ip_address: 192.168.0.10 + payload: SPOOF DATA + repeat: False + port_scan_p_of_success: 0.8 + dos_intensity: 1.0 + max_sessions: 1000 + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: DoSBot +.. |SOFTWARE_NAME_BACKTICK| replace:: ``DoSBot`` + +``target_ip_address`` +""""""""""""""""""""" + +IP address of the :ref:`DatabaseService` which the ``DataManipulationBot`` will try to attack. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``target_port`` +""""""""""""""" + +Optional. Default value is ``5432``. + +Port of the target service. + +See :ref:`List of IPProtocols ` for a list of protocols. + +``payload`` +""""""""""" + +Optional. Default value is ``None``. + +The payload that the ``DoSBot`` sends as part of its attack. + +``repeat`` +"""""""""" + +Optional. Default value is ``False``. + +If ``True`` the ``DoSBot`` will maintain its attack. + +``port_scan_p_of_success`` +"""""""""""""""""""""""""" + +Optional. Default value is ``0.1``. + +The chance of the ``DoSBot`` to succeed with a port scan (and therefore continue the attack). + +This must be a float value between ``0`` and ``1``. + +``dos_intensity`` +""""""""""""""""" + +Optional. Default value is ``1.0``. + +The intensity of the Denial of Service attack. This is multiplied by the number of ``max_sessions``. + +This must be a float value between ``0`` and ``1``. + +``max_sessions`` +"""""""""""""""" + +Optional. Default value is ``1000``. + +The maximum number of sessions the ``DoSBot`` is able to make. + +This must be an integer value above equal to or greater than ``0``. diff --git a/docs/source/simulation_components/system/applications/web_browser.rst b/docs/source/simulation_components/system/applications/web_browser.rst index ee4e8b94..c46089ba 100644 --- a/docs/source/simulation_components/system/applications/web_browser.rst +++ b/docs/source/simulation_components/system/applications/web_browser.rst @@ -2,16 +2,17 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _WebBrowser: WebBrowser -========== +########## -The ``WebBrowser`` provides a client interface for connecting to the ``WebServer``. +The ``WebBrowser`` provides a client interface for connecting to the :ref:`WebServer`. Key features -^^^^^^^^^^^^ +============ -- Connects to the ``WebServer`` via the ``SoftwareManager``. +- Connects to the :ref:`WebServer` via the ``SoftwareManager``. - Simulates HTTP requests and HTTP packet transfer across a network - Allows the emulation of HTTP requests between client and server: - Automatically uses ``DNSClient`` to resolve domain names @@ -19,66 +20,92 @@ Key features - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the ``WebBrowser``. - Service runs on HTTP port 80 by default. (TODO: HTTPS) - Execute sending an HTTP GET request with ``get_webpage`` Implementation -^^^^^^^^^^^^^^ +============== - Leverages ``SoftwareManager`` for sending payloads over the network. - Provides easy interface for making HTTP requests between an HTTP client and server. - Extends base Service class. -Example Usage -------------- +Examples +======== -Dependencies -^^^^^^^^^^^^ +Python +"""""" + +The ``WebBrowser`` utilises :ref:`DNSClient` and :ref:`DNSServer` to resolve a URL. + +The :ref:`DNSClient` must be configured to use the :ref:`DNSServer`. The :ref:`DNSServer` should be configured to have the ``WebBrowser`` ``target_url`` within its ``domain_mapping``. .. code-block:: python - from primaite.simulator.network.container import Network - from primaite.simulator.network.hardware.nodes.computer import Computer - from primaite.simulator.network.hardware.nodes.server import Server + from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.applications.web_browser import WebBrowser - from primaite.simulator.system.services.web_server.web_server_service import WebServer -Example peer to peer network -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + # Create Computer + computer = Computer( + hostname="computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + computer.power_on() -.. code-block:: python + # Install WebBrowser on computer + computer.software_manager.install(WebBrowser) + web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser.run() - net = Network() + # configure the WebBrowser + web_browser.target_url = "arcd.com" - pc1 = Computer(hostname="pc1", ip_address="192.168.1.50", subnet_mask="255.255.255.0") - srv = Server(hostname="srv", ip_address="192.168.1.10", subnet_mask="255.255.255.0") - pc1.power_on() - srv.power_on() - net.connect(pc1.network_interface[1], srv.network_interface[1]) + # once DNS server is configured with the correct domain mapping + # this should work + web_browser.get_webpage() -Install the Web Server -^^^^^^^^^^^^^^^^^^^^^^ +Via Configuration +""""""""""""""""" -.. code-block:: python +.. code-block:: yaml - # web browser is automatically installed in computer nodes - # IRL this is usually included with an OS - client: WebBrowser = pc1.software_manager.software['WebBrowser'] + simulation: + network: + nodes: + - ref: example_computer + hostname: example_computer + type: computer + ... + applications: + - ref: web_browser + type: WebBrowser + options: + target_url: http://arcd.com/ - # install web server - srv.software_manager.install(WebServer) - webserv: WebServer = srv.software_manager.software['WebServer'] +Configuration +============= -Open the web page -^^^^^^^^^^^^^^^^^ +.. include:: ../common/common_configuration.rst -Using a domain name to connect to a website requires setting up DNS Servers. For this example, it is possible to use the IP address directly +.. |SOFTWARE_NAME| replace:: WebBrowser +.. |SOFTWARE_NAME_BACKTICK| replace:: ``WebBrowser`` -.. code-block:: python +``target_url`` +"""""""""""""" - # check that the get request succeeded - print(client.get_webpage("http://192.168.1.10")) # should be True +The URL that the ``WebBrowser`` will request when ``get_webpage`` is called without parameters. + +The URL can be in any format so long as the domain is within it e.g. + +The domain ``arcd.com`` can be matched by + +- http://arcd.com/ +- http://arcd.com/users/ +- arcd.com diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst new file mode 100644 index 00000000..86991655 --- /dev/null +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -0,0 +1,14 @@ +``ref`` +======= + +Human readable name used as reference for the |SOFTWARE_NAME_BACKTICK|. Not used in code. + +``type`` +======== + +The type of software that should be added. To add |SOFTWARE_NAME| this must be |SOFTWARE_NAME_BACKTICK|. + +``options`` +=========== + +The configuration options are the attributes that fall under the options for an application. diff --git a/docs/source/simulation_components/system/list_of_applications.rst b/docs/source/simulation_components/system/list_of_applications.rst index 9aac23de..0ba0c45c 100644 --- a/docs/source/simulation_components/system/list_of_applications.rst +++ b/docs/source/simulation_components/system/list_of_applications.rst @@ -1,11 +1,11 @@ .. toctree:: :maxdepth: 1 + :glob: - applications/data_manipulation_bot.rst - applications/database_client.rst - applications/dos_bot.rst - applications/web_browser.rst + applications/* More info :py:mod:`primaite.game.game.APPLICATION_TYPES_MAPPING` .. include:: list_of_system_applications.rst + +.. |SOFTWARE_TYPE| replace:: application diff --git a/docs/source/simulation_components/system/list_of_services.rst b/docs/source/simulation_components/system/list_of_services.rst index 07bc25ee..e24b26dc 100644 --- a/docs/source/simulation_components/system/list_of_services.rst +++ b/docs/source/simulation_components/system/list_of_services.rst @@ -1,15 +1,11 @@ .. toctree:: :maxdepth: 1 + :glob: - services/database_service.rst - services/dns_client.rst - services/dns_server.rst - services/ftp_client.rst - services/ftp_server.rst - services/ntp_client.rst - services/ntp_server.rst - services/web_server.rst + services/* More info :py:mod:`primaite.game.game.SERVICE_TYPES_MAPPING` .. include:: list_of_system_services.rst + +.. |SOFTWARE_TYPE| replace:: service 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 ca5a7457..fae0f5d4 100644 --- a/docs/source/simulation_components/system/list_of_system_applications.rst +++ b/docs/source/simulation_components/system/list_of_system_applications.rst @@ -11,9 +11,6 @@ The application may not be configured as needed, in which case, see the relevant The list of applications that are considered system software are: -.. toctree:: - :maxdepth: 1 - - applications/web_browser.rst +- ``WebBrowser`` More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` 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 657faa52..4ff6f245 100644 --- a/docs/source/simulation_components/system/list_of_system_services.rst +++ b/docs/source/simulation_components/system/list_of_system_services.rst @@ -11,11 +11,8 @@ The service may not be configured as needed, in which case, see the relevant ser The list of services that are considered system software are: -.. toctree:: - :maxdepth: 1 - - services/dns_client.rst - services/ftp_client.rst - services/ntp_client.rst +- ``DNSClient`` +- ``FTPClient`` +- ``NTPClient`` More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index a4591d15..30d6b3ba 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -2,13 +2,15 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _DatabaseService: + DatabaseService -=============== +############### The ``DatabaseService`` provides a SQL database server simulation by extending the base Service class. Key capabilities -^^^^^^^^^^^^^^^^ +================ - Creates a database file in the ``Node`` 's ``FileSystem`` upon creation. - Handles connecting clients by maintaining a dictionary of connections mapped to session IDs. @@ -18,16 +20,90 @@ Key capabilities - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the database service. - Clients connect, execute queries, and disconnect. - Service runs on TCP port 5432 by default. Implementation -^^^^^^^^^^^^^^ +============== - Creates the database file within the node's file system. - Manages client connections in a dictionary by session ID. - Processes SQL queries. - Returns results and status codes in a standard dictionary format. - Extends Service class for integration with ``SoftwareManager``. + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.database.database_service import DatabaseService + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install DatabaseService on server + server.software_manager.install(DatabaseService) + db_service: DatabaseService = server.software_manager.software.get("DatabaseService") + db_service.start() + + # configure DatabaseService + db_service.configure_backup(IPv4Address("192.168.0.10")) + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: database_service + type: DatabaseService + options: + backup_server_ip: 192.168.0.10 + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: DatabaseService +.. |SOFTWARE_NAME_BACKTICK| replace:: ``DatabaseService`` + +``backup_server_ip`` +"""""""""""""""""""" + +Optional. Default value is ``None``. + +The IP Address of the backup server that the ``DatabaseService`` will use to create backups of the database. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. + +``password`` +"""""""""""" + +Optional. Default value is ``None``. + +The password that needs to be provided by connecting clients in order to create a successful connection. diff --git a/docs/source/simulation_components/system/services/dns_client.rst b/docs/source/simulation_components/system/services/dns_client.rst index f961ece3..91461590 100644 --- a/docs/source/simulation_components/system/services/dns_client.rst +++ b/docs/source/simulation_components/system/services/dns_client.rst @@ -2,20 +2,22 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK -DNSClient -========= +.. _DNSClient: -The DNSClient provides a client interface for connecting to the ``DNSServer``. +DNSClient +######### + +The DNSClient provides a client interface for connecting to the :ref:`DNSServer`. Key features -^^^^^^^^^^^^ +============ -- Connects to the ``DNSServer`` via the ``SoftwareManager``. +- Connects to the :ref:`DNSServer` via the ``SoftwareManager``. - Executes DNS lookup requests and keeps a cache of known domain name IP addresses. - Handles connection to DNSServer and querying for domain name IP addresses. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the database service. - Service runs on TCP port 53 by default. (TODO: TCP for now, should be UDP in future) @@ -23,8 +25,75 @@ Usage - ``DNSClient`` will automatically add the IP Address of the domain into its cache Implementation -^^^^^^^^^^^^^^ +============== - Leverages ``SoftwareManager`` for sending payloads over the network. - Provides easy interface for Nodes to find IP addresses via domain names. - Extends base Service class. + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.dns.dns_client import DNSClient + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install DNSClient on server + server.software_manager.install(DNSClient) + dns_client: DNSClient = server.software_manager.software.get("DNSClient") + dns_client.start() + + # configure DatabaseService + dns_client.dns_server = IPv4Address("192.168.0.10") + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: dns_client + type: DNSClient + options: + dns_server: 192.168.0.10 + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: DNSClient +.. |SOFTWARE_NAME_BACKTICK| replace:: ``DNSClient`` + +``dns_server`` +"""""""""""""" + +Optional. Default value is ``None``. + +The IP Address of the :ref:`DNSServer`. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. diff --git a/docs/source/simulation_components/system/services/dns_server.rst b/docs/source/simulation_components/system/services/dns_server.rst index ef463d9a..89ce7fc1 100644 --- a/docs/source/simulation_components/system/services/dns_server.rst +++ b/docs/source/simulation_components/system/services/dns_server.rst @@ -2,12 +2,15 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _DNSServer: + DNSServer -========= +######### + Also known as a DNS Resolver, the ``DNSServer`` provides a DNS Server simulation by extending the base Service class. Key capabilities -^^^^^^^^^^^^^^^^ +================ - Simulates DNS requests and DNSPacket transfer across a network - Registers domain names and the IP Address linked to the domain name @@ -15,12 +18,81 @@ Key capabilities - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the database service. - Service runs on TCP port 53 by default. (TODO: TCP for now, should be UDP in future) Implementation -^^^^^^^^^^^^^^ +============== - DNS request and responses use a ``DNSPacket`` object - Extends Service class for integration with ``SoftwareManager``. + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.dns.dns_server import DNSServer + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install DNSServer on server + server.software_manager.install(DNSServer) + dns_server: DNSServer = server.software_manager.software.get("DNSServer") + dns_server.start() + + # configure DatabaseService + dns_server.dns_register("arcd.com", IPv4Address("192.168.10.10")) + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: dns_server + type: DNSServer + options: + domain_mapping: + arcd.com: 192.168.0.10 + another-example.com: 192.168.10.10 + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: DNSServer +.. |SOFTWARE_NAME_BACKTICK| replace:: ``DNSServer`` + +domain_mapping +"""""""""""""" + +Domain mapping takes the domain and IP Addresses as a key-value pairs i.e. + +If the domain is "arcd.com" and the IP Address attributed to the domain is 192.168.0.10, then the value should be ``arcd.com: 192.168.0.10`` + +The key must be a string and the IP Address must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index 77111938..82b85770 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -2,16 +2,17 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _FTPClient: FTPClient -========= +######### -The ``FTPClient`` provides a client interface for connecting to the ``FTPServer``. +The ``FTPClient`` provides a client interface for connecting to the :ref:`FTPServer`. Key features -^^^^^^^^^^^^ +============ -- Connects to the ``FTPServer`` via the ``SoftwareManager``. +- Connects to the :ref:`FTPServer` via the ``SoftwareManager``. - Simulates FTP requests and FTPPacket transfer across a network - Allows the emulation of FTP commands between an FTP client and server: - PORT: specifies the port that server should connect to on the client (currently only uses ``Port.FTP``) @@ -21,7 +22,7 @@ Key features - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the FTP client service. - Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) @@ -29,81 +30,61 @@ Usage - Execute retrieving a file from the FTP server with ``request_file`` Implementation -^^^^^^^^^^^^^^ +============== - Leverages ``SoftwareManager`` for sending payloads over the network. - Provides easy interface for Nodes to transfer files between each other. - Extends base Service class. +Examples +======== -Example Usage -------------- - -Dependencies -^^^^^^^^^^^^ +Python +"""""" .. code-block:: python - from ipaddress import IPv4Address - - from primaite.simulator.network.container import Network - from primaite.simulator.network.hardware.nodes.computer import Computer - from primaite.simulator.network.hardware.nodes.server import Server - from primaite.simulator.system.services.ftp.ftp_server import FTPServer + from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.ftp.ftp_client import FTPClient - from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -Example peer to peer network -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - net = Network() - - pc1 = Computer( - hostname="pc1", - ip_address="120.10.10.10", + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", subnet_mask="255.255.255.0", - operating_state=NodeOperatingState.ON # initialise the computer in an ON state + default_gateway="192.168.1.1Ó", + start_up_duration=0, ) - srv = Server( - hostname="srv", - ip_address="120.10.10.20", - subnet_mask="255.255.255.0", - operating_state=NodeOperatingState.ON # initialise the server in an ON state - ) - net.connect(pc1.network_interface[1], srv.network_interface[1]) + server.power_on() -Install the FTP Server -^^^^^^^^^^^^^^^^^^^^^^ + # Install FTPClient on server + server.software_manager.install(FTPClient) + ftp_client: FTPClient = server.software_manager.software.get("FTPClient") + ftp_client.start() -FTP Client should be pre installed on nodes -.. code-block:: python +Via Configuration +""""""""""""""""" - srv.software_manager.install(FTPServer) - ftpserv: FTPServer = srv.software_manager.software['FTPServer'] +.. code-block:: yaml -Setting up the FTP Server -^^^^^^^^^^^^^^^^^^^^^^^^^ + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: ftp_client + type: FTPClient -Set up the FTP Server with a file that the client will need to retrieve +Configuration +============= -.. code-block:: python +.. include:: ../common/common_configuration.rst - srv.file_system.create_file('my_file.png') +.. |SOFTWARE_NAME| replace:: FTPClient +.. |SOFTWARE_NAME_BACKTICK| replace:: ``FTPClient`` -Check that file was retrieved -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - client.request_file( - src_folder_name='root', - src_file_name='my_file.png', - dest_folder_name='root', - dest_file_name='test.png', - dest_ip_address=IPv4Address("120.10.10.20") - ) - - print(client.get_file(folder_name="root", file_name="test.png")) +**FTPClient has no configuration options** diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index 81f51e6b..d807a14f 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -2,12 +2,15 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _FTPServer: + FTPServer -========= +######### + Provides a FTP Client-Server simulation by extending the base Service class. Key capabilities -^^^^^^^^^^^^^^^^ +================ - Simulates FTP requests and FTPPacket transfer across a network - Allows the emulation of FTP commands between an FTP client and server: @@ -16,12 +19,75 @@ Key capabilities - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== + - Install on a Node via the ``SoftwareManager`` to start the FTP server service. - Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) Implementation -^^^^^^^^^^^^^^ +============== - FTP request and responses use a ``FTPPacket`` object - Extends Service class for integration with ``SoftwareManager``. + + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.ftp.ftp_server import FTPServer + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install FTPServer on server + server.software_manager.install(FTPServer) + ftp_server: FTPServer = server.software_manager.software.get("FTPServer") + ftp_server.start() + + ftp_server.server_password = "test" + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: ftp_server + type: FTPServer + options: + server_password: test + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: FTPServer +.. |SOFTWARE_NAME_BACKTICK| replace:: ``FTPServer`` + +``server_password`` +""""""""""""""""""" + +Optional. Default value is ``None``. + +The password that needs to be provided by a connecting :ref:`FTPClient` in order to create a successful connection. diff --git a/docs/source/simulation_components/system/services/ntp_client.rst b/docs/source/simulation_components/system/services/ntp_client.rst index 27cd27e4..aaba3261 100644 --- a/docs/source/simulation_components/system/services/ntp_client.rst +++ b/docs/source/simulation_components/system/services/ntp_client.rst @@ -2,25 +2,94 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _NTPClient: + NTPClient -========= +######### The NTPClient provides a client interface for connecting to the ``NTPServer``. Key features -^^^^^^^^^^^^ +============ - Connects to the ``NTPServer`` via the ``SoftwareManager``. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the database service. - Service runs on UDP port 123 by default. Implementation -^^^^^^^^^^^^^^ +============== - Leverages ``SoftwareManager`` for sending payloads over the network. - Provides easy interface for Nodes to find IP addresses via domain names. - Extends base Service class. + + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from ipaddress import IPv4Address + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.ntp.ntp_client import NTPClient + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install NTPClient on server + server.software_manager.install(NTPClient) + ntp_client: NTPClient = server.software_manager.software.get("NTPClient") + ntp_client.start() + + ntp_client.configure(ntp_server_ip_address=IPv4Address("192.168.0.10")) + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: ntp_client + type: NTPClient + options: + ntp_server_ip: 192.168.0.10 + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: NTPClient +.. |SOFTWARE_NAME_BACKTICK| replace:: ``NTPClient`` + +``ntp_server_ip`` +""""""""""""""""" + +Optional. Default value is ``None``. + +The IP address of an NTP Server which provides a time that the ``NTPClient`` can synchronise to. + +This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. diff --git a/docs/source/simulation_components/system/services/ntp_server.rst b/docs/source/simulation_components/system/services/ntp_server.rst index 066ad5ac..0025b428 100644 --- a/docs/source/simulation_components/system/services/ntp_server.rst +++ b/docs/source/simulation_components/system/services/ntp_server.rst @@ -2,27 +2,85 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _NTPServer: + NTPServer -========= +######### + The ``NTPServer`` provides a NTP Server simulation by extending the base Service class. NTP Client ----------- +========== + The ``NTPClient`` provides a NTP Client simulation by extending the base Service class. Key capabilities -^^^^^^^^^^^^^^^^ +================ - Simulates NTP requests and NTPPacket transfer across a network - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== - Install on a Node via the ``SoftwareManager`` to start the database service. - Service runs on UDP port 123 by default. Implementation -^^^^^^^^^^^^^^ +============== - NTP request and responses use a ``NTPPacket`` object - Extends Service class for integration with ``SoftwareManager``. + + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.ntp.ntp_server import NTPServer + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install NTPServer on server + server.software_manager.install(NTPServer) + ntp_server: NTPServer = server.software_manager.software.get("NTPServer") + ntp_server.start() + + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: ntp_server + type: NTPServer + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: NTPServer +.. |SOFTWARE_NAME_BACKTICK| replace:: ``NTPServer`` + +**NTPServer has no configuration options** diff --git a/docs/source/simulation_components/system/services/web_server.rst b/docs/source/simulation_components/system/services/web_server.rst index ae3f32e6..62b1d090 100644 --- a/docs/source/simulation_components/system/services/web_server.rst +++ b/docs/source/simulation_components/system/services/web_server.rst @@ -2,12 +2,15 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _WebServer: + WebServer -========= +######### + Provides a Web Server simulation by extending the base Service class. Key capabilities -^^^^^^^^^^^^^^^^ +================ - Simulates a web server with the capability to also request data from a database - Allows the emulation of HTTP requests between client (e.g. a web browser) and server @@ -15,13 +18,69 @@ Key capabilities - Leverages the Service base class for install/uninstall, status tracking, etc. Usage -^^^^^ +===== + - Install on a Node via the ``SoftwareManager`` to start the `WebServer`. - Service runs on HTTP port 80 by default. (TODO: HTTPS) +- A :ref:`DatabaseClient` must be installed and configured on the same node as the ``WebServer`` if it is intended to send a users request i.e. + in the case that the :ref:`WebBrowser` sends a request with users in its request path, the ``WebServer`` will utilise the ``DatabaseClient`` to send a request to the ``DatabaseService`` Implementation -^^^^^^^^^^^^^^ +============== - HTTP request uses a ``HttpRequestPacket`` object - HTTP response uses a ``HttpResponsePacket`` object - Extends Service class for integration with ``SoftwareManager``. + + +Examples +======== + +Python +"""""" + +.. code-block:: python + + from primaite.simulator.network.hardware.nodes.host.server import Server + from primaite.simulator.system.services.web_server.web_server import WebServer + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Install WebServer on server + server.software_manager.install(WebServer) + web_server: WebServer = server.software_manager.software.get("WebServer") + web_server.start() + +Via Configuration +""""""""""""""""" + +.. code-block:: yaml + + simulation: + network: + nodes: + - ref: example_server + hostname: example_server + type: server + ... + services: + - ref: web_server + type: WebServer + +Configuration +============= + +.. include:: ../common/common_configuration.rst + +.. |SOFTWARE_NAME| replace:: WebServer +.. |SOFTWARE_NAME_BACKTICK| replace:: ``WebServer`` + +**WebServer has no configuration options** diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8e78f636..ef54893e 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -296,6 +296,7 @@ class PrimaiteGame: if service_type == "DatabaseService": if "options" in service_cfg: opt = service_cfg["options"] + new_service.password = opt.get("backup_server_ip", None) new_service.configure_backup(backup_server=IPv4Address(opt.get("backup_server_ip"))) if service_type == "FTPServer": if "options" in service_cfg: @@ -327,7 +328,7 @@ class PrimaiteGame: new_application.configure( server_ip_address=IPv4Address(opt.get("server_ip")), server_password=opt.get("server_password"), - payload=opt.get("payload"), + 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")), ) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 0b9554d5..c0390b4f 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -23,6 +23,7 @@ class DatabaseService(Service): """ password: Optional[str] = None + """Password that needs to be provided by clients if they want to connect to the DatabaseService.""" backup_server_ip: IPv4Address = None """IP address of the backup server.""" @@ -194,6 +195,8 @@ class DatabaseService(Service): """ Executes the given SQL query and returns the result. + .. _Database Payload List: + Possible queries: - SELECT : returns the data - DELETE : deletes the data From c115095157f27d6d7480df430c2c83e50078184d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 25 Feb 2024 16:17:12 +0000 Subject: [PATCH 023/128] Fix router from config using wrong method --- src/primaite/simulator/network/hardware/nodes/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 887bc9be..6bf80b3c 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -1016,7 +1016,7 @@ class Router(Node): # Add the router's default ACL rules from the config. if "acl" in cfg: for r_num, r_cfg in cfg["acl"].items(): - new.add_rule( + new.acl.add_rule( action=ACLAction[r_cfg["action"]], src_port=None if not (p := r_cfg.get("src_port")) else Port[p], dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], From 994dbc3501b7c50584322cf3bba9db7aa7e0b77d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 25 Feb 2024 17:44:41 +0000 Subject: [PATCH 024/128] Finalise the refactor. It works well now. --- .../config/_package_data/example_config.yaml | 5 +- src/primaite/game/game.py | 12 +++- src/primaite/notebooks/uc2_demo.ipynb | 66 +++++++++---------- src/primaite/session/environment.py | 7 +- src/primaite/simulator/network/container.py | 6 +- .../simulator/network/hardware/base.py | 10 +-- .../network/hardware/nodes/network/router.py | 2 +- src/primaite/simulator/sim_container.py | 2 +- .../system/services/web_server/web_server.py | 2 +- src/primaite/simulator/system/software.py | 7 +- 10 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index f85baf10..a32696c7 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -652,12 +652,13 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - ref: database_server diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 02d36c8a..f5649589 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -185,6 +185,10 @@ class PrimaiteGame: """Close the game, this will close the simulation.""" return NotImplemented + def setup_for_episode(self, episode: int) -> None: + """Perform any final configuration of components to make them ready for the game to start.""" + self.simulation.setup_for_episode(episode=episode) + @classmethod def from_config(cls, cfg: Dict) -> "PrimaiteGame": """Create a PrimaiteGame object from a config dictionary. @@ -258,7 +262,9 @@ class PrimaiteGame: new_service = new_node.software_manager.software[service_type] game.ref_map_services[service_ref] = new_service.uuid else: - _LOGGER.warning(f"service type not found {service_type}") + msg = f"Configuration contains an invalid service type: {service_type}" + _LOGGER.error(msg) + raise ValueError(msg) # service-dependent options if service_type == "DNSClient": if "options" in service_cfg: @@ -297,7 +303,9 @@ class PrimaiteGame: new_application = new_node.software_manager.software[application_type] game.ref_map_applications[application_ref] = new_application.uuid else: - _LOGGER.warning(f"application type not found {application_type}") + msg = f"Configuration contains an invalid application type: {application_type}" + _LOGGER.error(msg) + raise ValueError(msg) if application_type == "DataManipulationBot": if "options" in application_cfg: diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index c4fe4c9a..460e3d27 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -335,9 +335,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "%load_ext autoreload\n", @@ -347,9 +345,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "# Imports\n", @@ -372,9 +368,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "# create the env\n", @@ -385,10 +379,10 @@ " cfg['simulation']['network']['nodes'][9]['applications'][0]['options']['data_manipulation_p_of_success'] = 1.0\n", " cfg['simulation']['network']['nodes'][8]['applications'][0]['options']['port_scan_p_of_success'] = 1.0\n", " cfg['simulation']['network']['nodes'][9]['applications'][0]['options']['port_scan_p_of_success'] = 1.0\n", - "game = PrimaiteGame.from_config(cfg)\n", - "env = PrimaiteGymEnv(game = game)\n", - "# Don't flatten obs as we are not training an agent and we wish to see the dict-formatted observations\n", - "env.agent.flatten_obs = False\n", + " # don't flatten observations so that we can see what is going on\n", + " cfg['agents'][3]['agent_settings']['flatten_obs'] = False\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", "obs, info = env.reset()\n", "print('env created successfully')\n", "pprint(obs)" @@ -422,9 +416,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "for step in range(35):\n", @@ -442,9 +434,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "pprint(obs['NODES'])" @@ -460,9 +450,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", @@ -488,9 +476,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", @@ -515,9 +501,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", @@ -540,9 +524,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "metadata": {}, "outputs": [], "source": [ "env.step(13) # Patch the database\n", @@ -582,6 +564,22 @@ "obs['ACL']" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Reset the cell, you can rerun the other cells to verify that the attack works the same every episode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.reset()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -592,7 +590,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "venv", "language": "python", "name": "python3" }, @@ -606,9 +604,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 2 } diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index ad770f8f..bab81253 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -74,6 +74,7 @@ class PrimaiteGymEnv(gymnasium.Env): f"avg. reward: {self.game.rl_agents[0].reward_function.total_reward}" ) self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.game_config) + self.game.setup_for_episode(episode=self.episode_counter) self.agent = self.game.rl_agents[0] self.episode_counter += 1 state = self.game.get_sim_state() @@ -97,12 +98,12 @@ class PrimaiteGymEnv(gymnasium.Env): def _get_obs(self) -> ObsType: """Return the current observation.""" - if not self.agent.flatten_obs: - return self.agent.observation_manager.current_observation - else: + if self.agent.flatten_obs: unflat_space = self.agent.observation_manager.space unflat_obs = self.agent.observation_manager.current_observation return gymnasium.spaces.flatten(unflat_space, unflat_obs) + else: + return self.agent.observation_manager.current_observation class PrimaiteRayEnv(gymnasium.Env): diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index c3ad84c3..b5a16430 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -48,9 +48,9 @@ class Network(SimComponent): def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" for node in self.nodes.values(): - node.setup_for_episode(episode) + node.setup_for_episode(episode=episode) for link in self.links.values(): - link.setup_for_episode(episode) + link.setup_for_episode(episode=episode) for node in self.nodes.values(): node.power_on() @@ -172,7 +172,7 @@ class Network(SimComponent): def clear_links(self): """Clear all the links in the network by resetting their component state for the episode.""" for link in self.links.values(): - link.setup_for_episode() + link.setup_for_episode(episode=0) # TODO: shouldn't be using this method here. def draw(self, seed: int = 123): """ diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 771c3397..1b6d611e 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -90,7 +90,7 @@ class NetworkInterface(SimComponent, ABC): def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - super().setup_for_episode(episode) + super().setup_for_episode(episode=episode) if episode and self.pcap: self.pcap.current_episode = episode self.pcap.setup_logger() @@ -643,17 +643,17 @@ class Node(SimComponent): def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - super().setup_for_episode(episode) + super().setup_for_episode(episode=episode) # Reset File System - self.file_system.setup_for_episode(episode) + self.file_system.setup_for_episode(episode=episode) # Reset all Nics for network_interface in self.network_interfaces.values(): - network_interface.setup_for_episode(episode) + network_interface.setup_for_episode(episode=episode) for software in self.software_manager.software.values(): - software.setup_for_episode(episode) + software.setup_for_episode(episode=episode) if episode and self.sys_log: self.sys_log.current_episode = episode diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index c299dfb7..3111a153 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1078,7 +1078,7 @@ class Router(NetworkNode): for i, _ in self.network_interface.items(): self.enable_port(i) - super().setup_for_episode(episode) + super().setup_for_episode(episode=episode) def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index bb6132a8..a2285d92 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -23,7 +23,7 @@ class Simulation(SimComponent): def setup_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - self.network.setup_for_episode(episode) + self.network.setup_for_episode(episode=episode) def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() 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 5e4a6f6e..ce29a2f9 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -118,7 +118,7 @@ class WebServer(Service): self.set_health_state(SoftwareHealthState.COMPROMISED) return response - except Exception: + except Exception: # TODO: refactor this. Likely to cause silent bugs. # something went wrong on the server response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR return response diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 56a1e3d1..8864659c 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -3,7 +3,7 @@ from abc import abstractmethod from datetime import datetime from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, TYPE_CHECKING, Union from primaite.simulator.core import _LOGGER, RequestManager, RequestType, SimComponent from primaite.simulator.file_system.file_system import FileSystem, Folder @@ -13,6 +13,9 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.sys_log import SysLog +if TYPE_CHECKING: + from primaite.simulator.system.core.software_manager import SoftwareManager + class SoftwareType(Enum): """ @@ -84,7 +87,7 @@ class Software(SimComponent): "The count of times the software has been scanned, defaults to 0." revealed_to_red: bool = False "Indicates if the software has been revealed to red agent, defaults is False." - software_manager: Any = None + software_manager: "SoftwareManager" = None "An instance of Software Manager that is used by the parent node." sys_log: SysLog = None "An instance of SysLog that is used by the parent node." From 63c9a36c30adf38716759526968067ae990f1fdc Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 25 Feb 2024 18:36:20 +0000 Subject: [PATCH 025/128] Fix typos --- src/primaite/notebooks/uc2_demo.ipynb | 2 +- src/primaite/simulator/system/services/ntp/ntp_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index 460e3d27..7c90a885 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -568,7 +568,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Reset the cell, you can rerun the other cells to verify that the attack works the same every episode." + "Reset the environment, you can rerun the other cells to verify that the attack works the same every episode." ] }, { diff --git a/src/primaite/simulator/system/services/ntp/ntp_client.py b/src/primaite/simulator/system/services/ntp/ntp_client.py index 1e9dc139..ad00065c 100644 --- a/src/primaite/simulator/system/services/ntp/ntp_client.py +++ b/src/primaite/simulator/system/services/ntp/ntp_client.py @@ -1,6 +1,6 @@ from datetime import datetime from ipaddress import IPv4Address -from typing import Dict, List, Optional +from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.ntp import NTPPacket @@ -54,7 +54,7 @@ class NTPClient(Service): payload: NTPPacket, session_id: Optional[str] = None, dest_ip_address: IPv4Address = None, - dest_port: List[Port] = Port.NTP, + dest_port: Port = Port.NTP, **kwargs, ) -> bool: """Requests NTP data from NTP server. From 07373d941eab4ea4d03caad49e8db327d2e1b84e Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 26 Feb 2024 08:44:08 +0000 Subject: [PATCH 026/128] #2257: Downgrade version of sphinx - 7.2.0 drops support for Python 3.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 44ce75c6..19b5b7fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ dev = [ "pytest-cov==4.0.0", "pytest-flake8==1.1.1", "setuptools==66", - "Sphinx==7.2.6", + "Sphinx==7.1.2", "sphinx-copybutton==0.5.2", "wheel==0.38.4" ] From 1d5c153752269428c67bda75652bcbd8cef5f3fc Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 26 Feb 2024 08:49:11 +0000 Subject: [PATCH 027/128] #2257: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e45d2e..e96bf4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,12 @@ SessionManager. - `AirSpace` class to simulate wireless communications, managing wireless interfaces and facilitating the transmission of frames within specified frequencies. - `AirSpaceFrequency` enum for defining standard wireless frequencies, including 2.4 GHz and 5 GHz bands, to support realistic wireless network simulations. - `WirelessRouter` class, extending the `Router` class, to incorporate wireless networking capabilities alongside traditional wired connections. This class allows the configuration of wireless access points with specific IP settings and operating frequencies. +- Configuration examples in documentation: + - Examples include how to set up PrimAITE session + - Examples include how to create nodes and install software +- Ability to add Firewall node via config +- Ability to add Router routes via config +- Ability to add Router/Firewall ACL Rules via config ### Changed From e964b8a3eaebf3f3415dc089ccca9c6db4412183 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 26 Feb 2024 08:58:03 +0000 Subject: [PATCH 028/128] #2257: more in depth changelog --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e96bf4a6..e51a912e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,12 +82,19 @@ SessionManager. - `AirSpace` class to simulate wireless communications, managing wireless interfaces and facilitating the transmission of frames within specified frequencies. - `AirSpaceFrequency` enum for defining standard wireless frequencies, including 2.4 GHz and 5 GHz bands, to support realistic wireless network simulations. - `WirelessRouter` class, extending the `Router` class, to incorporate wireless networking capabilities alongside traditional wired connections. This class allows the configuration of wireless access points with specific IP settings and operating frequencies. -- Configuration examples in documentation: - - Examples include how to set up PrimAITE session - - Examples include how to create nodes and install software -- Ability to add Firewall node via config -- Ability to add Router routes via config -- Ability to add Router/Firewall ACL Rules via config +- Documentation Updates: + - Examples include how to set up PrimAITE session via config + - Examples include how to create nodes and install software via config + - Examples include how to set up PrimAITE session via Python + - Examples include how to create nodes and install software via Python + - Added missing ``DoSBot`` documentation page + - Added diagrams where needed to make understanding some things easier + - Templated parts of the documentation to prevent unnecessary repetition and for easier maintaining of documentation + - Separated documentation pages of some items i.e. client and server software were on the same pages - which may make things confusing + - Configuration section at the bottom of the software pages specifying the configuration options available (and which ones are optional) +- Ability to add ``Firewall`` node via config +- Ability to add ``Router`` routes via config +- Ability to add ``Router``/``Firewall`` ``ACLRule`` via config ### Changed From 634f6340973eca2b2927f03e897593714034ded0 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 26 Feb 2024 09:47:12 +0000 Subject: [PATCH 029/128] #2257: fix text and make examples in node configs more specific --- .../common/common_network_node_attributes.rst | 2 +- .../simulation/nodes/computer.rst | 26 ++++---- .../simulation/nodes/firewall.rst | 64 ++++++++++--------- .../configuration/simulation/nodes/router.rst | 20 +++--- .../configuration/simulation/nodes/server.rst | 26 ++++---- .../configuration/simulation/nodes/switch.rst | 12 ++-- 6 files changed, 80 insertions(+), 70 deletions(-) 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 83007145..d0b3e65b 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 @@ -23,7 +23,7 @@ e.g. ``address`` """"""""""" -The target IP address for the route. If the packet destination IP address matches this, the router will route the packet according to the ``next_hop_ip_address``. +The target IP address for the route. If the packet destination IP address matches this, the |NODE| will route the packet according to the ``next_hop_ip_address``. This must be a valid octet i.e. in the range of ``0.0.0.0`` and ``255.255.255.255``. diff --git a/docs/source/configuration/simulation/nodes/computer.rst b/docs/source/configuration/simulation/nodes/computer.rst index bbdf087d..04a45766 100644 --- a/docs/source/configuration/simulation/nodes/computer.rst +++ b/docs/source/configuration/simulation/nodes/computer.rst @@ -16,18 +16,20 @@ example computer .. code-block:: yaml - nodes: - - ref: client_1 - hostname: client_1 - type: computer - ip_address: 192.168.0.10 - subnet_mask: 255.255.255.0 - default_gateway: 192.168.0.1 - dns_server: 192.168.1.10 - applications: - ... - services: - ... + simulation: + network: + nodes: + - ref: client_1 + hostname: client_1 + type: computer + ip_address: 192.168.0.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.0.1 + dns_server: 192.168.1.10 + applications: + ... + services: + ... .. include:: common/common_node_attributes.rst diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst index c8a21a02..3c1fce0a 100644 --- a/docs/source/configuration/simulation/nodes/firewall.rst +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -18,37 +18,39 @@ example firewall .. code-block:: yaml - nodes: - - ref: firewall - hostname: firewall - type: firewall - start_up_duration: 0 - shut_down_duration: 0 - ports: - external_port: # port 1 - ip_address: 192.168.20.1 - subnet_mask: 255.255.255.0 - internal_port: # port 2 - ip_address: 192.168.1.2 - subnet_mask: 255.255.255.0 - dmz_port: # port 3 - ip_address: 192.168.10.1 - subnet_mask: 255.255.255.0 - acl: - internal_inbound_acl: - ... - internal_outbound_acl: - ... - dmz_inbound_acl: - ... - dmz_outbound_acl: - ... - external_inbound_acl: - ... - external_outbound_acl: - ... - routes: - ... + simulation: + network: + nodes: + - ref: firewall + hostname: firewall + type: firewall + start_up_duration: 0 + shut_down_duration: 0 + ports: + external_port: # port 1 + ip_address: 192.168.20.1 + subnet_mask: 255.255.255.0 + internal_port: # port 2 + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + dmz_port: # port 3 + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + acl: + internal_inbound_acl: + ... + internal_outbound_acl: + ... + dmz_inbound_acl: + ... + dmz_outbound_acl: + ... + external_inbound_acl: + ... + external_outbound_acl: + ... + routes: + ... .. include:: common/common_node_attributes.rst diff --git a/docs/source/configuration/simulation/nodes/router.rst b/docs/source/configuration/simulation/nodes/router.rst index 8a8efc06..b9ba1ad5 100644 --- a/docs/source/configuration/simulation/nodes/router.rst +++ b/docs/source/configuration/simulation/nodes/router.rst @@ -16,15 +16,17 @@ example router .. code-block:: yaml - nodes: - - ref: router_1 - hostname: router_1 - type: router - num_ports: 5 - ports: - ... - acl: - ... + simulation: + network: + nodes: + - ref: router_1 + hostname: router_1 + type: router + num_ports: 5 + ports: + ... + acl: + ... .. include:: common/common_node_attributes.rst diff --git a/docs/source/configuration/simulation/nodes/server.rst b/docs/source/configuration/simulation/nodes/server.rst index 7f51eaf2..dbc32235 100644 --- a/docs/source/configuration/simulation/nodes/server.rst +++ b/docs/source/configuration/simulation/nodes/server.rst @@ -16,18 +16,20 @@ example server .. code-block:: yaml - nodes: - - ref: server_1 - hostname: server_1 - type: server - ip_address: 192.168.10.10 - subnet_mask: 255.255.255.0 - default_gateway: 192.168.10.1 - dns_server: 192.168.1.10 - applications: - ... - services: - ... + simulation: + network: + nodes: + - ref: server_1 + hostname: server_1 + type: server + ip_address: 192.168.10.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + ... + services: + ... .. include:: common/common_node_attributes.rst diff --git a/docs/source/configuration/simulation/nodes/switch.rst b/docs/source/configuration/simulation/nodes/switch.rst index 4d57f76e..263bedbb 100644 --- a/docs/source/configuration/simulation/nodes/switch.rst +++ b/docs/source/configuration/simulation/nodes/switch.rst @@ -16,11 +16,13 @@ example switch .. code-block:: yaml - nodes: - - ref: switch_1 - hostname: switch_1 - type: switch - num_ports: 8 + simulation: + network: + nodes: + - ref: switch_1 + hostname: switch_1 + type: switch + num_ports: 8 .. include:: common/common_node_attributes.rst From e5982c4599b07ef5cf994218f4323d1105f65bc7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 10:26:28 +0000 Subject: [PATCH 030/128] Change agents list in game object to dictionary --- .../example_config_2_rl_agents.yaml | 446 +++++++++++------- src/primaite/game/game.py | 18 +- .../training_example_ray_multi_agent.ipynb | 9 +- .../training_example_ray_single_agent.ipynb | 2 +- .../notebooks/training_example_sb3.ipynb | 11 +- src/primaite/session/environment.py | 52 +- tests/conftest.py | 2 +- tests/integration_tests/game_configuration.py | 16 +- 8 files changed, 331 insertions(+), 225 deletions(-) diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 93019c9d..1ccd7b38 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -10,6 +10,8 @@ io_settings: save_checkpoints: true checkpoint_interval: 5 save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true game: @@ -36,9 +38,9 @@ agents: - type: NODE_APPLICATION_EXECUTE options: nodes: - - node_ref: client_2 + - node_name: client_2 applications: - - application_ref: client_2_web_browser + - application_name: WebBrowser max_folders_per_node: 1 max_files_per_folder: 1 max_services_per_node: 1 @@ -54,6 +56,31 @@ agents: frequency: 4 variance: 3 + - ref: client_1_green_user + team: GREEN + type: GreenWebBrowsingAgent + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + 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: + - type: DUMMY + + + + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent @@ -72,7 +99,7 @@ agents: - type: NODE_OS_SCAN options: nodes: - - node_ref: client_1 + - node_name: client_1 applications: - application_name: DataManipulationBot - node_name: client_2 @@ -104,25 +131,21 @@ agents: num_files_per_folder: 1 num_nics_per_node: 2 nodes: - - node_ref: domain_controller + - node_hostname: domain_controller services: - - service_ref: domain_controller_dns_server - - node_ref: web_server + - service_name: DNSServer + - node_hostname: web_server services: - - service_ref: web_server_database_client - - node_ref: database_server - services: - - service_ref: database_service + - service_name: WebServer + - node_hostname: database_server folders: - folder_name: database files: - file_name: database.db - - node_ref: backup_server - # services: - # - service_ref: backup_service - - node_ref: security_suite - - node_ref: client_1 - - node_ref: client_2 + - node_hostname: backup_server + - node_hostname: security_suite + - node_hostname: client_1 + - node_hostname: client_2 links: - link_ref: router_1___switch_1 - link_ref: router_1___switch_2 @@ -137,23 +160,23 @@ agents: acl: options: max_acl_rules: 10 - router_node_ref: router_1 + router_hostname: router_1 ip_address_order: - - node_ref: domain_controller + - node_hostname: domain_controller nic_num: 1 - - node_ref: web_server + - node_hostname: web_server nic_num: 1 - - node_ref: database_server + - node_hostname: database_server nic_num: 1 - - node_ref: backup_server + - node_hostname: backup_server nic_num: 1 - - node_ref: security_suite + - node_hostname: security_suite nic_num: 1 - - node_ref: client_1 + - node_hostname: client_1 nic_num: 1 - - node_ref: client_2 + - node_hostname: client_2 nic_num: 1 - - node_ref: security_suite + - node_hostname: security_suite nic_num: 2 ics: null @@ -184,10 +207,10 @@ agents: - type: NODE_RESET - type: NETWORK_ACL_ADDRULE options: - target_router_ref: router_1 + target_router_hostname: router_1 - type: NETWORK_ACL_REMOVERULE options: - target_router_ref: router_1 + target_router_hostname: router_1 - type: NETWORK_NIC_ENABLE - type: NETWORK_NIC_DISABLE @@ -242,25 +265,25 @@ agents: action: "NODE_FILE_SCAN" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 10: action: "NODE_FILE_CHECKHASH" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 11: action: "NODE_FILE_DELETE" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 12: action: "NODE_FILE_REPAIR" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 13: action: "NODE_SERVICE_PATCH" @@ -271,22 +294,22 @@ agents: action: "NODE_FOLDER_SCAN" options: node_id: 2 - folder_id: 1 + folder_id: 0 15: action: "NODE_FOLDER_CHECKHASH" options: node_id: 2 - folder_id: 1 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: node_id: 2 - folder_id: 1 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: node_id: 2 - folder_id: 1 + folder_id: 0 18: action: "NODE_OS_SCAN" options: @@ -303,63 +326,63 @@ agents: action: "NODE_RESET" options: node_id: 5 - 22: + 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" action: "NETWORK_ACL_ADDRULE" options: position: 1 permission: 2 - source_ip_id: 7 - dest_ip_id: 1 + source_ip_id: 7 # client 1 + dest_ip_id: 1 # ALL source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 23: + 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 2 permission: 2 - source_ip_id: 8 - dest_ip_id: 1 + source_ip_id: 8 # client 2 + dest_ip_id: 1 # ALL source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 24: + 24: # block tcp traffic from client 1 to web app action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 3 permission: 2 - source_ip_id: 7 - dest_ip_id: 3 + source_ip_id: 7 # client 1 + dest_ip_id: 3 # web server source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 25: + 25: # block tcp traffic from client 2 to web app action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 4 permission: 2 - source_ip_id: 8 - dest_ip_id: 3 + source_ip_id: 8 # client 2 + dest_ip_id: 3 # web server source_port_id: 1 dest_port_id: 1 protocol_id: 3 26: action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 5 permission: 2 - source_ip_id: 7 - dest_ip_id: 4 + source_ip_id: 7 # client 1 + dest_ip_id: 4 # database source_port_id: 1 dest_port_id: 1 protocol_id: 3 27: action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 6 permission: 2 - source_ip_id: 8 - dest_ip_id: 4 + source_ip_id: 8 # client 2 + dest_ip_id: 4 # database source_port_id: 1 dest_port_id: 1 protocol_id: 3 @@ -407,123 +430,148 @@ agents: action: "NETWORK_NIC_DISABLE" options: node_id: 0 - nic_id: 1 + nic_id: 0 39: action: "NETWORK_NIC_ENABLE" options: node_id: 0 - nic_id: 1 + nic_id: 0 40: action: "NETWORK_NIC_DISABLE" options: node_id: 1 - nic_id: 1 + nic_id: 0 41: action: "NETWORK_NIC_ENABLE" options: node_id: 1 - nic_id: 1 + nic_id: 0 42: action: "NETWORK_NIC_DISABLE" options: node_id: 2 - nic_id: 1 + nic_id: 0 43: action: "NETWORK_NIC_ENABLE" options: node_id: 2 - nic_id: 1 + nic_id: 0 44: action: "NETWORK_NIC_DISABLE" options: node_id: 3 - nic_id: 1 + nic_id: 0 45: action: "NETWORK_NIC_ENABLE" options: node_id: 3 - nic_id: 1 + nic_id: 0 46: action: "NETWORK_NIC_DISABLE" options: node_id: 4 - nic_id: 1 + nic_id: 0 47: action: "NETWORK_NIC_ENABLE" options: node_id: 4 - nic_id: 1 + nic_id: 0 48: action: "NETWORK_NIC_DISABLE" options: node_id: 4 - nic_id: 2 + nic_id: 1 49: action: "NETWORK_NIC_ENABLE" options: node_id: 4 - nic_id: 2 + nic_id: 1 50: action: "NETWORK_NIC_DISABLE" options: node_id: 5 - nic_id: 1 + nic_id: 0 51: action: "NETWORK_NIC_ENABLE" options: node_id: 5 - nic_id: 1 + nic_id: 0 52: action: "NETWORK_NIC_DISABLE" options: node_id: 6 - nic_id: 1 + nic_id: 0 53: action: "NETWORK_NIC_ENABLE" options: node_id: 6 - nic_id: 1 + nic_id: 0 options: nodes: - - node_ref: domain_controller - - node_ref: web_server + - node_name: domain_controller + - node_name: web_server + applications: + - application_name: DatabaseClient services: - - service_ref: web_server_web_service - - node_ref: database_server + - service_name: WebServer + - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db services: - - service_ref: database_service - - node_ref: backup_server - - node_ref: security_suite - - node_ref: client_1 - - node_ref: client_2 + - 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_address_order: + - node_name: domain_controller + nic_num: 1 + - node_name: web_server + nic_num: 1 + - node_name: database_server + nic_num: 1 + - node_name: backup_server + nic_num: 1 + - node_name: security_suite + nic_num: 1 + - node_name: client_1 + nic_num: 1 + - node_name: client_2 + nic_num: 1 + - node_name: security_suite + nic_num: 2 + reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.5 + weight: 0.34 options: - node_ref: database_server + node_hostname: database_server folder_name: database file_name: database.db - - - - type: WEB_SERVER_404_PENALTY - weight: 0.5 + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.33 options: - node_ref: web_server - service_ref: web_server_web_service + node_hostname: client_1 + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.33 + options: + node_hostname: client_2 agent_settings: - # ... - + flatten_obs: true - ref: defender_2 team: BLUE @@ -537,25 +585,21 @@ agents: num_files_per_folder: 1 num_nics_per_node: 2 nodes: - - node_ref: domain_controller + - node_hostname: domain_controller services: - - service_ref: domain_controller_dns_server - - node_ref: web_server + - service_name: DNSServer + - node_hostname: web_server services: - - service_ref: web_server_database_client - - node_ref: database_server - services: - - service_ref: database_service + - service_name: WebServer + - node_hostname: database_server folders: - folder_name: database files: - file_name: database.db - - node_ref: backup_server - # services: - # - service_ref: backup_service - - node_ref: security_suite - - node_ref: client_1 - - node_ref: client_2 + - node_hostname: backup_server + - node_hostname: security_suite + - node_hostname: client_1 + - node_hostname: client_2 links: - link_ref: router_1___switch_1 - link_ref: router_1___switch_2 @@ -570,23 +614,23 @@ agents: acl: options: max_acl_rules: 10 - router_node_ref: router_1 + router_hostname: router_1 ip_address_order: - - node_ref: domain_controller + - node_hostname: domain_controller nic_num: 1 - - node_ref: web_server + - node_hostname: web_server nic_num: 1 - - node_ref: database_server + - node_hostname: database_server nic_num: 1 - - node_ref: backup_server + - node_hostname: backup_server nic_num: 1 - - node_ref: security_suite + - node_hostname: security_suite nic_num: 1 - - node_ref: client_1 + - node_hostname: client_1 nic_num: 1 - - node_ref: client_2 + - node_hostname: client_2 nic_num: 1 - - node_ref: security_suite + - node_hostname: security_suite nic_num: 2 ics: null @@ -617,10 +661,10 @@ agents: - type: NODE_RESET - type: NETWORK_ACL_ADDRULE options: - target_router_ref: router_1 + target_router_hostname: router_1 - type: NETWORK_ACL_REMOVERULE options: - target_router_ref: router_1 + target_router_hostname: router_1 - type: NETWORK_NIC_ENABLE - type: NETWORK_NIC_DISABLE @@ -675,25 +719,25 @@ agents: action: "NODE_FILE_SCAN" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 10: action: "NODE_FILE_CHECKHASH" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 11: action: "NODE_FILE_DELETE" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 12: action: "NODE_FILE_REPAIR" options: node_id: 2 - folder_id: 1 + folder_id: 0 file_id: 0 13: action: "NODE_SERVICE_PATCH" @@ -704,22 +748,22 @@ agents: action: "NODE_FOLDER_SCAN" options: node_id: 2 - folder_id: 1 + folder_id: 0 15: action: "NODE_FOLDER_CHECKHASH" options: node_id: 2 - folder_id: 1 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: node_id: 2 - folder_id: 1 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: node_id: 2 - folder_id: 1 + folder_id: 0 18: action: "NODE_OS_SCAN" options: @@ -736,63 +780,63 @@ agents: action: "NODE_RESET" options: node_id: 5 - 22: + 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" action: "NETWORK_ACL_ADDRULE" options: position: 1 permission: 2 - source_ip_id: 7 - dest_ip_id: 1 + source_ip_id: 7 # client 1 + dest_ip_id: 1 # ALL source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 23: + 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 2 permission: 2 - source_ip_id: 8 - dest_ip_id: 1 + source_ip_id: 8 # client 2 + dest_ip_id: 1 # ALL source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 24: + 24: # block tcp traffic from client 1 to web app action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 3 permission: 2 - source_ip_id: 7 - dest_ip_id: 3 + source_ip_id: 7 # client 1 + dest_ip_id: 3 # web server source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 25: + 25: # block tcp traffic from client 2 to web app action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 4 permission: 2 - source_ip_id: 8 - dest_ip_id: 3 + source_ip_id: 8 # client 2 + dest_ip_id: 3 # web server source_port_id: 1 dest_port_id: 1 protocol_id: 3 26: action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 5 permission: 2 - source_ip_id: 7 - dest_ip_id: 4 + source_ip_id: 7 # client 1 + dest_ip_id: 4 # database source_port_id: 1 dest_port_id: 1 protocol_id: 3 27: action: "NETWORK_ACL_ADDRULE" options: - position: 1 + position: 6 permission: 2 - source_ip_id: 8 - dest_ip_id: 4 + source_ip_id: 8 # client 2 + dest_ip_id: 4 # database source_port_id: 1 dest_port_id: 1 protocol_id: 3 @@ -840,122 +884,148 @@ agents: action: "NETWORK_NIC_DISABLE" options: node_id: 0 - nic_id: 1 + nic_id: 0 39: action: "NETWORK_NIC_ENABLE" options: node_id: 0 - nic_id: 1 + nic_id: 0 40: action: "NETWORK_NIC_DISABLE" options: node_id: 1 - nic_id: 1 + nic_id: 0 41: action: "NETWORK_NIC_ENABLE" options: node_id: 1 - nic_id: 1 + nic_id: 0 42: action: "NETWORK_NIC_DISABLE" options: node_id: 2 - nic_id: 1 + nic_id: 0 43: action: "NETWORK_NIC_ENABLE" options: node_id: 2 - nic_id: 1 + nic_id: 0 44: action: "NETWORK_NIC_DISABLE" options: node_id: 3 - nic_id: 1 + nic_id: 0 45: action: "NETWORK_NIC_ENABLE" options: node_id: 3 - nic_id: 1 + nic_id: 0 46: action: "NETWORK_NIC_DISABLE" options: node_id: 4 - nic_id: 1 + nic_id: 0 47: action: "NETWORK_NIC_ENABLE" options: node_id: 4 - nic_id: 1 + nic_id: 0 48: action: "NETWORK_NIC_DISABLE" options: node_id: 4 - nic_id: 2 + nic_id: 1 49: action: "NETWORK_NIC_ENABLE" options: node_id: 4 - nic_id: 2 + nic_id: 1 50: action: "NETWORK_NIC_DISABLE" options: node_id: 5 - nic_id: 1 + nic_id: 0 51: action: "NETWORK_NIC_ENABLE" options: node_id: 5 - nic_id: 1 + nic_id: 0 52: action: "NETWORK_NIC_DISABLE" options: node_id: 6 - nic_id: 1 + nic_id: 0 53: action: "NETWORK_NIC_ENABLE" options: node_id: 6 - nic_id: 1 + nic_id: 0 options: nodes: - - node_ref: domain_controller - - node_ref: web_server + - node_name: domain_controller + - node_name: web_server + applications: + - application_name: DatabaseClient services: - - service_ref: web_server_web_service - - node_ref: database_server + - service_name: WebServer + - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db services: - - service_ref: database_service - - node_ref: backup_server - - node_ref: security_suite - - node_ref: client_1 - - node_ref: client_2 + - 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_address_order: + - node_name: domain_controller + nic_num: 1 + - node_name: web_server + nic_num: 1 + - node_name: database_server + nic_num: 1 + - node_name: backup_server + nic_num: 1 + - node_name: security_suite + nic_num: 1 + - node_name: client_1 + nic_num: 1 + - node_name: client_2 + nic_num: 1 + - node_name: security_suite + nic_num: 2 + reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.5 + weight: 0.34 options: - node_ref: database_server + node_hostname: database_server folder_name: database file_name: database.db - - - - type: WEB_SERVER_404_PENALTY - weight: 0.5 + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.33 options: - node_ref: web_server - service_ref: web_server_web_service + node_hostname: client_1 + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.33 + options: + node_hostname: client_2 agent_settings: - # ... + flatten_obs: true @@ -1032,12 +1102,13 @@ simulation: default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - ref: database_server @@ -1089,10 +1160,14 @@ simulation: - ref: data_manipulation_bot type: DataManipulationBot options: - port_scan_p_of_success: 0.1 - data_manipulation_p_of_success: 0.1 + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 + - ref: client_1_web_browser + type: WebBrowser + options: + target_url: http://arcd.com/users/ services: - ref: client_1_dns_client type: DNSClient @@ -1109,6 +1184,13 @@ simulation: type: WebBrowser options: target_url: http://arcd.com/users/ + - ref: data_manipulation_bot + type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 services: - ref: client_2_dns_client type: DNSClient diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index f5649589..8edf70ea 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -79,11 +79,11 @@ class PrimaiteGame: self.simulation: Simulation = Simulation() """Simulation object with which the agents will interact.""" - self.agents: List[AbstractAgent] = [] - """List of agents.""" + self.agents: Dict[str, AbstractAgent] = {} + """Mapping from agent name to agent object.""" - self.rl_agents: List[ProxyAgent] = [] - """Subset of agent list including only the reinforcement learning agents.""" + self.rl_agents: Dict[str, ProxyAgent] = {} + """Subset of agents which are intended for reinforcement learning.""" self.step_counter: int = 0 """Current timestep within the episode.""" @@ -144,7 +144,7 @@ class PrimaiteGame: def update_agents(self, state: Dict) -> None: """Update agents' observations and rewards based on the current state.""" - for agent in self.agents: + for name, agent in self.agents.items(): agent.update_observation(state) agent.update_reward(state) agent.reward_function.total_reward += agent.reward_function.current_reward @@ -158,7 +158,7 @@ class PrimaiteGame: """ agent_actions = {} - for agent in self.agents: + for name, agent in self.agents.items(): obs = agent.observation_manager.current_observation rew = agent.reward_function.current_reward action_choice, options = agent.get_action(obs, rew) @@ -396,7 +396,6 @@ class PrimaiteGame: reward_function=reward_function, agent_settings=agent_settings, ) - game.agents.append(new_agent) elif agent_type == "ProxyAgent": new_agent = ProxyAgent( agent_name=agent_cfg["ref"], @@ -405,8 +404,7 @@ class PrimaiteGame: reward_function=reward_function, agent_settings=agent_settings, ) - game.agents.append(new_agent) - game.rl_agents.append(new_agent) + game.rl_agents[agent_cfg["ref"]] = new_agent elif agent_type == "RedDatabaseCorruptingAgent": new_agent = DataManipulationAgent( agent_name=agent_cfg["ref"], @@ -415,8 +413,8 @@ class PrimaiteGame: reward_function=reward_function, agent_settings=agent_settings, ) - game.agents.append(new_agent) else: _LOGGER.warning(f"agent type {agent_type} not found") + game.agents[agent_cfg["ref"]] = new_agent return game diff --git a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb index 0d4b6d0e..4ef02443 100644 --- a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb @@ -60,7 +60,7 @@ " policies={'defender_1','defender_2'}, # These names are the same as the agents defined in the example config.\n", " policy_mapping_fn=lambda agent_id, episode, worker, **kw: agent_id,\n", " )\n", - " .environment(env=PrimaiteRayMARLEnv, env_config={\"cfg\":cfg})#, disable_env_checking=True)\n", + " .environment(env=PrimaiteRayMARLEnv, env_config=cfg)#, disable_env_checking=True)\n", " .rollouts(num_rollout_workers=0)\n", " .training(train_batch_size=128)\n", " )\n" @@ -88,6 +88,13 @@ " param_space=config\n", ").fit()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/primaite/notebooks/training_example_ray_single_agent.ipynb b/src/primaite/notebooks/training_example_ray_single_agent.ipynb index ea006ae9..3c27bdc6 100644 --- a/src/primaite/notebooks/training_example_ray_single_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_single_agent.ipynb @@ -54,7 +54,7 @@ "metadata": {}, "outputs": [], "source": [ - "env_config = {\"cfg\":cfg}\n", + "env_config = cfg\n", "\n", "config = (\n", " PPOConfig()\n", diff --git a/src/primaite/notebooks/training_example_sb3.ipynb b/src/primaite/notebooks/training_example_sb3.ipynb index 164142b2..0472854e 100644 --- a/src/primaite/notebooks/training_example_sb3.ipynb +++ b/src/primaite/notebooks/training_example_sb3.ipynb @@ -27,9 +27,7 @@ "outputs": [], "source": [ "with open(example_config_path(), 'r') as f:\n", - " cfg = yaml.safe_load(f)\n", - "\n", - "game = PrimaiteGame.from_config(cfg)" + " cfg = yaml.safe_load(f)\n" ] }, { @@ -76,6 +74,13 @@ "source": [ "model.save(\"deleteme\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index bab81253..f8dbab9d 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, Final, Optional, SupportsFloat, Tuple +from typing import Any, Dict, Optional, SupportsFloat, Tuple import gymnasium from gymnasium.core import ActType, ObsType @@ -25,12 +25,17 @@ class PrimaiteGymEnv(gymnasium.Env): """PrimaiteGame definition. This can be changed between episodes to enable curriculum learning.""" self.game: PrimaiteGame = PrimaiteGame.from_config(self.game_config) """Current game.""" - self.agent: ProxyAgent = self.game.rl_agents[0] - """The agent within the game that is controlled by the RL algorithm.""" + self._agent_name = next(iter(self.game.rl_agents)) + """Name of the RL agent. Since there should only be one RL agent we can just pull the first and only key.""" self.episode_counter: int = 0 """Current episode number.""" + @property + def agent(self) -> ProxyAgent: + """Grab a fresh reference to the agent object because it will be reinstantiated each episode.""" + return self.game.rl_agents[self._agent_name] + def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]: """Perform a step in the environment.""" # make ProxyAgent store the action chosen my the RL policy @@ -71,11 +76,10 @@ class PrimaiteGymEnv(gymnasium.Env): """Reset the environment.""" print( f"Resetting environment, episode {self.episode_counter}, " - f"avg. reward: {self.game.rl_agents[0].reward_function.total_reward}" + f"avg. reward: {self.agent.reward_function.total_reward}" ) self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.game_config) self.game.setup_for_episode(episode=self.episode_counter) - self.agent = self.game.rl_agents[0] self.episode_counter += 1 state = self.game.get_sim_state() self.game.update_agents(state) @@ -112,11 +116,10 @@ class PrimaiteRayEnv(gymnasium.Env): def __init__(self, env_config: Dict) -> None: """Initialise the environment. - :param env_config: A dictionary containing the environment configuration. It must contain a single key, `game` - which is the PrimaiteGame instance. - :type env_config: Dict[str, PrimaiteGame] + :param env_config: A dictionary containing the environment configuration. + :type env_config: Dict """ - self.env = PrimaiteGymEnv(game=PrimaiteGame.from_config(env_config["cfg"])) + self.env = PrimaiteGymEnv(game_config=env_config) self.env.episode_counter -= 1 self.action_space = self.env.action_space self.observation_space = self.env.observation_space @@ -138,13 +141,16 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): :param env_config: A dictionary containing the environment configuration. It must contain a single key, `game` which is the PrimaiteGame instance. - :type env_config: Dict[str, PrimaiteGame] + :type env_config: Dict """ - self.game: PrimaiteGame = PrimaiteGame.from_config(env_config["cfg"]) + self.game_config: Dict = env_config + """PrimaiteGame definition. This can be changed between episodes to enable curriculum learning.""" + self.game: PrimaiteGame = PrimaiteGame.from_config(self.game_config) """Reference to the primaite game""" - self.agents: Final[Dict[str, ProxyAgent]] = {agent.agent_name: agent for agent in self.game.rl_agents} - """List of all possible agents in the environment. This list should not change!""" - self._agent_ids = list(self.agents.keys()) + self._agent_ids = list(self.game.rl_agents.keys()) + """Agent ids. This is a list of strings of agent names.""" + self.episode_counter: int = 0 + """Current episode number.""" self.terminateds = set() self.truncateds = set() @@ -159,9 +165,16 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): ) super().__init__() + @property + def agents(self) -> Dict[str, ProxyAgent]: + """Grab a fresh reference to the agents from this episode's game object.""" + return {name: self.game.rl_agents[name] for name in self._agent_ids} + def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" - self.game.reset() + self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.game_config) + self.game.setup_for_episode(episode=self.episode_counter) + self.episode_counter += 1 state = self.game.get_sim_state() self.game.update_agents(state) next_obs = self._get_obs() @@ -182,7 +195,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): # 1. Perform actions for agent_name, action in actions.items(): self.agents[agent_name].store_action(action) - agent_actions = self.game.apply_agent_actions() + self.game.apply_agent_actions() # 2. Advance timestep self.game.advance_timestep() @@ -196,7 +209,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): rewards = {name: agent.reward_function.current_reward for name, agent in self.agents.items()} terminateds = {name: False for name, _ in self.agents.items()} truncateds = {name: self.game.calculate_truncated() for name, _ in self.agents.items()} - infos = {"agent_actions": agent_actions} + infos = {name: {} for name, _ in self.agents.items()} terminateds["__all__"] = len(self.terminateds) == len(self.agents) truncateds["__all__"] = self.game.calculate_truncated() if self.game.save_step_metadata: @@ -222,8 +235,9 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def _get_obs(self) -> Dict[str, ObsType]: """Return the current observation.""" obs = {} - for name, agent in self.agents.items(): + for agent_name in self._agent_ids: + agent = self.game.rl_agents[agent_name] unflat_space = agent.observation_manager.space unflat_obs = agent.observation_manager.current_observation - obs[name] = gymnasium.spaces.flatten(unflat_space, unflat_obs) + obs[agent_name] = gymnasium.spaces.flatten(unflat_space, unflat_obs) return obs diff --git a/tests/conftest.py b/tests/conftest.py index 5084c339..83ac9559 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -510,6 +510,6 @@ def game_and_agent(): reward_function=reward_function, ) - game.agents.append(test_agent) + game.agents["test_agent"] = test_agent return (game, test_agent) diff --git a/tests/integration_tests/game_configuration.py b/tests/integration_tests/game_configuration.py index 3bd870e3..f3dc51bd 100644 --- a/tests/integration_tests/game_configuration.py +++ b/tests/integration_tests/game_configuration.py @@ -42,20 +42,20 @@ def test_example_config(): assert len(game.agents) == 4 # red, blue and 2 green agents # green agent 1 - assert game.agents[0].agent_name == "client_2_green_user" - assert isinstance(game.agents[0], RandomAgent) + assert "client_2_green_user" in game.agents + assert isinstance(game.agents["client_2_green_user"], RandomAgent) # green agent 2 - assert game.agents[1].agent_name == "client_1_green_user" - assert isinstance(game.agents[1], RandomAgent) + assert "client_1_green_user" in game.agents + assert isinstance(game.agents["client_1_green_user"], RandomAgent) # red agent - assert game.agents[2].agent_name == "client_1_data_manipulation_red_bot" - assert isinstance(game.agents[2], DataManipulationAgent) + assert "client_1_data_manipulation_red_bot" in game.agents + assert isinstance(game.agents["client_1_data_manipulation_red_bot"], DataManipulationAgent) # blue agent - assert game.agents[3].agent_name == "defender" - assert isinstance(game.agents[3], ProxyAgent) + assert "defender" in game.agents + assert isinstance(game.agents["defender"], ProxyAgent) network: Network = game.simulation.network From ccb10f1160cee454216c205e15c08d2ecc0c02d7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 11:02:37 +0000 Subject: [PATCH 031/128] Update docs based on reset refactor --- CHANGELOG.md | 1 + docs/index.rst | 1 + docs/source/environment.rst | 10 ++++++++++ docs/source/game_layer.rst | 5 +++++ docs/source/primaite_session.rst | 5 +++++ 5 files changed, 22 insertions(+) create mode 100644 docs/source/environment.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e45d2e..d2a582be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Made environment reset completely recreate the game object. - Changed the red agent in the data manipulation scenario to randomly choose client 1 or client 2 to start its attack. - Changed the data manipulation scenario to include a second green agent on client 1. - Refactored actions and observations to be configurable via object name, instead of UUID. diff --git a/docs/index.rst b/docs/index.rst index 9eae8adc..08e0ac21 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -108,6 +108,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/simulation source/game_layer source/config + source/environment .. toctree:: :caption: Developer information: diff --git a/docs/source/environment.rst b/docs/source/environment.rst new file mode 100644 index 00000000..87e7f060 --- /dev/null +++ b/docs/source/environment.rst @@ -0,0 +1,10 @@ +RL Environments +*************** + +RL environments are the objects that directly interface with RL libraries such as Stable-Baselines3 and Ray RLLib. The PrimAITE simulation is exposed via three different environment APIs: + +* Gymnasium API - this is the standard interface that works with many RL libraries like SB3, Ray, Tianshou, etc. ``PrimaiteGymEnv`` adheres to the `Official Gymnasium documentation `_. +* Ray Single agent API - For training a single Ray RLLib agent +* Ray MARL API - For training multi-agent systems with Ray RLLib. ``PrimaiteRayMARLEnv`` adheres to the `Official Ray documentation `_. + +There is a Jupyter notebook which demonstrates integration with each of these three environments. They are located in ``~/primaite//notebooks/example_notebooks``. diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index cdae17dd..1f2921fe 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -20,6 +20,11 @@ The game layer is responsible for managing agents and getting them to interface PrimAITE Session ^^^^^^^^^^^^^^^ +.. admonition:: Deprecated + :class: deprecated + + PrimAITE Session is being deprecated in favour of Jupyter Notebooks. The `session` command will be removed in future releases, but example notebooks will be provided to demonstrate the same functionality. + ``PrimaiteSession`` is the main entry point into Primaite and it allows the simultaneous coordination of a simulation and agents that interact with it. ``PrimaiteSession`` keeps track of multiple agents of different types. Agents diff --git a/docs/source/primaite_session.rst b/docs/source/primaite_session.rst index 706397b6..87a3f03d 100644 --- a/docs/source/primaite_session.rst +++ b/docs/source/primaite_session.rst @@ -4,6 +4,11 @@ .. _run a primaite session: +.. admonition:: Deprecated + :class: deprecated + + PrimAITE Session is being deprecated in favour of Jupyter Notebooks. The ``session`` command will be removed in future releases, but example notebooks will be provided to demonstrate the same functionality. + Run a PrimAITE Session ====================== From d738a2370935c408973777ae5aeea542ba6e5294 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 26 Feb 2024 11:35:17 +0000 Subject: [PATCH 032/128] #2257: list of db payloads --- .../system/applications/data_manipulation_bot.rst | 2 +- .../system/applications/dos_bot.rst | 4 +++- .../system/common/common_configuration.rst | 4 ++++ .../system/common/db_payload_list.rst | 11 +++++++++++ .../system/services/database/database_service.py | 2 -- 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 docs/source/simulation_components/system/common/db_payload_list.rst 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 209cdcbd..d0e89f2e 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -178,7 +178,7 @@ Optional. Default value is ``DELETE``. The payload that the ``DataManipulationBot`` will send to the :ref:`DatabaseService`. -See :ref:`Database Payload List` +.. include:: ../common/db_payload_list.rst ``port_scan_p_of_success`` """""""""""""""""""""""""" diff --git a/docs/source/simulation_components/system/applications/dos_bot.rst b/docs/source/simulation_components/system/applications/dos_bot.rst index fcf3f207..6ddbac72 100644 --- a/docs/source/simulation_components/system/applications/dos_bot.rst +++ b/docs/source/simulation_components/system/applications/dos_bot.rst @@ -123,6 +123,8 @@ Optional. Default value is ``None``. The payload that the ``DoSBot`` sends as part of its attack. +.. include:: ../common/db_payload_list.rst + ``repeat`` """""""""" @@ -155,4 +157,4 @@ Optional. Default value is ``1000``. The maximum number of sessions the ``DoSBot`` is able to make. -This must be an integer value above equal to or greater than ``0``. +This must be an integer value equal to or greater than ``0``. diff --git a/docs/source/simulation_components/system/common/common_configuration.rst b/docs/source/simulation_components/system/common/common_configuration.rst index 86991655..27625407 100644 --- a/docs/source/simulation_components/system/common/common_configuration.rst +++ b/docs/source/simulation_components/system/common/common_configuration.rst @@ -1,3 +1,7 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + ``ref`` ======= diff --git a/docs/source/simulation_components/system/common/db_payload_list.rst b/docs/source/simulation_components/system/common/db_payload_list.rst new file mode 100644 index 00000000..f51227c6 --- /dev/null +++ b/docs/source/simulation_components/system/common/db_payload_list.rst @@ -0,0 +1,11 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _Database Payload List: + +Available Database Payloads: + +- ``SELECT`` +- ``INSERT`` +- ``DELETE`` diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index c0390b4f..726d213e 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -195,8 +195,6 @@ class DatabaseService(Service): """ Executes the given SQL query and returns the result. - .. _Database Payload List: - Possible queries: - SELECT : returns the data - DELETE : deletes the data From a5043a8fbe01b38699cedf677129a17bec32655d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 12:15:53 +0000 Subject: [PATCH 033/128] Modify tests based on refactoring --- src/primaite/session/session.py | 6 ++-- src/primaite/simulator/network/airspace.py | 5 --- .../network/hardware/nodes/host/host_node.py | 5 --- .../hardware/nodes/network/firewall.py | 18 ---------- .../network/hardware/nodes/network/switch.py | 6 ---- .../hardware/nodes/network/wireless_router.py | 3 -- .../assets/configs/bad_primaite_session.yaml | 7 ++-- .../configs/eval_only_primaite_session.yaml | 9 ++--- tests/assets/configs/multi_agent_session.yaml | 10 +++--- .../assets/configs/test_primaite_session.yaml | 9 ++--- .../configs/train_only_primaite_session.yaml | 9 ++--- .../environments/test_sb3_environment.py | 3 +- .../_simulator/_domain/test_account.py | 19 ----------- .../_file_system/test_file_system.py | 31 ----------------- .../_simulator/_network/test_container.py | 34 ------------------- .../_red_applications/test_dos_bot.py | 28 --------------- 16 files changed, 28 insertions(+), 174 deletions(-) diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index 5c663cfd..b8f80e95 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -101,11 +101,11 @@ class PrimaiteSession: # CREATE ENVIRONMENT if sess.training_options.rl_framework == "RLLIB_single_agent": - sess.env = PrimaiteRayEnv(env_config={"cfg": cfg}) + sess.env = PrimaiteRayEnv(env_config=cfg) elif sess.training_options.rl_framework == "RLLIB_multi_agent": - sess.env = PrimaiteRayMARLEnv(env_config={"cfg": cfg}) + sess.env = PrimaiteRayMARLEnv(env_config=cfg) elif sess.training_options.rl_framework == "SB3": - sess.env = PrimaiteGymEnv(game=game) + sess.env = PrimaiteGymEnv(game_config=cfg) sess.policy = PolicyABC.from_config(sess.training_options, session=sess) if agent_load_path: diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 724b8728..d264f751 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -273,11 +273,6 @@ class IPWirelessNetworkInterface(WirelessNetworkInterface, Layer3Interface, ABC) return state - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"ip_address", "subnet_mask", "mac_address", "speed", "mtu", "wake_on_lan", "enabled"} - self._original_state = self.model_dump(include=vals_to_include) - def enable(self): """ Enables this wired network interface and attempts to send a "hello" message to the default gateway. 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 3f34f736..329a5fa0 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -213,11 +213,6 @@ class NIC(IPWiredNetworkInterface): return state - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"ip_address", "subnet_mask", "mac_address", "speed", "mtu", "wake_on_lan", "enabled"} - self._original_state = self.model_dump(include=vals_to_include) - def receive_frame(self, frame: Frame) -> bool: """ Attempt to receive and process a network frame from the connected Link. diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 22effa2a..f2305652 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -109,24 +109,6 @@ class Firewall(Router): sys_log=kwargs["sys_log"], implicit_action=ACLAction.PERMIT, name=f"{hostname} - External Outbound" ) - self.set_original_state() - - def set_original_state(self): - """Set the original state for the Firewall.""" - super().set_original_state() - vals_to_include = { - "internal_port", - "external_port", - "dmz_port", - "internal_inbound_acl", - "internal_outbound_acl", - "dmz_inbound_acl", - "dmz_outbound_acl", - "external_inbound_acl", - "external_outbound_acl", - } - self._original_state.update(self.model_dump(include=vals_to_include)) - def describe_state(self) -> Dict: """ Describes the current state of the Firewall. diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 33e6ee9a..557ea287 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -32,12 +32,6 @@ class SwitchPort(WiredNetworkInterface): _connected_node: Optional[Switch] = None "The Switch to which the SwitchPort is connected." - def set_original_state(self): - """Sets the original state.""" - vals_to_include = {"port_num", "mac_address", "speed", "mtu", "enabled"} - self._original_state = self.model_dump(include=vals_to_include) - super().set_original_state() - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. 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 dd0b58d3..91833d6a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -122,8 +122,6 @@ class WirelessRouter(Router): self.connect_nic(RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0")) - self.set_original_state() - @property def wireless_access_point(self) -> WirelessAccessPoint: """ @@ -166,7 +164,6 @@ class WirelessRouter(Router): network_interface.ip_address = ip_address network_interface.subnet_mask = subnet_mask self.sys_log.info(f"Configured WAP {network_interface}") - self.set_original_state() self.wireless_access_point.frequency = frequency # Set operating frequency self.wireless_access_point.enable() # Re-enable the WAP with new settings diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 5bdc3273..c76aeef6 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -589,15 +589,16 @@ simulation: hostname: web_server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 - default_gateway: 192.168.1.10 + default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - ref: database_server diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 8361e318..1cb59f87 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -593,15 +593,16 @@ simulation: hostname: web_server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 - default_gateway: 192.168.1.10 + default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - ref: database_server @@ -624,7 +625,7 @@ simulation: dns_server: 192.168.1.10 services: - ref: backup_service - type: DatabaseBackup + type: FTPServer - ref: security_suite type: server diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 87bd9d1c..b1b15372 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -1043,16 +1043,16 @@ simulation: hostname: web_server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 - default_gateway: 192.168.1.10 + default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - - ref: database_server type: server @@ -1074,7 +1074,7 @@ simulation: dns_server: 192.168.1.10 services: - ref: backup_service - type: DatabaseBackup + type: FTPServer - ref: security_suite type: server diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 76190a64..e5f9d544 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -599,15 +599,16 @@ simulation: hostname: web_server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 - default_gateway: 192.168.1.10 + default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - ref: database_server @@ -630,7 +631,7 @@ simulation: dns_server: 192.168.1.10 services: - ref: backup_service - type: DatabaseBackup + type: FTPServer - ref: security_suite type: server diff --git a/tests/assets/configs/train_only_primaite_session.yaml b/tests/assets/configs/train_only_primaite_session.yaml index 5d004c7e..10e088d8 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -600,15 +600,16 @@ simulation: hostname: web_server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 - default_gateway: 192.168.1.10 + default_gateway: 192.168.1.1 dns_server: 192.168.1.10 services: + - ref: web_server_web_service + type: WebServer + applications: - ref: web_server_database_client type: DatabaseClient options: db_server_ip: 192.168.1.14 - - ref: web_server_web_service - type: WebServer - ref: database_server @@ -631,7 +632,7 @@ simulation: dns_server: 192.168.1.10 services: - ref: backup_service - type: DatabaseBackup + type: FTPServer - ref: security_suite type: server diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index 91cf5c1e..dc5d10e9 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -17,8 +17,7 @@ def test_sb3_compatibility(): with open(example_config_path(), "r") as f: cfg = yaml.safe_load(f) - game = PrimaiteGame.from_config(cfg) - gym = PrimaiteGymEnv(game=game) + gym = PrimaiteGymEnv(game_config=cfg) model = PPO("MlpPolicy", gym) model.learn(total_timesteps=1000) diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py index 695b15dd..786fe851 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py @@ -12,20 +12,6 @@ def account() -> Account: def test_original_state(account): """Test the original state - see if it resets properly""" - account.log_on() - account.log_off() - account.disable() - - state = account.describe_state() - assert state["num_logons"] is 1 - assert state["num_logoffs"] is 1 - assert state["num_group_changes"] is 0 - assert state["username"] is "Jake" - assert state["password"] is "totally_hashed_password" - assert state["account_type"] is AccountType.USER.value - assert state["enabled"] is False - - account.reset_component_for_episode(episode=1) state = account.describe_state() assert state["num_logons"] is 0 assert state["num_logoffs"] is 0 @@ -39,11 +25,6 @@ def test_original_state(account): account.log_off() account.disable() - account.log_on() - state = account.describe_state() - assert state["num_logons"] is 2 - - account.reset_component_for_episode(episode=2) state = account.describe_state() assert state["num_logons"] is 1 assert state["num_logoffs"] is 1 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 2fe3f04c..4defc80c 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 @@ -185,37 +185,6 @@ def test_get_file(file_system): file_system.show(full=True) -def test_reset_file_system(file_system): - # file and folder that existed originally - file_system.create_file(file_name="test_file.zip") - file_system.create_folder(folder_name="test_folder") - - # create a new file - file_system.create_file(file_name="new_file.txt") - - # create a new folder - file_system.create_folder(folder_name="new_folder") - - # delete the file that existed originally - file_system.delete_file(folder_name="root", file_name="test_file.zip") - assert file_system.get_file(folder_name="root", file_name="test_file.zip") is None - - # delete the folder that existed originally - file_system.delete_folder(folder_name="test_folder") - assert file_system.get_folder(folder_name="test_folder") is None - - # reset - file_system.reset_component_for_episode(episode=1) - - # deleted original file and folder should be back - assert file_system.get_file(folder_name="root", file_name="test_file.zip") - assert file_system.get_folder(folder_name="test_folder") - - # new file and folder should be removed - assert file_system.get_file(folder_name="root", file_name="new_file.txt") is None - assert file_system.get_folder(folder_name="new_folder") is None - - @pytest.mark.skip(reason="Skipping until we tackle serialisation") def test_serialisation(file_system): """Test to check that the object serialisation works correctly.""" diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index bf79677e..2cfc3f11 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -44,40 +44,6 @@ def test_describe_state(network): assert len(state["links"]) is 6 -def test_reset_network(network): - """ - Test that the network is properly reset. - - TODO: make sure that once implemented - any installed/uninstalled services, processes, apps, - etc are also removed/reinstalled - - """ - state_before = network.describe_state() - - client_1: Computer = network.get_node_by_hostname("client_1") - server_1: Computer = network.get_node_by_hostname("server_1") - - assert client_1.operating_state is NodeOperatingState.ON - assert server_1.operating_state is NodeOperatingState.ON - - client_1.power_off() - assert client_1.operating_state is NodeOperatingState.SHUTTING_DOWN - - server_1.power_off() - assert server_1.operating_state is NodeOperatingState.SHUTTING_DOWN - - assert network.describe_state() != state_before - - network.reset_component_for_episode(episode=1) - - assert client_1.operating_state is NodeOperatingState.ON - assert server_1.operating_state is NodeOperatingState.ON - # don't worry if UUIDs change - a = filter_keys_nested_item(json.dumps(network.describe_state(), sort_keys=True, indent=2), ["uuid"]) - b = filter_keys_nested_item(json.dumps(state_before, sort_keys=True, indent=2), ["uuid"]) - assert a == b - - def test_creating_container(): """Check that we can create a network container""" net = Network() 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 1f28244d..4bfd28d0 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 @@ -27,34 +27,6 @@ def test_dos_bot_creation(dos_bot): assert dos_bot is not None -def test_dos_bot_reset(dos_bot): - assert dos_bot.target_ip_address == IPv4Address("192.168.0.1") - assert dos_bot.target_port is Port.POSTGRES_SERVER - assert dos_bot.payload is None - assert dos_bot.repeat is False - - dos_bot.configure( - target_ip_address=IPv4Address("192.168.1.1"), target_port=Port.HTTP, payload="payload", repeat=True - ) - - # should reset the relevant items - dos_bot.reset_component_for_episode(episode=0) - assert dos_bot.target_ip_address == IPv4Address("192.168.0.1") - assert dos_bot.target_port is Port.POSTGRES_SERVER - assert dos_bot.payload is None - assert dos_bot.repeat is False - - dos_bot.configure( - target_ip_address=IPv4Address("192.168.1.1"), target_port=Port.HTTP, payload="payload", repeat=True - ) - dos_bot.reset_component_for_episode(episode=1) - # should reset to the configured value - assert dos_bot.target_ip_address == IPv4Address("192.168.1.1") - assert dos_bot.target_port is Port.HTTP - assert dos_bot.payload == "payload" - assert dos_bot.repeat is True - - def test_dos_bot_cannot_run_when_node_offline(dos_bot): dos_bot_node: Computer = dos_bot.parent assert dos_bot_node.operating_state is NodeOperatingState.ON From 2076b011ba8837f8e85e362a68c71b1010aa8e0a Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 14:26:47 +0000 Subject: [PATCH 034/128] Put back default router rules --- src/primaite/simulator/network/hardware/nodes/network/router.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 3111a153..52f38eb6 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1039,6 +1039,8 @@ class Router(NetworkNode): self.connect_nic(network_interface) self.network_interface[i] = network_interface + self._set_default_acl() + def _install_system_software(self): """ Installs essential system software and network services on the router. From f2d7a2fc1646e86f1c2fbb2d3f47020ac785a7c5 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 26 Feb 2024 14:34:34 +0000 Subject: [PATCH 035/128] #2257: added way to ensure nodes are on at start + more test to make sure nodes are on when added via config --- src/primaite/game/game.py | 9 +++++++-- .../nodes/network/test_firewall_config.py | 11 ++++++++++- .../nodes/network/test_router_config.py | 6 +++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index ef54893e..fbf6ea50 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -359,13 +359,18 @@ class PrimaiteGame: 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"])) - new_node.start_up_duration = int(node_cfg.get("start_up_duration", 3)) - new_node.shut_down_duration = int(node_cfg.get("shut_down_duration", 3)) + # temporarily set to 0 so all nodes are initially on + new_node.start_up_duration = 0 + new_node.shut_down_duration = 0 net.add_node(new_node) new_node.power_on() game.ref_map_nodes[node_ref] = new_node.uuid + # set start up and shut down duration + new_node.start_up_duration = int(node_cfg.get("start_up_duration", 3)) + new_node.shut_down_duration = int(node_cfg.get("shut_down_duration", 3)) + # 2. create links between nodes for link_cfg in links_cfg: node_a = net.nodes[game.ref_map_nodes[link_cfg["endpoint_a_ref"]]] 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 ae71809b..2e0556e9 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,6 +1,7 @@ import pytest from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.firewall import Firewall @@ -20,7 +21,10 @@ def test_firewall_is_in_configuration(dmz_config): """Test that the firewall exists in the configuration file.""" network: Network = dmz_config - assert network.get_node_by_hostname("firewall") + firewall: Firewall = network.get_node_by_hostname("firewall") + + assert firewall + assert firewall.operating_state == NodeOperatingState.ON def test_firewall_routes_are_correctly_added(dmz_config): @@ -39,6 +43,11 @@ def test_firewall_routes_are_correctly_added(dmz_config): assert external_computer.ping(client_1.network_interface[1].ip_address) assert external_server.ping(client_1.network_interface[1].ip_address) + # client_1 should be able to ping other nodes + assert client_1.ping(dmz_server.network_interface[1].ip_address) + assert client_1.ping(external_computer.network_interface[1].ip_address) + assert client_1.ping(external_server.network_interface[1].ip_address) + def test_firewall_acl_rules_correctly_added(dmz_config): """ diff --git a/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py b/tests/integration_tests/configuration_file_parsing/nodes/network/test_router_config.py index fbaca12d..4382cc30 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,6 +1,7 @@ import pytest from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router @@ -19,7 +20,10 @@ def test_router_is_in_configuration(dmz_config): """Test that the router exists in the configuration file.""" network: Network = dmz_config - assert network.get_node_by_hostname("router_1") + router_1: Router = network.get_node_by_hostname("router_1") + + assert router_1 + assert router_1.operating_state == NodeOperatingState.ON def test_router_routes_are_correctly_added(dmz_config): From f9cc5af7aab3d822dcc23472f65c74a04ced4650 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 16:06:58 +0000 Subject: [PATCH 036/128] Not sure how this test was passing before --- .../_primaite/_simulator/_system/test_software.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/_primaite/_simulator/_system/test_software.py b/tests/unit_tests/_primaite/_simulator/_system/test_software.py index e77cd895..6f680012 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/test_software.py +++ b/tests/unit_tests/_primaite/_simulator/_system/test_software.py @@ -2,12 +2,14 @@ from typing import Dict import pytest +from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.sys_log import SysLog -from primaite.simulator.system.software import Software, SoftwareHealthState +from primaite.simulator.system.services.service import Service +from primaite.simulator.system.software import IOSoftware, SoftwareHealthState -class TestSoftware(Software): +class TestSoftware(Service): def describe_state(self) -> Dict: pass @@ -15,7 +17,11 @@ class TestSoftware(Software): @pytest.fixture(scope="function") def software(file_system): return TestSoftware( - name="TestSoftware", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="test_service") + name="TestSoftware", + port=Port.ARP, + file_system=file_system, + sys_log=SysLog(hostname="test_service"), + protocol=IPProtocol.TCP, ) From 33d2ecc26a4ac125607477c0a2846afa1b6fc728 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 16:58:43 +0000 Subject: [PATCH 037/128] Apply suggestions from code review. --- docs/source/environment.rst | 2 +- .../config/_package_data/example_config_2_rl_agents.yaml | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/source/environment.rst b/docs/source/environment.rst index 87e7f060..2b76572d 100644 --- a/docs/source/environment.rst +++ b/docs/source/environment.rst @@ -7,4 +7,4 @@ RL environments are the objects that directly interface with RL libraries such a * Ray Single agent API - For training a single Ray RLLib agent * Ray MARL API - For training multi-agent systems with Ray RLLib. ``PrimaiteRayMARLEnv`` adheres to the `Official Ray documentation `_. -There is a Jupyter notebook which demonstrates integration with each of these three environments. They are located in ``~/primaite//notebooks/example_notebooks``. +There are Jupyter notebooks which demonstrate integration with each of these three environments. They are located in ``~/primaite//notebooks/example_notebooks``. diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 1ccd7b38..c1e077be 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -1,11 +1,17 @@ training_config: rl_framework: RLLIB_multi_agent - # rl_framework: SB3 + rl_algorithm: PPO + seed: 333 + n_learn_episodes: 1 + n_eval_episodes: 5 + max_steps_per_episode: 256 + deterministic_eval: false n_agents: 2 agent_references: - defender_1 - defender_2 + io_settings: save_checkpoints: true checkpoint_interval: 5 From 922298eaf02f2ab7897f7523aa9bf3122add51f4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 20:07:02 +0000 Subject: [PATCH 038/128] Make database admin action possible --- .../config/_package_data/example_config.yaml | 69 ++++++++++++++++--- src/primaite/game/agent/rewards.py | 41 +++++++++++ .../system/applications/database_client.py | 30 +++++++- 3 files changed, 129 insertions(+), 11 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index f85baf10..a0e9667e 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -33,7 +33,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: GreenUC2Agent observation_space: type: UC2GreenObservation action_space: @@ -45,24 +45,39 @@ agents: - 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: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 reward_function: reward_components: - type: DUMMY agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 - ref: client_1_green_user team: GREEN - type: GreenWebBrowsingAgent + type: GreenUC2Agent observation_space: type: UC2GreenObservation action_space: @@ -74,14 +89,36 @@ agents: - 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: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + reward_function: reward_components: - type: DUMMY + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + @@ -572,6 +609,14 @@ agents: weight: 0.33 options: node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.1 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.1 + options: + node_hostname: client_2 agent_settings: @@ -717,6 +762,10 @@ simulation: type: WebBrowser options: target_url: http://arcd.com/users/ + - ref: client_1_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - ref: client_1_dns_client type: DNSClient @@ -740,6 +789,10 @@ simulation: data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 + - ref: client_2_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - ref: client_2_dns_client type: DNSClient diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index b5d5f998..acc37711 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -242,6 +242,46 @@ class WebpageUnavailablePenalty(AbstractReward): return cls(node_hostname=node_hostname) +class GreenAdminDatabaseUnreachablePenalty(AbstractReward): + """Penalises the agent when the green db clients fail to connect to the database.""" + + def __init__(self, node_hostname: str) -> None: + """ + Initialise the reward component. + + :param node_hostname: Hostname of the node where the database client sits. + :type node_hostname: str + """ + self._node = node_hostname + self.location_in_state = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] + + def calculate(self, state: Dict) -> float: + """ + Calculate the reward based on current simulation state. + + :param state: The current state of the simulation. + :type state: Dict + """ + db_state = access_from_nested_dict(state, self.location_in_state) + if db_state is NOT_PRESENT_IN_STATE or "connections_status" not in db_state: + _LOGGER.debug(f"Can't calculate reward for {self.__class__.__name__}") + connections_status = db_state["connections_status"] + if False in connections_status: + return -1.0 + return 0 + + @classmethod + def from_config(cls, config: Dict) -> AbstractReward: + """ + Build the reward component object from config. + + :param config: Configuration dictionary. + :type config: Dict + """ + node_hostname = config.get("node_hostname") + return cls(node_hostname=node_hostname) + + class RewardFunction: """Manages the reward function for the agent.""" @@ -250,6 +290,7 @@ class RewardFunction: "DATABASE_FILE_INTEGRITY": DatabaseFileIntegrity, "WEB_SERVER_404_PENALTY": WebServer404Penalty, "WEBPAGE_UNAVAILABLE_PENALTY": WebpageUnavailablePenalty, + "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY": GreenAdminDatabaseUnreachablePenalty, } def __init__(self): diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 50d9f3d4..67c0c9b4 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -1,8 +1,9 @@ from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from uuid import uuid4 from primaite import getLogger +from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application @@ -25,6 +26,8 @@ class DatabaseClient(Application): server_password: Optional[str] = None connected: bool = False _query_success_tracker: Dict[str, bool] = {} + _connections_status: List[bool] = [] + """Keep track of connections that were established or verified during this step. Used for rewards.""" def __init__(self, **kwargs): kwargs["name"] = "DatabaseClient" @@ -33,6 +36,20 @@ class DatabaseClient(Application): super().__init__(**kwargs) self.set_original_state() + def _init_request_manager(self) -> RequestManager: + rm = super()._init_request_manager() + rm.add_request("execute", RequestType(func=lambda request, context: self.execute())) + return rm + + def execute(self) -> bool: + """Execution definition for db client: perform a select query.""" + if self.connections: + can_connect = self.connect(connection_id=list(self.connections.keys())[-1]) + else: + can_connect = self.connect() + self._connections_status.append(can_connect) + return can_connect + def set_original_state(self): """Sets the original state.""" _LOGGER.debug(f"Setting DatabaseClient WebServer original state on node {self.software_manager.node.hostname}") @@ -52,8 +69,11 @@ class DatabaseClient(Application): :return: A dictionary representing the current state. """ - pass - return super().describe_state() + state = super().describe_state() + # list of connections that were established or verified during this step. + state["connections_status"] = [c for c in self._connections_status] + self._connections_status.clear() + return state def configure(self, server_ip_address: IPv4Address, server_password: Optional[str] = None): """ @@ -74,6 +94,10 @@ class DatabaseClient(Application): if not connection_id: connection_id = str(uuid4()) + # if we are reusing a connection_id, remove it from self.connections so that its new status can be populated + # warning: janky + self._connections.pop(connection_id, None) + self.connected = self._connect( server_ip_address=self.server_ip_address, password=self.server_password, connection_id=connection_id ) From c54f82fb1bd8c961bc4d7a250e8ea9572fb44b33 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 26 Feb 2024 20:08:13 +0000 Subject: [PATCH 039/128] Start implementing green agent logic for UC2 --- src/primaite/game/agent/scripted_agents.py | 62 +++++++++++++++++++++- src/primaite/game/game.py | 5 +- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/agent/scripted_agents.py b/src/primaite/game/agent/scripted_agents.py index 3748494b..a88e563d 100644 --- a/src/primaite/game/agent/scripted_agents.py +++ b/src/primaite/game/agent/scripted_agents.py @@ -1,10 +1,70 @@ """Agents with predefined behaviours.""" +from typing import Dict, Optional, Tuple + +import numpy as np +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.observations import ObservationManager +from primaite.game.agent.rewards import RewardFunction -class GreenWebBrowsingAgent(AbstractScriptedAgent): +class GreenUC2Agent(AbstractScriptedAgent): """Scripted agent which attempts to send web requests to a target node.""" + class GreenUC2AgentSettings(pydantic.BaseModel): + model_config = pydantic.ConfigDict(extra="forbid") + action_probabilities: Dict[int, float] + """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" + random_seed: Optional[int] = None + + @pydantic.field_validator("action_probabilities", mode="after") + @classmethod + def probabilities_sum_to_one(cls, v: Dict[int, float]) -> Dict[int, float]: + if not abs(sum(v.values()) - 1) < 1e-6: + raise ValueError(f"Green action probabilities must sum to 1") + return v + + @pydantic.field_validator("action_probabilities", mode="after") + @classmethod + def action_map_covered_correctly(cls, v: Dict[int, float]) -> Dict[int, float]: + if not all((i in v) for i in range(len(v))): + raise ValueError( + "Green action probabilities must be defined as a mapping where the keys are consecutive integers " + "from 0 to N." + ) + + 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)}} + + # If seed not specified, set it to None so that numpy chooses a random one. + settings.setdefault("random_seed") + + self.settings = GreenUC2Agent.GreenUC2AgentSettings(settings) + + self.rng = np.random.default_rng(self.settings.random_seed) + + # convert probabilities from + self.probabilities = np.array[self.settings.action_probabilities.values()] + + super().__init__(agent_name, action_space, observation_space, reward_function) + + def get_action(self, obs: ObsType, reward: float = 0) -> Tuple[str, Dict]: + choice = self.rng.choice(len(self.action_manager.action_map), p=self.probabilities) + return self.action_manager.get_action(choice) + raise NotImplementedError diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index ed98accd..a9d564ba 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -10,6 +10,7 @@ from primaite.game.agent.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent, RandomAgent from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction +from primaite.game.agent.scripted_agents import GreenUC2Agent from primaite.session.io import SessionIO, SessionIOSettings from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -392,9 +393,9 @@ class PrimaiteGame: agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) # CREATE AGENT - if agent_type == "GreenWebBrowsingAgent": + if agent_type == "GreenUC2Agent": # TODO: implement non-random agents and fix this parsing - new_agent = RandomAgent( + new_agent = GreenUC2Agent( agent_name=agent_cfg["ref"], action_space=action_space, observation_space=obs_space, From af8ca82fcbbb22a7c3d529b7f28f1befb1c20104 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 27 Feb 2024 13:30:16 +0000 Subject: [PATCH 040/128] Get the db admin green agent working --- .../config/_package_data/example_config.yaml | 19 +- .../example_config_2_rl_agents.yaml | 2 +- src/primaite/game/agent/actions.py | 43 ++-- .../game/agent/data_manipulation_bot.py | 10 +- src/primaite/game/agent/interface.py | 10 +- src/primaite/game/agent/scripted_agents.py | 43 ++-- src/primaite/game/game.py | 13 +- src/primaite/notebooks/uc2_demo.ipynb | 232 +++++++++++++++++- .../assets/configs/bad_primaite_session.yaml | 2 +- .../configs/basic_switched_network.yaml | 2 +- .../configs/eval_only_primaite_session.yaml | 2 +- tests/assets/configs/multi_agent_session.yaml | 2 +- .../assets/configs/test_primaite_session.yaml | 2 +- .../configs/train_only_primaite_session.yaml | 2 +- tests/conftest.py | 5 +- .../_game/_agent/test_probabilistic_agent.py | 84 +++++++ 16 files changed, 386 insertions(+), 87 deletions(-) create mode 100644 tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index a0e9667e..6813161d 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -33,7 +33,12 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenUC2Agent + type: probabilistic_agent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 observation_space: type: UC2GreenObservation action_space: @@ -69,15 +74,14 @@ agents: reward_components: - type: DUMMY + - ref: client_1_green_user + team: GREEN + type: probabilistic_agent agent_settings: action_probabilities: 0: 0.3 1: 0.6 2: 0.1 - - - ref: client_1_green_user - team: GREEN - type: GreenUC2Agent observation_space: type: UC2GreenObservation action_space: @@ -113,11 +117,6 @@ agents: reward_components: - type: DUMMY - agent_settings: - action_probabilities: - 0: 0.3 - 1: 0.6 - 2: 0.1 diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 93019c9d..df6130d1 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -27,7 +27,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 1793d420..18cb6262 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -607,7 +607,6 @@ class ActionManager: def __init__( self, - game: "PrimaiteGame", # reference to game for information lookup 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 @@ -618,7 +617,7 @@ class ActionManager: 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_address_list: Optional[List[str]] = None, # to allow us to map an index to an ip address. + ip_address_list: List[str] = [], # to allow us to map an index to an ip address. act_map: Optional[Dict[int, Dict]] = None, # allows restricting set of possible actions ) -> None: """Init method for ActionManager. @@ -649,7 +648,6 @@ 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.game: "PrimaiteGame" = game 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]] = [] @@ -707,25 +705,7 @@ class ActionManager: self.protocols: List[str] = protocols self.ports: List[str] = ports - self.ip_address_list: List[str] - - # If the user has provided a list of IP addresses, use that. Otherwise, generate a list of IP addresses from - # the nodes in the simulation. - # TODO: refactor. Options: - # 1: This should be pulled out into it's own function for clarity - # 2: The simulation itself should be able to provide a list of IP addresses with its API, rather than having to - # go through the nodes here. - if ip_address_list is not None: - self.ip_address_list = ip_address_list - else: - self.ip_address_list = [] - for node_name in self.node_names: - node_obj = self.game.simulation.network.get_node_by_hostname(node_name) - if node_obj is None: - continue - network_interfaces = node_obj.network_interfaces - for nic_uuid, nic_obj in network_interfaces.items(): - self.ip_address_list.append(nic_obj.ip_address) + self.ip_address_list: List[str] = ip_address_list # action_args are settings which are applied to the action space as a whole. global_action_args = { @@ -958,6 +938,12 @@ class ActionManager: :return: The constructed ActionManager. :rtype: ActionManager """ + # If the user has provided a list of IP addresses, use that. Otherwise, generate a list of IP addresses from + # the nodes in the simulation. + # TODO: refactor. Options: + # 1: This should be pulled out into it's own function for clarity + # 2: The simulation itself should be able to provide a list of IP addresses with its API, rather than having to + # go through the nodes here. ip_address_order = cfg["options"].pop("ip_address_order", {}) ip_address_list = [] for entry in ip_address_order: @@ -967,13 +953,22 @@ class ActionManager: ip_address = node_obj.network_interface[nic_num].ip_address ip_address_list.append(ip_address) + if not ip_address_list: + node_names = [n["node_name"] for n in cfg.get("nodes", {})] + for node_name in node_names: + node_obj = game.simulation.network.get_node_by_hostname(node_name) + if node_obj is None: + continue + network_interfaces = node_obj.network_interfaces + for nic_uuid, nic_obj in network_interfaces.items(): + ip_address_list.append(nic_obj.ip_address) + obj = cls( - game=game, actions=cfg["action_list"], **cfg["options"], protocols=game.options.protocols, ports=game.options.ports, - ip_address_list=ip_address_list or None, + ip_address_list=ip_address_list, act_map=cfg.get("action_map"), ) diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/data_manipulation_bot.py index 126c55ec..b5de9a5a 100644 --- a/src/primaite/game/agent/data_manipulation_bot.py +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -1,5 +1,5 @@ import random -from typing import Dict, Tuple +from typing import Dict, Optional, Tuple from gymnasium.core import ObsType @@ -26,7 +26,7 @@ class DataManipulationAgent(AbstractScriptedAgent): ) self.next_execution_timestep = timestep + random_timestep_increment - def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: """Randomly sample an action from the action space. :param obs: _description_ @@ -36,12 +36,10 @@ class DataManipulationAgent(AbstractScriptedAgent): :return: _description_ :rtype: Tuple[str, Dict] """ - current_timestep = self.action_manager.game.step_counter - - if current_timestep < self.next_execution_timestep: + if timestep < self.next_execution_timestep: return "DONOTHING", {"dummy": 0} - self._set_next_execution_timestep(current_timestep + self.agent_settings.start_settings.frequency) + self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 276715f7..4f434bad 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -112,7 +112,7 @@ class AbstractAgent(ABC): return self.reward_function.update(state) @abstractmethod - def get_action(self, obs: ObsType, reward: float = 0.0) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: """ Return an action to be taken in the environment. @@ -122,6 +122,8 @@ class AbstractAgent(ABC): :type obs: ObsType :param reward: Reward from the previous action, defaults to None TODO: should this parameter even be accepted? :type reward: float, optional + :param timestep: The current timestep in the simulation, used for non-RL agents. Optional + :type timestep: int :return: Action to be taken in the environment. :rtype: Tuple[str, Dict] """ @@ -144,13 +146,13 @@ class AbstractAgent(ABC): class AbstractScriptedAgent(AbstractAgent): """Base class for actors which generate their own behaviour.""" - ... + pass class RandomAgent(AbstractScriptedAgent): """Agent that ignores its observation and acts completely at random.""" - def get_action(self, obs: ObsType, reward: float = 0.0) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: """Randomly sample an action from the action space. :param obs: _description_ @@ -183,7 +185,7 @@ class ProxyAgent(AbstractAgent): self.most_recent_action: ActType self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False - def get_action(self, obs: ObsType, reward: float = 0.0) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: """ Return the agent's most recent action, formatted in CAOS format. diff --git a/src/primaite/game/agent/scripted_agents.py b/src/primaite/game/agent/scripted_agents.py index a88e563d..28d94062 100644 --- a/src/primaite/game/agent/scripted_agents.py +++ b/src/primaite/game/agent/scripted_agents.py @@ -11,30 +11,39 @@ from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction -class GreenUC2Agent(AbstractScriptedAgent): - """Scripted agent which attempts to send web requests to a target node.""" +class ProbabilisticAgent(AbstractScriptedAgent): + """Scripted agent which randomly samples its action space with prescribed probabilities for each action.""" + + class Settings(pydantic.BaseModel): + """Config schema for Probabilistic agent settings.""" - class GreenUC2AgentSettings(pydantic.BaseModel): model_config = pydantic.ConfigDict(extra="forbid") + """Strict validation.""" action_probabilities: Dict[int, float] """Probability to perform each action in the action map. The sum of probabilities should sum to 1.""" random_seed: Optional[int] = None + """Random seed. If set, each episode the agent will choose the same random sequence of actions.""" + # 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 def probabilities_sum_to_one(cls, v: Dict[int, float]) -> Dict[int, float]: + """Make sure the probabilities sum to 1.""" if not abs(sum(v.values()) - 1) < 1e-6: - raise ValueError(f"Green action probabilities must sum to 1") + raise ValueError("Green action probabilities must sum to 1") return v @pydantic.field_validator("action_probabilities", mode="after") @classmethod def action_map_covered_correctly(cls, v: Dict[int, float]) -> Dict[int, float]: + """Ensure that the keys of the probability dictionary cover all integers from 0 to N.""" if not all((i in v) for i in range(len(v))): raise ValueError( "Green action probabilities must be defined as a mapping where the keys are consecutive integers " "from 0 to N." ) + return v def __init__( self, @@ -52,23 +61,27 @@ class GreenUC2Agent(AbstractScriptedAgent): # If seed not specified, set it to None so that numpy chooses a random one. settings.setdefault("random_seed") - self.settings = GreenUC2Agent.GreenUC2AgentSettings(settings) + self.settings = ProbabilisticAgent.Settings(**settings) self.rng = np.random.default_rng(self.settings.random_seed) # convert probabilities from - self.probabilities = np.array[self.settings.action_probabilities.values()] + self.probabilities = np.asarray(list(self.settings.action_probabilities.values())) super().__init__(agent_name, action_space, observation_space, reward_function) - def get_action(self, obs: ObsType, reward: float = 0) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: + """ + Choose a random action from the action space. + + The probability of each action is given by the corresponding index in ``self.probabilities``. + + :param obs: Current observation of the simulation + :type obs: ObsType + :param reward: Reward for the last step, not used for scripted agents, defaults to 0 + :type reward: float, optional + :return: Action to be taken in CAOS format. + :rtype: Tuple[str, Dict] + """ choice = self.rng.choice(len(self.action_manager.action_map), p=self.probabilities) return self.action_manager.get_action(choice) - - raise NotImplementedError - - -class RedDatabaseCorruptingAgent(AbstractScriptedAgent): - """Scripted agent which attempts to corrupt the database of the target node.""" - - raise NotImplementedError diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index a9d564ba..b44abe16 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -7,10 +7,10 @@ from pydantic import BaseModel, ConfigDict from primaite import getLogger from primaite.game.agent.actions import ActionManager from primaite.game.agent.data_manipulation_bot import DataManipulationAgent -from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent, RandomAgent +from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction -from primaite.game.agent.scripted_agents import GreenUC2Agent +from primaite.game.agent.scripted_agents import ProbabilisticAgent from primaite.session.io import SessionIO, SessionIOSettings from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -165,7 +165,7 @@ class PrimaiteGame: for agent in self.agents: obs = agent.observation_manager.current_observation rew = agent.reward_function.current_reward - action_choice, options = agent.get_action(obs, rew) + action_choice, options = agent.get_action(obs, rew, timestep=self.step_counter) agent_actions[agent.agent_name] = (action_choice, options) request = agent.format_request(action_choice, options) self.simulation.apply_request(request) @@ -393,14 +393,15 @@ class PrimaiteGame: agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) # CREATE AGENT - if agent_type == "GreenUC2Agent": + if agent_type == "probabilistic_agent": # TODO: implement non-random agents and fix this parsing - new_agent = GreenUC2Agent( + 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, - agent_settings=agent_settings, + settings=settings, ) game.agents.append(new_agent) elif agent_type == "ProxyAgent": diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index c4fe4c9a..fa4a28a4 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -334,7 +334,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [] }, @@ -346,7 +346,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "tags": [] }, @@ -371,11 +371,150 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-02-27 09:43:39,312::WARNING::primaite.game.game::275::service type not found DatabaseClient\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resetting environment, episode 0, avg. reward: 0.0\n", + "env created successfully\n", + "{'ACL': {1: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 2: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 3: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 4: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 5: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 6: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 7: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 8: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 9: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 10: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0}},\n", + " 'ICS': 0,\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 1}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", + " 'health_status': 1}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1},\n", + " 2: {'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}}\n" + ] + } + ], "source": [ "# create the env\n", "with open(example_config_path(), 'r') as f:\n", @@ -403,7 +542,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -421,15 +560,57 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 211, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 212, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 213, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 214, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 215, Red action: DO NOTHING, Blue reward:-0.42\n", + "step: 216, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 217, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 218, Red action: DO NOTHING, Blue reward:-0.42\n", + "step: 219, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 220, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 221, Red action: ATTACK from client 2, Blue reward:-0.32\n", + "step: 222, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 223, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 224, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 225, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 226, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 227, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 228, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 229, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 230, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 231, Red action: DO NOTHING, Blue reward:-0.42\n", + "step: 232, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 233, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 234, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 235, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 236, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 237, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 238, Red action: ATTACK from client 2, Blue reward:-0.32\n", + "step: 239, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 240, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 241, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 242, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 243, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 244, Red action: DO NOTHING, Blue reward:-0.32\n", + "step: 245, Red action: DO NOTHING, Blue reward:-0.32\n" + ] + } + ], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", - " print(f\"step: {env.game.step_counter}, Red action: {friendly_output_red_action(info)}, Blue reward:{reward}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {friendly_output_red_action(info)}, Blue reward:{reward:.2f}\" )" ] }, { @@ -509,7 +690,7 @@ "\n", "The reward will increase slightly as soon as the file finishes restoring. Then, the reward will increase to 1 when both green agents make successful requests.\n", "\n", - "Run the following cell until the green action is `NODE_APPLICATION_EXECUTE`, then the reward should become 1. 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 become 1. If you run it enough times, another red attack will happen and the reward will drop again." ] }, { @@ -523,8 +704,8 @@ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'][0]}\" )\n", - "print(f\"Green action: {info['agent_actions']['client_2_green_user'][0]}\" )\n", - "print(f\"Green action: {info['agent_actions']['client_1_green_user'][0]}\" )\n", + "print(f\"Green action: {info['agent_actions']['client_2_green_user']}\" )\n", + "print(f\"Green action: {info['agent_actions']['client_1_green_user']}\" )\n", "print(f\"Blue reward:{reward}\" )" ] }, @@ -582,6 +763,33 @@ "obs['ACL']" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "net = env.game.simulation.network" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dbc = net.get_node_by_hostname('client_1').software_manager.software.get('DatabaseClient')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dbc._query_success_tracker" + ] + }, { "cell_type": "code", "execution_count": null, @@ -606,7 +814,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 5bdc3273..892e6af7 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -21,7 +21,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index d1cec079..ad2ea787 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -33,7 +33,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index 8361e318..9b668686 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -25,7 +25,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 87bd9d1c..5a7d8366 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -31,7 +31,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 76190a64..42dd27fb 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -29,7 +29,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/train_only_primaite_session.yaml b/tests/assets/configs/train_only_primaite_session.yaml index 5d004c7e..8a4a1178 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -25,7 +25,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/conftest.py b/tests/conftest.py index 5084c339..2add835f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK from pathlib import Path -from typing import Any, Dict, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union import pytest import yaml @@ -309,7 +309,7 @@ class ControlledAgent(AbstractAgent): ) self.most_recent_action: Tuple[str, Dict] - def get_action(self, obs: None, reward: float = 0.0) -> Tuple[str, Dict]: + def get_action(self, obs: None, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: """Return the agent's most recent action, formatted in CAOS format.""" return self.most_recent_action @@ -478,7 +478,6 @@ def game_and_agent(): ] action_space = ActionManager( - game=game, actions=actions, # ALL POSSIBLE ACTIONS nodes=[ { diff --git a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py new file mode 100644 index 00000000..f0b37cac --- /dev/null +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -0,0 +1,84 @@ +from primaite.game.agent.actions import ActionManager +from primaite.game.agent.observations import ICSObservation, ObservationManager +from primaite.game.agent.rewards import RewardFunction +from primaite.game.agent.scripted_agents import ProbabilisticAgent + + +def test_probabilistic_agent(): + """ + Check that the probabilistic agent selects actions with approximately the right probabilities. + + Using a binomial probability calculator (https://www.wolframalpha.com/input/?i=binomial+distribution+calculator), + we can generate some lower and upper bounds of how many times we expect the agent to take each action. These values + were chosen to guarantee a less than 1 in a million chance of the test failing due to unlucky random number + generation. + """ + N_TRIALS = 10_000 + P_DO_NOTHING = 0.1 + 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 + + action_space = ActionManager( + actions=[ + {"type": "DONOTHING"}, + {"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": "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}}, + }, + ) + observation_space = ObservationManager(ICSObservation()) + 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}, + "random_seed": 120, + }, + ) + + do_nothing_count = 0 + node_application_execute_count = 0 + node_file_delete_count = 0 + for _ in range(N_TRIALS): + a = pa.get_action(0, timestep=0) + if a == ("DONOTHING", {}): + do_nothing_count += 1 + elif a == ("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}): + node_application_execute_count += 1 + elif a == ("NODE_FILE_DELETE", {"node_id": 0, "folder_id": 0, "file_id": 0}): + node_file_delete_count += 1 + else: + 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 From d55b6a5b48bf0faa6aeed6bd5ee94c65ab90912b Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Wed, 28 Feb 2024 12:03:58 +0000 Subject: [PATCH 041/128] #2238 - Fixed the observations issue causing tests to fail --- src/primaite/game/agent/observations.py | 7 +++++-- src/primaite/simulator/network/hardware/base.py | 2 ++ .../simulator/network/hardware/nodes/host/host_node.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index 7ccc3f11..82e11fe0 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -351,6 +351,8 @@ class NicObservation(AbstractObservation): def default_observation(self) -> Dict: """The default NIC observation dict.""" data = {"nic_status": 0} + if CAPTURE_NMNE: + data.update({"nmne": {"inbound": 0, "outbound": 0}}) return data @@ -404,8 +406,9 @@ class NicObservation(AbstractObservation): if nic_state is NOT_PRESENT_IN_STATE: return self.default_observation else: - obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2, "nmne": {}} - if CAPTURE_NMNE and nic_state.get("nmne"): + obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2} + if CAPTURE_NMNE: + obs_dict.update({"nmne": {}}) direction_dict = nic_state["nmne"].get("direction", {}) inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) inbound_count = inbound_keywords.get("*", 0) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index b22bea25..35c90d05 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -123,6 +123,8 @@ class NetworkInterface(SimComponent, ABC): "enabled": self.enabled, } ) + if CAPTURE_NMNE: + state.update({"nmne": self.nmne}) return state def reset_component_for_episode(self, episode: int): 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 8e104924..b48950b7 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -205,7 +205,7 @@ class NIC(IPWiredNetworkInterface): state = super().describe_state() # Update the state with NIC-specific information - state.update({"wake_on_lan": self.wake_on_lan, "nmne": self.nmne}) + state.update({"wake_on_lan": self.wake_on_lan}) return state From 63ea5478ab4fc39ffde3359983c332098fd318b6 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Wed, 28 Feb 2024 13:56:19 +0000 Subject: [PATCH 042/128] #2238 - Updated uc2_demo.ipynb to explain the NMNE in observation space --- src/primaite/notebooks/uc2_demo.ipynb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index c4fe4c9a..b1e12370 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -130,6 +130,9 @@ " - NETWORK_INTERFACES\n", " - \n", " - nic_status\n", + " - nmne\n", + " - inbound\n", + " - outbound\n", " - operating_status\n", "- LINKS\n", " - \n", @@ -220,6 +223,14 @@ "|1|ENABLED|\n", "|2|DISABLED|\n", "\n", + "NMNE (number of malicious network events) means, for inbound or outbound traffic, means:\n", + "|value|NMNEs|\n", + "|--|--|\n", + "|0|None|\n", + "|1|1 - 5|\n", + "|2|6 - 10|\n", + "|3|More than 10|\n", + "\n", "Link load has the following meaning:\n", "|load|percent utilisation|\n", "|--|--|\n", From 6d43c61058f4d342b2ed1f62bf601cb9bd824729 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Wed, 28 Feb 2024 15:08:00 +0000 Subject: [PATCH 043/128] #2257: apply PR suggestions --- .../common/common_host_node_attributes.rst | 2 +- .../simulation/nodes/firewall.rst | 38 ++++++++++++++++++- .../applications/data_manipulation_bot.rst | 4 +- .../system/list_of_applications.rst | 4 ++ .../system/list_of_services.rst | 4 ++ .../system/services/database_service.rst | 2 +- .../system/services/ftp_client.rst | 3 +- .../system/services/ftp_server.rst | 3 +- src/primaite/game/agent/rewards.py | 6 +-- .../hardware/nodes/network/firewall.py | 38 +++++++++---------- .../network/hardware/nodes/network/router.py | 12 +++--- 11 files changed, 81 insertions(+), 35 deletions(-) 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 a95f98d4..b9f173c6 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 @@ -24,7 +24,7 @@ The IP address that the |NODE| will use as the default gateway. Typically, this Optional. Default value is ``None`` -The IP address of the node which holds an instance of the DNS server. Some applications may use a domain name e.g. the WebBrowser (TODO: WebBrowser page) +The IP address of the node which holds an instance of the :ref:`DNSServer`. Some applications may use a domain name e.g. the :ref:`WebBrowser` .. include:: ../software/applications.rst diff --git a/docs/source/configuration/simulation/nodes/firewall.rst b/docs/source/configuration/simulation/nodes/firewall.rst index 3c1fce0a..47db4001 100644 --- a/docs/source/configuration/simulation/nodes/firewall.rst +++ b/docs/source/configuration/simulation/nodes/firewall.rst @@ -7,7 +7,7 @@ ``firewall`` ============ -A basic representation of a network router within the simulation. +A basic representation of a network firewall within the simulation. The firewall is similar to how :ref:`Router ` works, with the difference being how firewall has specific ACL rules for inbound and outbound traffic as well as firewall being limited to 3 ports. @@ -133,6 +133,10 @@ example: ... acl: internal_inbound_acl: + 21: # position 21 on ACL list + action: PERMIT # allow packets that + src_port: POSTGRES_SERVER # are emitted from the POSTGRES_SERVER port + dst_port: POSTGRES_SERVER # are going towards an POSTGRES_SERVER port 22: # position 22 on ACL list action: PERMIT # allow packets that src_port: ARP # are emitted from the ARP port @@ -155,6 +159,10 @@ example: ... acl: internal_outbound_acl: + 21: # position 21 on ACL list + action: PERMIT # allow packets that + src_port: POSTGRES_SERVER # are emitted from the POSTGRES_SERVER port + dst_port: POSTGRES_SERVER # are going towards an POSTGRES_SERVER port 22: # position 22 on ACL list action: PERMIT # allow packets that src_port: ARP # are emitted from the ARP port @@ -178,6 +186,18 @@ example: ... acl: dmz_inbound_acl: + 19: # position 19 on ACL list + action: PERMIT # allow packets that + src_port: POSTGRES_SERVER # are emitted from the POSTGRES_SERVER port + dst_port: POSTGRES_SERVER # are going towards an POSTGRES_SERVER port + 20: # position 20 on ACL list + action: PERMIT # allow packets that + src_port: HTTP # are emitted from the HTTP port + dst_port: HTTP # are going towards an HTTP port + 21: # position 21 on ACL list + action: PERMIT # allow packets that + src_port: HTTPS # are emitted from the HTTPS port + dst_port: HTTPS # are going towards an HTTPS port 22: # position 22 on ACL list action: PERMIT # allow packets that src_port: ARP # are emitted from the ARP port @@ -200,6 +220,18 @@ example: ... acl: dmz_outbound_acl: + 19: # position 19 on ACL list + action: PERMIT # allow packets that + src_port: POSTGRES_SERVER # are emitted from the POSTGRES_SERVER port + dst_port: POSTGRES_SERVER # are going towards an POSTGRES_SERVER port + 20: # position 20 on ACL list + action: PERMIT # allow packets that + src_port: HTTP # are emitted from the HTTP port + dst_port: HTTP # are going towards an HTTP port + 21: # position 21 on ACL list + action: PERMIT # allow packets that + src_port: HTTPS # are emitted from the HTTPS port + dst_port: HTTPS # are going towards an HTTPS port 22: # position 22 on ACL list action: PERMIT # allow packets that src_port: ARP # are emitted from the ARP port @@ -226,6 +258,10 @@ example: ... acl: external_inbound_acl: + 21: # position 19 on ACL list + action: DENY # deny packets that + src_port: POSTGRES_SERVER # are emitted from the POSTGRES_SERVER port + dst_port: POSTGRES_SERVER # are going towards an POSTGRES_SERVER port 22: # position 22 on ACL list action: PERMIT # allow packets that src_port: ARP # are emitted from the ARP port 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 d0e89f2e..d67e82d4 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -99,7 +99,7 @@ If not using the data manipulation bot manually, it needs to be used with a data type: UC2RedObservation options: nodes: - - node_ref: client_1 + - node_name: client_1 observations: - logon_status - operating_status @@ -116,7 +116,7 @@ If not using the data manipulation bot manually, it needs to be used with a data - type: NODE_APPLICATION_EXECUTE options: nodes: - - node_ref: client_1 + - node_name: client_1 applications: - application_ref: data_manipulation_bot max_folders_per_node: 1 diff --git a/docs/source/simulation_components/system/list_of_applications.rst b/docs/source/simulation_components/system/list_of_applications.rst index 0ba0c45c..8f792e4c 100644 --- a/docs/source/simulation_components/system/list_of_applications.rst +++ b/docs/source/simulation_components/system/list_of_applications.rst @@ -1,3 +1,7 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + .. toctree:: :maxdepth: 1 :glob: diff --git a/docs/source/simulation_components/system/list_of_services.rst b/docs/source/simulation_components/system/list_of_services.rst index e24b26dc..9f1c9fe2 100644 --- a/docs/source/simulation_components/system/list_of_services.rst +++ b/docs/source/simulation_components/system/list_of_services.rst @@ -1,3 +1,7 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + .. toctree:: :maxdepth: 1 :glob: diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index 30d6b3ba..2c962c0a 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -12,7 +12,7 @@ The ``DatabaseService`` provides a SQL database server simulation by extending t Key capabilities ================ -- Creates a database file in the ``Node`` 's ``FileSystem`` upon creation. +- Creates a database file in the ``FileSystem`` of the ``Node`` (which the ``DatabaseService`` is installed on) upon creation. - Handles connecting clients by maintaining a dictionary of connections mapped to session IDs. - Authenticates connections using a configurable password. - Simulates ``SELECT``, ``DELETE`` and ``INSERT`` SQL queries. diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index 82b85770..604ef8e8 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -20,6 +20,7 @@ Key features - RETR: retrieves a file from the FTP server - QUIT: disconnect from server - Leverages the Service base class for install/uninstall, status tracking, etc. +- :ref:`FTPClient` and ``FTPServer`` utilise port 21 (FTP) throughout all file transfer / request Usage ===== @@ -52,7 +53,7 @@ Python hostname="server", ip_address="192.168.2.2", subnet_mask="255.255.255.0", - default_gateway="192.168.1.1Ó", + default_gateway="192.168.1.10", start_up_duration=0, ) server.power_on() diff --git a/docs/source/simulation_components/system/services/ftp_server.rst b/docs/source/simulation_components/system/services/ftp_server.rst index d807a14f..fb57a762 100644 --- a/docs/source/simulation_components/system/services/ftp_server.rst +++ b/docs/source/simulation_components/system/services/ftp_server.rst @@ -17,12 +17,13 @@ Key capabilities - STOR: stores a file from client to server - RETR: retrieves a file from the FTP server - Leverages the Service base class for install/uninstall, status tracking, etc. +- :ref:`FTPClient` and ``FTPServer`` utilise port 21 (FTP) throughout all file transfer / request Usage ===== - Install on a Node via the ``SoftwareManager`` to start the FTP server service. -- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) +- Service runs on FTP (command) port 21 by default Implementation ============== diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 27c39b65..ba6d1fa3 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -13,7 +13,7 @@ the structure: - type: DATABASE_FILE_INTEGRITY weight: 0.5 options: - node_ref: database_server + node_name: database_server folder_name: database file_name: database.db @@ -21,7 +21,7 @@ the structure: - type: WEB_SERVER_404_PENALTY weight: 0.5 options: - node_ref: web_server + node_name: web_server service_ref: web_server_database_client ``` """ @@ -184,7 +184,7 @@ class WebServer404Penalty(AbstractReward): service_name = config.get("service_name") if not (node_hostname and service_name): msg = ( - f"{cls.__name__} could not be initialised from config because node_ref and service_ref were not " + f"{cls.__name__} could not be initialised from config because node_name and service_ref were not " "found in reward config." ) _LOGGER.warning(msg) diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index 903ce3f3..ce98cec4 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -497,66 +497,66 @@ class Firewall(Router): @classmethod def from_config(cls, cfg: dict) -> "Firewall": """Create a firewall based on a config dict.""" - new = Firewall(hostname=cfg["hostname"], operating_state=NodeOperatingState.ON) + firewall = Firewall(hostname=cfg["hostname"], operating_state=NodeOperatingState.ON) if "ports" in cfg: internal_port = cfg["ports"]["internal_port"] external_port = cfg["ports"]["external_port"] dmz_port = cfg["ports"]["dmz_port"] # configure internal port - new.configure_internal_port( + firewall.configure_internal_port( ip_address=IPV4Address(internal_port.get("ip_address")), subnet_mask=IPV4Address(internal_port.get("subnet_mask", "255.255.255.0")), ) # configure external port - new.configure_external_port( + firewall.configure_external_port( ip_address=IPV4Address(external_port.get("ip_address")), subnet_mask=IPV4Address(external_port.get("subnet_mask", "255.255.255.0")), ) # configure dmz port - new.configure_dmz_port( + firewall.configure_dmz_port( ip_address=IPV4Address(dmz_port.get("ip_address")), subnet_mask=IPV4Address(dmz_port.get("subnet_mask", "255.255.255.0")), ) if "acl" in cfg: # acl rules for internal_inbound_acl if cfg["acl"]["internal_inbound_acl"]: - new.internal_inbound_acl.max_acl_rules - new.internal_inbound_acl._default_config = cfg["acl"]["internal_inbound_acl"] - new.internal_inbound_acl._reset_rules_to_default() + firewall.internal_inbound_acl.max_acl_rules + firewall.internal_inbound_acl._default_config = cfg["acl"]["internal_inbound_acl"] + firewall.internal_inbound_acl._reset_rules_to_default() # acl rules for internal_outbound_acl if cfg["acl"]["internal_outbound_acl"]: - new.internal_outbound_acl._default_config = cfg["acl"]["internal_outbound_acl"] - new.internal_outbound_acl._reset_rules_to_default() + firewall.internal_outbound_acl._default_config = cfg["acl"]["internal_outbound_acl"] + firewall.internal_outbound_acl._reset_rules_to_default() # acl rules for dmz_inbound_acl if cfg["acl"]["dmz_inbound_acl"]: - new.dmz_inbound_acl._default_config = cfg["acl"]["dmz_inbound_acl"] - new.dmz_inbound_acl._reset_rules_to_default() + firewall.dmz_inbound_acl._default_config = cfg["acl"]["dmz_inbound_acl"] + firewall.dmz_inbound_acl._reset_rules_to_default() # acl rules for dmz_outbound_acl if cfg["acl"]["dmz_outbound_acl"]: - new.dmz_outbound_acl._default_config = cfg["acl"]["dmz_outbound_acl"] - new.dmz_outbound_acl._reset_rules_to_default() + firewall.dmz_outbound_acl._default_config = cfg["acl"]["dmz_outbound_acl"] + firewall.dmz_outbound_acl._reset_rules_to_default() # acl rules for external_inbound_acl if cfg["acl"]["external_inbound_acl"]: - new.external_inbound_acl._default_config = cfg["acl"]["external_inbound_acl"] - new.external_inbound_acl._reset_rules_to_default() + firewall.external_inbound_acl._default_config = cfg["acl"]["external_inbound_acl"] + firewall.external_inbound_acl._reset_rules_to_default() # acl rules for external_outbound_acl if cfg["acl"]["external_outbound_acl"]: - new.external_outbound_acl._default_config = cfg["acl"]["external_outbound_acl"] - new.external_outbound_acl._reset_rules_to_default() + firewall.external_outbound_acl._default_config = cfg["acl"]["external_outbound_acl"] + firewall.external_outbound_acl._reset_rules_to_default() if "routes" in cfg: for route in cfg.get("routes"): - new.route_table.add_route( + firewall.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric", 0)), ) - return new + return firewall diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index b3d7f7bf..a9e12401 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1480,27 +1480,27 @@ class Router(NetworkNode): :return: Configured router. :rtype: Router """ - new = Router( + router = Router( hostname=cfg["hostname"], num_ports=int(cfg.get("num_ports", "5")), operating_state=NodeOperatingState.ON, ) if "ports" in cfg: for port_num, port_cfg in cfg["ports"].items(): - new.configure_port( + router.configure_port( port=port_num, ip_address=port_cfg["ip_address"], subnet_mask=IPv4Address(port_cfg.get("subnet_mask", "255.255.255.0")), ) if "acl" in cfg: - new.acl._default_config = cfg["acl"] # save the config to allow resetting - new.acl._reset_rules_to_default() # read the config and apply rules + router.acl._default_config = cfg["acl"] # save the config to allow resetting + router.acl._reset_rules_to_default() # read the config and apply rules if "routes" in cfg: for route in cfg.get("routes"): - new.route_table.add_route( + router.route_table.add_route( address=IPv4Address(route.get("address")), subnet_mask=IPv4Address(route.get("subnet_mask", "255.255.255.0")), next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")), metric=float(route.get("metric", 0)), ) - return new + return router From 8730330f73bf5b38ade30bfc18c23ee3c5523367 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 29 Feb 2024 10:14:31 +0000 Subject: [PATCH 044/128] Apply PR suggestions --- src/primaite/config/_package_data/example_config.yaml | 2 +- .../_package_data/example_config_2_rl_agents.yaml | 2 +- src/primaite/game/game.py | 10 ++++++---- .../simulator/network/hardware/nodes/network/router.py | 1 - .../simulator/system/services/web_server/web_server.py | 2 +- .../environments/test_sb3_environment.py | 1 - 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index a32696c7..ebee4980 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -14,7 +14,7 @@ io_settings: save_checkpoints: true checkpoint_interval: 5 save_step_metadata: false - save_pcap_logs: true + save_pcap_logs: false save_sys_logs: true diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index c1e077be..992c3a1a 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -16,7 +16,7 @@ io_settings: save_checkpoints: true checkpoint_interval: 5 save_step_metadata: false - save_pcap_logs: true + save_pcap_logs: false save_sys_logs: true diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8edf70ea..baa84d1d 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -133,7 +133,7 @@ class PrimaiteGame: self.update_agents(sim_state) # Apply all actions to simulation as requests - agent_actions = self.apply_agent_actions() # noqa + self.apply_agent_actions() # Advance timestep self.advance_timestep() @@ -144,7 +144,7 @@ class PrimaiteGame: def update_agents(self, state: Dict) -> None: """Update agents' observations and rewards based on the current state.""" - for name, agent in self.agents.items(): + for _, agent in self.agents.items(): agent.update_observation(state) agent.update_reward(state) agent.reward_function.total_reward += agent.reward_function.current_reward @@ -158,7 +158,7 @@ class PrimaiteGame: """ agent_actions = {} - for name, agent in self.agents.items(): + for _, agent in self.agents.items(): obs = agent.observation_manager.current_observation rew = agent.reward_function.current_reward action_choice, options = agent.get_action(obs, rew) @@ -414,7 +414,9 @@ class PrimaiteGame: agent_settings=agent_settings, ) else: - _LOGGER.warning(f"agent type {agent_type} not found") + 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 return game diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 52f38eb6..aa6eec3a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1076,7 +1076,6 @@ class Router(NetworkNode): :param episode: The episode number for which the router is being reset. """ self.software_manager.arp.clear() - # self.acl.reset_component_for_episode(episode) for i, _ in self.network_interface.items(): self.enable_port(i) 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 ce29a2f9..5e7591e9 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -118,7 +118,7 @@ class WebServer(Service): self.set_health_state(SoftwareHealthState.COMPROMISED) return response - except Exception: # TODO: refactor this. Likely to cause silent bugs. + except Exception: # TODO: refactor this. Likely to cause silent bugs. (ADO ticket #2345 ) # something went wrong on the server response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR return response diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index dc5d10e9..c48ddbc9 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -11,7 +11,6 @@ from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv -# @pytest.mark.skip(reason="no way of currently testing this") def test_sb3_compatibility(): """Test that the Gymnasium environment can be used with an SB3 agent.""" with open(example_config_path(), "r") as f: From 9a4587155b4c3eab5b50f272a1d214fe9e7ed878 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 29 Feb 2024 11:07:21 +0000 Subject: [PATCH 045/128] #2257: specifically stating that enpoint refs are node hostnames + remove TODO --- docs/source/configuration/simulation.rst | 4 ++-- .../simulation_components/system/services/ftp_client.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/simulation.rst b/docs/source/configuration/simulation.rst index 89c1669b..e2fa5476 100644 --- a/docs/source/configuration/simulation.rst +++ b/docs/source/configuration/simulation.rst @@ -73,7 +73,7 @@ The human readable name for the link. Not used in code, however is useful for a ``endpoint_a_ref`` ^^^^^^^^^^^^^^^^^^ -The name of the node which must be connected. +The ``hostname`` of the node which must be connected. ``endpoint_a_port`` ^^^^^^^^^^^^^^^^^^^ @@ -84,7 +84,7 @@ This accepts an integer value e.g. if port 1 is to be connected, the configurati ``endpoint_b_ref`` ^^^^^^^^^^^^^^^^^^ -The name of the node which must be connected. +The ``hostname`` of the node which must be connected. ``endpoint_b_port`` ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/simulation_components/system/services/ftp_client.rst b/docs/source/simulation_components/system/services/ftp_client.rst index 604ef8e8..259a626d 100644 --- a/docs/source/simulation_components/system/services/ftp_client.rst +++ b/docs/source/simulation_components/system/services/ftp_client.rst @@ -26,7 +26,7 @@ Usage ===== - Install on a Node via the ``SoftwareManager`` to start the FTP client service. -- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) +- Service runs on FTP (command) port 21 by default - Execute sending a file to the FTP server with ``send_file`` - Execute retrieving a file from the FTP server with ``request_file`` From cf0674ce22198a3519023f6077ccbc1f98133b2f Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Thu, 29 Feb 2024 13:00:27 +0000 Subject: [PATCH 046/128] #2326 - Network Interface port name/num fixed so that it carries through to sys log and PCAP outputs. --- CHANGELOG.md | 2 +- src/primaite/simulator/network/airspace.py | 4 +++- .../simulator/network/hardware/base.py | 15 ++++++++++---- .../wireless/wireless_access_point.py | 2 +- .../wireless/wireless_nic.py | 2 +- .../network/hardware/nodes/host/host_node.py | 2 +- .../hardware/nodes/network/firewall.py | 12 ++++++++++- .../network/hardware/nodes/network/router.py | 2 +- .../hardware/nodes/network/wireless_router.py | 5 ++++- .../simulator/system/core/packet_capture.py | 20 +++++++++++++++---- 10 files changed, 50 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcff5934..55202de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,7 +107,7 @@ SessionManager. ### Fixed - Addressed network transmission issues that previously allowed ARP requests to be incorrectly routed and repeated across different subnets. This fix ensures ARP requests are correctly managed and confined to their appropriate network segments. - Resolved problems in `Node` and its subclasses where the default gateway configuration was not properly utilized for communications across different subnets. This correction ensures that nodes effectively use their configured default gateways for outbound communications to other network segments, thereby enhancing the network's routing functionality and reliability. - +- Network Interface Port name/num being set properly for sys log and PCAP output. ## [2.0.0] - 2023-07-26 diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index d264f751..5ceedc8e 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -168,7 +168,9 @@ class WirelessNetworkInterface(NetworkInterface, ABC): self.enabled = True self._connected_node.sys_log.info(f"Network Interface {self} enabled") - self.pcap = PacketCapture(hostname=self._connected_node.hostname, interface_num=self.port_num) + self.pcap = PacketCapture( + hostname=self._connected_node.hostname, port_num=self.port_num, port_name=self.port_name + ) AIR_SPACE.add_wireless_interface(self) def disable(self): diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ff79f314..991913dd 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -94,6 +94,9 @@ class NetworkInterface(SimComponent, ABC): port_num: Optional[int] = None "The port number assigned to this interface on the connected node." + port_name: Optional[str] = None + "The port name assigned to this interface on the connected node." + pcap: Optional[PacketCapture] = None "A PacketCapture instance for capturing and analysing packets passing through this interface." @@ -248,7 +251,7 @@ class NetworkInterface(SimComponent, ABC): :return: A string combining the port number and the mac address """ - return f"Port {self.port_num}: {self.mac_address}" + return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}" class WiredNetworkInterface(NetworkInterface, ABC): @@ -293,7 +296,9 @@ class WiredNetworkInterface(NetworkInterface, ABC): self.enabled = True self._connected_node.sys_log.info(f"Network Interface {self} enabled") - self.pcap = PacketCapture(hostname=self._connected_node.hostname, interface_num=self.port_num) + self.pcap = PacketCapture( + hostname=self._connected_node.hostname, port_num=self.port_num, port_name=self.port_name + ) if self._connected_link: self._connected_link.endpoint_up() @@ -1024,7 +1029,7 @@ class Node(SimComponent): self.sys_log.info("Resetting") self.power_off() - def connect_nic(self, network_interface: NetworkInterface): + def connect_nic(self, network_interface: NetworkInterface, port_name: Optional[str] = None): """ Connect a Network Interface to the node. @@ -1036,7 +1041,9 @@ class Node(SimComponent): new_nic_num = len(self.network_interfaces) self.network_interface[new_nic_num] = network_interface network_interface._connected_node = self - network_interface._port_num_on_node = new_nic_num + network_interface.port_num = new_nic_num + if port_name: + network_interface.port_name = port_name network_interface.parent = self self.sys_log.info(f"Connected Network Interface {network_interface}") if self.operating_state == NodeOperatingState.ON: 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 bc24270e..721814f8 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 @@ -83,4 +83,4 @@ class WirelessAccessPoint(IPWirelessNetworkInterface): :return: A string combining the port number, MAC address and IP address of the NIC. """ - return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}" + return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" 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 32acc08a..7b8f6f54 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 @@ -80,4 +80,4 @@ class WirelessNIC(IPWirelessNetworkInterface): :return: A string combining the port number, MAC address and IP address of the NIC. """ - return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}" + return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" 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 977380be..14a237a4 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -250,7 +250,7 @@ class NIC(IPWiredNetworkInterface): :return: A string combining the port number, MAC address and IP address of the NIC. """ - return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}" + return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" class HostNode(Node): diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index f2305652..7912d5d6 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -85,7 +85,17 @@ class Firewall(Router): if not kwargs.get("sys_log"): kwargs["sys_log"] = SysLog(hostname) - super().__init__(hostname=hostname, num_ports=3, **kwargs) + super().__init__(hostname=hostname, num_ports=0, **kwargs) + + self.connect_nic( + RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", port_name="external") + ) + self.connect_nic( + RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", port_name="internal") + ) + self.connect_nic( + RouterInterface(ip_address="127.0.0.1", subnet_mask="255.0.0.0", gateway="0.0.0.0", port_name="dmz") + ) # Initialise ACLs for internal and dmz interfaces with a default DENY policy self.internal_inbound_acl = AccessControlList( diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index aa6eec3a..b63fb43c 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -998,7 +998,7 @@ class RouterInterface(IPWiredNetworkInterface): :return: A string combining the port number, MAC address and IP address of the NIC. """ - return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}" + return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}/{self.ip_address}" class Router(NetworkNode): 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 91833d6a..3e8d715f 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/wireless_router.py @@ -80,7 +80,10 @@ class WirelessAccessPoint(IPWirelessNetworkInterface): :return: A string combining the port number, MAC address and IP address of the NIC. """ - return f"Port {self.port_num}: {self.mac_address}/{self.ip_address} ({self.frequency})" + return ( + f"Port {self.port_name if self.port_name else self.port_num}: " + f"{self.mac_address}/{self.ip_address} ({self.frequency})" + ) class WirelessRouter(Router): diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index fb8a1624..5419dde6 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -21,7 +21,13 @@ class PacketCapture: The PCAPs are logged to: //__pcap.log """ - def __init__(self, hostname: str, ip_address: Optional[str] = None, interface_num: Optional[int] = None): + def __init__( + self, + hostname: str, + ip_address: Optional[str] = None, + port_num: Optional[int] = None, + port_name: Optional[str] = None, + ): """ Initialize the PacketCapture process. @@ -32,9 +38,12 @@ class PacketCapture: "The hostname for which PCAP logs are being recorded." self.ip_address: str = ip_address "The IP address associated with the PCAP logs." - self.interface_num = interface_num + self.port_num = port_num "The interface num on the Node." + self.port_name = port_name + "The interface name on the Node." + self.inbound_logger = None self.outbound_logger = None @@ -42,6 +51,7 @@ class PacketCapture: self.setup_logger(outbound=False) self.setup_logger(outbound=True) + print(port_name) def setup_logger(self, outbound: bool = False): """Set up the logger configuration.""" @@ -79,10 +89,12 @@ class PacketCapture: def _get_logger_name(self, outbound: bool = False) -> str: """Get PCAP the logger name.""" + if self.port_name: + return f"{self.hostname}_{self.port_name}_{'outbound' if outbound else 'inbound'}_pcap" if self.ip_address: return f"{self.hostname}_{self.ip_address}_{'outbound' if outbound else 'inbound'}_pcap" - if self.interface_num: - return f"{self.hostname}_port-{self.interface_num}_{'outbound' if outbound else 'inbound'}_pcap" + if self.port_num: + return f"{self.hostname}_port-{self.port_num}_{'outbound' if outbound else 'inbound'}_pcap" return f"{self.hostname}_{'outbound' if outbound else 'inbound'}_pcap" def _get_log_path(self, outbound: bool = False) -> Path: From 2f3e40fb6b6abe943770119b109319bc0edb7266 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 29 Feb 2024 13:22:05 +0000 Subject: [PATCH 047/128] Fix issue around reset --- src/primaite/game/game.py | 2 +- src/primaite/session/environment.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index eeb0d007..3b9a21d4 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -417,7 +417,7 @@ class PrimaiteGame: agent_settings=agent_settings, ) else: - msg(f"Configuration error: {agent_type} is not a valid agent type.") + 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 diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index f8dbab9d..d54503a3 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -1,3 +1,4 @@ +import copy import json from typing import Any, Dict, Optional, SupportsFloat, Tuple @@ -23,7 +24,7 @@ class PrimaiteGymEnv(gymnasium.Env): super().__init__() self.game_config: Dict = game_config """PrimaiteGame definition. This can be changed between episodes to enable curriculum learning.""" - self.game: PrimaiteGame = PrimaiteGame.from_config(self.game_config) + self.game: PrimaiteGame = PrimaiteGame.from_config(copy.deepcopy(self.game_config)) """Current game.""" self._agent_name = next(iter(self.game.rl_agents)) """Name of the RL agent. Since there should only be one RL agent we can just pull the first and only key.""" @@ -78,7 +79,7 @@ class PrimaiteGymEnv(gymnasium.Env): f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {self.agent.reward_function.total_reward}" ) - self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.game_config) + self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=copy.deepcopy(self.game_config)) self.game.setup_for_episode(episode=self.episode_counter) self.episode_counter += 1 state = self.game.get_sim_state() From bd0b2e003346482fb84a3ecac86c8100439382d2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 29 Feb 2024 13:22:41 +0000 Subject: [PATCH 048/128] Remove redundant notebook cells --- src/primaite/notebooks/uc2_demo.ipynb | 33 +++------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index cf973905..13fb7d80 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -345,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, @@ -357,7 +357,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, @@ -412,7 +412,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -595,33 +595,6 @@ "env.reset()" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "net = env.game.simulation.network" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dbc = net.get_node_by_hostname('client_1').software_manager.software.get('DatabaseClient')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dbc._query_success_tracker" - ] - }, { "cell_type": "code", "execution_count": null, From 8f0de8521e087a580b8e3922a8114e418066b188 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Thu, 29 Feb 2024 14:08:42 +0000 Subject: [PATCH 049/128] #2326 - removed port_name print statement --- src/primaite/simulator/system/core/packet_capture.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index 5419dde6..4916966d 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -51,7 +51,6 @@ class PacketCapture: self.setup_logger(outbound=False) self.setup_logger(outbound=True) - print(port_name) def setup_logger(self, outbound: bool = False): """Set up the logger configuration.""" From 49a4e1fb5655f83d2d13437927bff71148d30c57 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 29 Feb 2024 15:20:54 +0000 Subject: [PATCH 050/128] #2257: added common node attributes page + ability to set node operating state via config + tests --- .../simulation/nodes/common/common.rst | 35 ++++++++ .../common/common_host_node_attributes.rst | 9 +- .../common/common_network_node_attributes.rst | 2 + .../nodes/common/common_node_attributes.rst | 42 ++++++++++ .../network/base_hardware.rst | 22 ++--- .../applications/data_manipulation_bot.rst | 2 +- .../system/applications/database_client.rst | 6 -- .../system/list_of_system_applications.rst | 2 +- .../system/list_of_system_services.rst | 2 +- .../simulation_components/system/software.rst | 2 +- src/primaite/game/game.py | 16 +++- src/primaite/simulator/core.py | 4 +- .../network/hardware/nodes/host/host_node.py | 25 +++--- .../hardware/nodes/network/firewall.py | 83 +++++++++++++++---- .../network/hardware/nodes/network/router.py | 4 +- .../configs/basic_switched_network.yaml | 11 +++ .../nodes/test_node_config.py | 20 ++++- ...software_installation_and_configuration.py | 10 ++- 18 files changed, 227 insertions(+), 70 deletions(-) create mode 100644 docs/source/configuration/simulation/nodes/common/common.rst diff --git a/docs/source/configuration/simulation/nodes/common/common.rst b/docs/source/configuration/simulation/nodes/common/common.rst new file mode 100644 index 00000000..d1c8f307 --- /dev/null +++ b/docs/source/configuration/simulation/nodes/common/common.rst @@ -0,0 +1,35 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +.. _Node Attributes: + +Common Attributes +################# + +Node Attributes +=============== + +Attributes that are shared by all nodes. + +.. include:: common_node_attributes.rst + +.. _Network Node Attributes: + +Network Node Attributes +======================= + +Attributes that are shared by nodes that inherit from :py:mod:`primaite.simulator.network.hardware.nodes.network.network_node.NetworkNode` + +.. include:: common_host_node_attributes.rst + +.. _Host Node Attributes: + +Host Node Attributes +==================== + +Attributes that are shared by nodes that inherit from :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.HostNode` + +.. include:: common_host_node_attributes.rst + +.. |NODE| replace:: node 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 b9f173c6..929d5714 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 @@ -2,6 +2,8 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _common_host_node_attributes: + ``ip_address`` -------------- @@ -19,13 +21,6 @@ The subnet mask for the |NODE| to use. The IP address that the |NODE| will use as the default gateway. Typically, this is the IP address of the closest router that the |NODE| is connected to. -``dns_server`` --------------- - -Optional. Default value is ``None`` - -The IP address of the node which holds an instance of the :ref:`DNSServer`. Some applications may use a domain name e.g. the :ref:`WebBrowser` - .. include:: ../software/applications.rst .. include:: ../software/services.rst 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 d0b3e65b..1161059f 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 @@ -2,6 +2,8 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _common_network_node_attributes: + ``routes`` ---------- 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 c1523518..34519adc 100644 --- a/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst +++ b/docs/source/configuration/simulation/nodes/common/common_node_attributes.rst @@ -2,6 +2,8 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK +.. _common_node_attributes: + ``ref`` ------- @@ -11,3 +13,43 @@ Human readable name used as reference for the |NODE|. Not used in code. ------------ The hostname of the |NODE|. This will be used to reference the |NODE|. + +``operating_state`` +------------------- + +The initial operating state of the node. + +Optional. Default value is ``ON``. + +Options available are: + +- ``ON`` +- ``OFF`` +- ``BOOTING`` +- ``SHUTTING_DOWN`` + +Note that YAML may assume non quoted ``ON`` and ``OFF`` as ``True`` and ``False`` respectively. To prevent this, use ``"ON"`` or ``"OFF"`` + +See :py:mod:`primaite.simulator.network.hardware.node_operating_state.NodeOperatingState` + + +``dns_server`` +-------------- + +Optional. Default value is ``None``. + +The IP address of the node which holds an instance of the :ref:`DNSServer`. Some applications may use a domain name e.g. the :ref:`WebBrowser` + +``start_up_duration`` +--------------------- + +Optional. Default value is ``3``. + +The number of time steps required to occur in order for the node to cycle from ``OFF`` to ``BOOTING_UP`` and then finally ``ON``. + +``shut_down_duration`` +---------------------- + +Optional. Default value is ``3``. + +The number of time steps required to occur in order for the node to cycle from ``ON`` to ``SHUTTING_DOWN`` and then finally ``OFF``. diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index 3aa6b073..1b83f3f4 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -12,34 +12,22 @@ complex, specialized hardware components inherit from and build upon. The key elements defined in ``base.py`` are: -NetworkInterface -================ +``NetworkInterface`` +==================== - Abstract base class for network interfaces like NICs. Defines common attributes like MAC address, speed, MTU. - Requires subclasses to implement ``enable()``, ``disable()``, ``send_frame()`` and ``receive_frame()``. - Provides basic state description and request handling capabilities. -Node -==== +``Node`` +======== The Node class stands as a central component in ``base.py``, acting as the superclass for all network nodes within a PrimAITE simulation. - - Node Attributes --------------- - -- **hostname**: The network hostname of the node. -- **operating_state**: Indicates the current hardware state of the node. -- **network_interfaces**: Maps interface names to NetworkInterface objects on the node. -- **network_interface**: Maps port IDs to ``NetworkInterface`` objects on the node. -- **dns_server**: Specifies DNS servers for domain name resolution. -- **start_up_duration**: The time it takes for the node to become fully operational after being powered on. -- **shut_down_duration**: The time required for the node to properly shut down. -- **sys_log**: A system log for recording events related to the node. -- **session_manager**: Manages user sessions within the node. -- **software_manager**: Controls the installation and management of software and services on the node. +See :ref:`Node Attributes` .. _Node Start up and Shut down: 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 d67e82d4..304621dd 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -79,7 +79,7 @@ Python data_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE") data_manipulation_bot.run() -This would connect to the database service at 192.168.1.14, authenticate, and execute the SQL statement to drop the 'users' table. +This would connect to the database service at 192.168.1.14, authenticate, and execute the SQL statement to delete database contents. Example with ``DataManipulationAgent`` """""""""""""""""""""""""""""""""""""" diff --git a/docs/source/simulation_components/system/applications/database_client.rst b/docs/source/simulation_components/system/applications/database_client.rst index 61d955f2..ddf6db11 100644 --- a/docs/source/simulation_components/system/applications/database_client.rst +++ b/docs/source/simulation_components/system/applications/database_client.rst @@ -24,12 +24,6 @@ Usage - Retrieve results in a dictionary. - Disconnect when finished. -To create database backups: - -- Configure the backup server on the :ref:`DatabaseService` by providing the Backup server ``IPv4Address`` with ``configure_backup`` -- Create a backup using ``backup_database``. This fails if the backup server is not configured. -- Restore a backup using ``restore_backup``. By default, this uses the database created via ``backup_database``. - Implementation ============== 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 fae0f5d4..193b3dc6 100644 --- a/docs/source/simulation_components/system/list_of_system_applications.rst +++ b/docs/source/simulation_components/system/list_of_system_applications.rst @@ -13,4 +13,4 @@ The list of applications that are considered system software are: - ``WebBrowser`` -More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` +More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.HostNode.SYSTEM_SOFTWARE` 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 4ff6f245..5acfc12e 100644 --- a/docs/source/simulation_components/system/list_of_system_services.rst +++ b/docs/source/simulation_components/system/list_of_system_services.rst @@ -15,4 +15,4 @@ The list of services that are considered system software are: - ``FTPClient`` - ``NTPClient`` -More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.SYSTEM_SOFTWARE` +More info :py:mod:`primaite.simulator.network.hardware.nodes.host.host_node.HostNode.SYSTEM_SOFTWARE` diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index 459064f0..2ba8e841 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -10,7 +10,7 @@ Software Base Software ------------- -All software which inherits ``IOSoftware`` installed on a node will not work unless the node has been turned on. +Software which inherits ``IOSoftware`` installed on a node will not work unless the node has been turned on. See :ref:`Node Start up and Shut down` diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 8d272418..42d998c7 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -234,7 +234,9 @@ class PrimaiteGame: subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), default_gateway=node_cfg["default_gateway"], dns_server=node_cfg.get("dns_server", None), - operating_state=NodeOperatingState.ON, + operating_state=NodeOperatingState.ON + if not (p := node_cfg.get("operating_state")) + else NodeOperatingState[p.upper()], ) elif n_type == "server": new_node = Server( @@ -243,13 +245,17 @@ class PrimaiteGame: subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")), default_gateway=node_cfg["default_gateway"], dns_server=node_cfg.get("dns_server", None), - operating_state=NodeOperatingState.ON, + operating_state=NodeOperatingState.ON + if not (p := node_cfg.get("operating_state")) + else NodeOperatingState[p.upper()], ) elif n_type == "switch": new_node = Switch( hostname=node_cfg["hostname"], num_ports=int(node_cfg.get("num_ports", "8")), - operating_state=NodeOperatingState.ON, + operating_state=NodeOperatingState.ON + if not (p := node_cfg.get("operating_state")) + else NodeOperatingState[p.upper()], ) elif n_type == "router": new_node = Router.from_config(node_cfg) @@ -359,7 +365,9 @@ class PrimaiteGame: new_node.shut_down_duration = 0 net.add_node(new_node) - new_node.power_on() + # run through the power on step if the node is to be turned on at the start + if new_node.operating_state == NodeOperatingState.ON: + new_node.power_on() game.ref_map_nodes[node_ref] = new_node.uuid # set start up and shut down duration diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 99e9be7f..6ab7c6e3 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -1,7 +1,7 @@ # flake8: noqa """Core of the PrimAITE Simulator.""" -from abc import ABC, abstractmethod -from typing import Callable, ClassVar, Dict, List, Optional, Union +from abc import abstractmethod +from typing import Callable, Dict, List, Optional, Union from uuid import uuid4 from pydantic import BaseModel, ConfigDict, Field 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 cb3c1bd7..703c2538 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -1,7 +1,7 @@ from __future__ import annotations from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, ClassVar, Dict, Optional from primaite import getLogger from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link, Node @@ -253,17 +253,6 @@ class NIC(IPWiredNetworkInterface): return f"Port {self.port_num}: {self.mac_address}/{self.ip_address}" -SYSTEM_SOFTWARE = { - "HostARP": HostARP, - "ICMP": ICMP, - "DNSClient": DNSClient, - "FTPClient": FTPClient, - "NTPClient": NTPClient, - "WebBrowser": WebBrowser, -} -"""List of system software that is automatically installed on nodes.""" - - class HostNode(Node): """ Represents a host node in the network. @@ -308,6 +297,16 @@ class HostNode(Node): * Web Browser: Provides web browsing capabilities. """ + SYSTEM_SOFTWARE: ClassVar[Dict] = { + "HostARP": HostARP, + "ICMP": ICMP, + "DNSClient": DNSClient, + "FTPClient": FTPClient, + "NTPClient": NTPClient, + "WebBrowser": WebBrowser, + } + """List of system software that is automatically installed on nodes.""" + network_interfaces: Dict[str, NIC] = {} "The Network Interfaces on the node." network_interface: Dict[int, NIC] = {} @@ -324,7 +323,7 @@ class HostNode(Node): This method equips the host with essential network services and applications, preparing it for various network-related tasks and operations. """ - for _, software_class in SYSTEM_SOFTWARE.items(): + for _, software_class in self.SYSTEM_SOFTWARE.items(): self.software_manager.install(software_class) super()._install_system_software() diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index b4d5cdba..26c50ff0 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -12,6 +12,8 @@ from primaite.simulator.network.hardware.nodes.network.router import ( RouterInterface, ) from primaite.simulator.network.transmission.data_link_layer import Frame +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.sys_log import SysLog from primaite.utils.validators import IPV4Address @@ -479,7 +481,12 @@ class Firewall(Router): @classmethod def from_config(cls, cfg: dict) -> "Firewall": """Create a firewall based on a config dict.""" - firewall = Firewall(hostname=cfg["hostname"], operating_state=NodeOperatingState.ON) + firewall = Firewall( + hostname=cfg["hostname"], + operating_state=NodeOperatingState.ON + if not (p := cfg.get("operating_state")) + else NodeOperatingState[p.upper()], + ) if "ports" in cfg: internal_port = cfg["ports"]["internal_port"] external_port = cfg["ports"]["external_port"] @@ -505,34 +512,82 @@ class Firewall(Router): if "acl" in cfg: # acl rules for internal_inbound_acl if cfg["acl"]["internal_inbound_acl"]: - firewall.internal_inbound_acl.max_acl_rules - firewall.internal_inbound_acl._default_config = cfg["acl"]["internal_inbound_acl"] - firewall.internal_inbound_acl._reset_rules_to_default() + for r_num, r_cfg in cfg["acl"]["internal_inbound_acl"].items(): + firewall.internal_inbound_acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) # acl rules for internal_outbound_acl if cfg["acl"]["internal_outbound_acl"]: - firewall.internal_outbound_acl._default_config = cfg["acl"]["internal_outbound_acl"] - firewall.internal_outbound_acl._reset_rules_to_default() + for r_num, r_cfg in cfg["acl"]["internal_outbound_acl"].items(): + firewall.internal_outbound_acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) # acl rules for dmz_inbound_acl if cfg["acl"]["dmz_inbound_acl"]: - firewall.dmz_inbound_acl._default_config = cfg["acl"]["dmz_inbound_acl"] - firewall.dmz_inbound_acl._reset_rules_to_default() + for r_num, r_cfg in cfg["acl"]["dmz_inbound_acl"].items(): + firewall.dmz_inbound_acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) # acl rules for dmz_outbound_acl if cfg["acl"]["dmz_outbound_acl"]: - firewall.dmz_outbound_acl._default_config = cfg["acl"]["dmz_outbound_acl"] - firewall.dmz_outbound_acl._reset_rules_to_default() + for r_num, r_cfg in cfg["acl"]["dmz_outbound_acl"].items(): + firewall.dmz_outbound_acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) # acl rules for external_inbound_acl if cfg["acl"]["external_inbound_acl"]: - firewall.external_inbound_acl._default_config = cfg["acl"]["external_inbound_acl"] - firewall.external_inbound_acl._reset_rules_to_default() + for r_num, r_cfg in cfg["acl"]["external_inbound_acl"].items(): + firewall.external_inbound_acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) # acl rules for external_outbound_acl if cfg["acl"]["external_outbound_acl"]: - firewall.external_outbound_acl._default_config = cfg["acl"]["external_outbound_acl"] - firewall.external_outbound_acl._reset_rules_to_default() + for r_num, r_cfg in cfg["acl"]["external_outbound_acl"].items(): + firewall.external_outbound_acl.add_rule( + action=ACLAction[r_cfg["action"]], + src_port=None if not (p := r_cfg.get("src_port")) else Port[p], + dst_port=None if not (p := r_cfg.get("dst_port")) else Port[p], + protocol=None if not (p := r_cfg.get("protocol")) else IPProtocol[p], + src_ip_address=r_cfg.get("src_ip"), + dst_ip_address=r_cfg.get("dst_ip"), + position=r_num, + ) + if "routes" in cfg: for route in cfg.get("routes"): firewall.route_table.add_route( diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 5b45f59c..d5302345 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -1401,7 +1401,9 @@ class Router(NetworkNode): router = Router( hostname=cfg["hostname"], num_ports=int(cfg.get("num_ports", "5")), - operating_state=NodeOperatingState.ON, + operating_state=NodeOperatingState.ON + if not (p := cfg.get("operating_state")) + else NodeOperatingState[p.upper()], ) if "ports" in cfg: for port_num, port_cfg in cfg["ports"].items(): diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index a248065c..daa40aa7 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -141,6 +141,17 @@ simulation: default_gateway: 192.168.10.1 dns_server: 192.168.1.10 # pre installed services and applications + - ref: client_3 + type: computer + hostname: client_3 + ip_address: 192.168.10.23 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + start_up_duration: 0 + shut_down_duration: 0 + operating_state: "OFF" + # pre installed services and applications links: - ref: switch_1___client_1 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 e222bfaf..f23e7612 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,6 +1,8 @@ from primaite.config.load import example_config_path from primaite.simulator.network.container import Network -from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ_NETWORK, load_config def test_example_config(): @@ -24,3 +26,19 @@ def test_dmz_config(): assert len(network.routers) == 2 # 2 routers in network assert len(network.switches) == 3 # 3 switches in network assert len(network.servers) == 2 # 2 servers in network + + +def test_basic_config(): + """Test that the basic_switched_network config can be parsed properly.""" + game = load_config(BASIC_CONFIG) + network: Network = game.simulation.network + assert len(network.nodes) == 4 # 4 nodes in network + + client_1: Computer = network.get_node_by_hostname("client_1") + assert client_1.operating_state == NodeOperatingState.ON + client_2: Computer = network.get_node_by_hostname("client_2") + assert client_2.operating_state == NodeOperatingState.ON + + # client 3 should not be online + client_3: Computer = network.get_node_by_hostname("client_3") + assert client_3.operating_state == NodeOperatingState.OFF 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 306f591d..f3dc51bd 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,6 +1,14 @@ from ipaddress import IPv4Address +from pathlib import Path +from typing import Union -from primaite.game.game import APPLICATION_TYPES_MAPPING, SERVICE_TYPES_MAPPING +import yaml + +from primaite.config.load import example_config_path +from primaite.game.agent.data_manipulation_bot import DataManipulationAgent +from primaite.game.agent.interface import ProxyAgent, RandomAgent +from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING +from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot From 10a40538876930afa371bac5c77691626917472b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Mar 2024 15:14:00 +0000 Subject: [PATCH 051/128] Fix tests --- docs/source/configuration/agents.rst | 6 +++--- .../_package_data/example_config_2_rl_agents.yaml | 2 +- src/primaite/session/session.py | 9 +++------ tests/assets/configs/dmz_network.yaml | 2 +- tests/e2e_integration_tests/test_primaite_session.py | 10 +++++----- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index f32843b1..ac67c365 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -19,7 +19,7 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo ... - ref: green_agent_example team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: @@ -57,11 +57,11 @@ Specifies if the agent is malicious (``RED``), benign (``GREEN``), or defensive ``type`` -------- -Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``GreenWebBrowsingAgent`` generate their own behaviour. +Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``probabilistic_agent`` generate their own behaviour. Available agent types: -- ``GreenWebBrowsingAgent`` +- ``probabilistic_agent`` - ``ProxyAgent`` - ``RedDatabaseCorruptingAgent`` diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index d6d3f044..b6b07afa 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -64,7 +64,7 @@ agents: - ref: client_1_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index b8f80e95..d244f6b0 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -4,7 +4,6 @@ from typing import Dict, List, Literal, Optional, Union from pydantic import BaseModel, ConfigDict -from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv, PrimaiteRayEnv, PrimaiteRayMARLEnv from primaite.session.io import SessionIO, SessionIOSettings @@ -40,7 +39,7 @@ class SessionMode(Enum): class PrimaiteSession: """The main entrypoint for PrimAITE sessions, this manages a simulation, policy training, and environments.""" - def __init__(self, game: PrimaiteGame): + def __init__(self, game_cfg: Dict): """Initialise PrimaiteSession object.""" self.training_options: TrainingOptions """Options specific to agent training.""" @@ -57,7 +56,7 @@ class PrimaiteSession: self.io_manager: Optional["SessionIO"] = None """IO manager for the session.""" - self.game: PrimaiteGame = game + self.game_cfg: Dict = game_cfg """Primaite Game object for managing main simulation loop and agents.""" def start_session(self) -> None: @@ -93,9 +92,7 @@ class PrimaiteSession: io_settings = cfg.get("io_settings", {}) io_manager = SessionIO(SessionIOSettings(**io_settings)) - game = PrimaiteGame.from_config(cfg) - - sess = cls(game=game) + sess = cls(game_cfg=cfg) sess.io_manager = io_manager sess.training_options = TrainingOptions(**cfg["training_config"]) diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 880735d9..56a68410 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -65,7 +65,7 @@ game: agents: - ref: client_1_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/e2e_integration_tests/test_primaite_session.py b/tests/e2e_integration_tests/test_primaite_session.py index 7785e4ae..da13dcd8 100644 --- a/tests/e2e_integration_tests/test_primaite_session.py +++ b/tests/e2e_integration_tests/test_primaite_session.py @@ -21,15 +21,15 @@ class TestPrimaiteSession: raise AssertionError assert session is not None - assert session.game.simulation - assert len(session.game.agents) == 3 - assert len(session.game.rl_agents) == 1 + assert session.env.game.simulation + assert len(session.env.game.agents) == 3 + assert len(session.env.game.rl_agents) == 1 assert session.policy assert session.env - assert session.game.simulation.network - assert len(session.game.simulation.network.nodes) == 10 + assert session.env.game.simulation.network + assert len(session.env.game.simulation.network.nodes) == 10 @pytest.mark.parametrize("temp_primaite_session", [[CFG_PATH]], indirect=True) def test_start_session(self, temp_primaite_session): From ed01293b862cb28fc7d13b9d04e994d98ca663cb Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Mar 2024 16:02:27 +0000 Subject: [PATCH 052/128] Make db admin reward persistent --- src/primaite/game/agent/rewards.py | 8 +++++--- src/primaite/game/game.py | 2 +- .../simulator/system/applications/database_client.py | 9 ++++----- .../simulator/system/applications/web_browser.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 4eb1ab3f..882ad024 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -263,11 +263,13 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): :type state: Dict """ db_state = access_from_nested_dict(state, self.location_in_state) - if db_state is NOT_PRESENT_IN_STATE or "connections_status" not in db_state: + if db_state is NOT_PRESENT_IN_STATE or "last_connection_successful" not in db_state: _LOGGER.debug(f"Can't calculate reward for {self.__class__.__name__}") - connections_status = db_state["connections_status"] - if False in connections_status: + last_connection_successful = db_state["last_connection_successful"] + if last_connection_successful is False: return -1.0 + elif last_connection_successful is True: + return 1.0 return 0 @classmethod diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index b9f92d3a..cf21dd40 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -296,7 +296,7 @@ class PrimaiteGame: if service_type == "DatabaseService": if "options" in service_cfg: opt = service_cfg["options"] - new_service.password = opt.get("backup_server_ip", None) + new_service.password = opt.get("db_password", None) new_service.configure_backup(backup_server=IPv4Address(opt.get("backup_server_ip"))) if service_type == "FTPServer": if "options" in service_cfg: diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index fe8180d7..addad35a 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from uuid import uuid4 from primaite import getLogger @@ -26,7 +26,7 @@ class DatabaseClient(Application): server_password: Optional[str] = None connected: bool = False _query_success_tracker: Dict[str, bool] = {} - _connections_status: List[bool] = [] + _last_connection_successful: Optional[bool] = None """Keep track of connections that were established or verified during this step. Used for rewards.""" def __init__(self, **kwargs): @@ -46,7 +46,7 @@ class DatabaseClient(Application): can_connect = self.connect(connection_id=list(self.connections.keys())[-1]) else: can_connect = self.connect() - self._connections_status.append(can_connect) + self._last_connection_successful = can_connect return can_connect def describe_state(self) -> Dict: @@ -57,8 +57,7 @@ class DatabaseClient(Application): """ state = super().describe_state() # list of connections that were established or verified during this step. - state["connections_status"] = [c for c in self._connections_status] - self._connections_status.clear() + state["last_connection_successful"] = self._last_connection_successful return state def configure(self, server_ip_address: IPv4Address, server_password: Optional[str] = None): diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 6f2c479c..9fa86328 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -199,7 +199,7 @@ class WebBrowser(Application): def state(self) -> Dict: """Return the contents of this dataclass as a dict for use with describe_state method.""" if self.status == self._HistoryItemStatus.LOADED: - outcome = self.response_code + outcome = self.response_code.value else: outcome = self.status.value return {"url": self.url, "outcome": outcome} From 2a1d99cccee0c49360f84fc0640240c77052c65f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Mar 2024 16:36:41 +0000 Subject: [PATCH 053/128] Fix problem with checking connection for db admin --- .../system/applications/database_client.py | 14 ++++++++------ .../applications/red_applications/dos_bot.py | 2 +- .../system/services/database/database_service.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index addad35a..69065225 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -43,9 +43,9 @@ class DatabaseClient(Application): def execute(self) -> bool: """Execution definition for db client: perform a select query.""" if self.connections: - can_connect = self.connect(connection_id=list(self.connections.keys())[-1]) + can_connect = self.check_connection(connection_id=list(self.connections.keys())[-1]) else: - can_connect = self.connect() + can_connect = self.check_connection(connection_id=str(uuid4())) self._last_connection_successful = can_connect return can_connect @@ -79,15 +79,17 @@ class DatabaseClient(Application): if not connection_id: connection_id = str(uuid4()) - # if we are reusing a connection_id, remove it from self.connections so that its new status can be populated - # warning: janky - self._connections.pop(connection_id, None) - self.connected = self._connect( server_ip_address=self.server_ip_address, password=self.server_password, connection_id=connection_id ) return self.connected + def check_connection(self, connection_id:str) -> bool: + if not self._can_perform_action(): + return False + print(self.query("SELECT * FROM pg_stat_activity", connection_id=connection_id)) + return self.connected + def _connect( self, server_ip_address: IPv4Address, 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 9dac6b25..1247bc99 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -28,7 +28,7 @@ class DoSAttackStage(IntEnum): "Attack is completed." -class DoSBot(DatabaseClient, Application): +class DoSBot(DatabaseClient): """A bot that simulates a Denial of Service attack.""" target_ip_address: Optional[IPv4Address] = None diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 9fdfd5ff..c73132eb 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -221,6 +221,18 @@ class DatabaseService(Service): } else: return {"status_code": 404, "data": False} + elif query == "SELECT * FROM pg_stat_activity": + # Check if the connection is active. + if self.health_state_actual == SoftwareHealthState.GOOD: + return { + "status_code": 200, + "type": "sql", + "data": False, + "uuid": query_id, + "connection_id": connection_id, + } + else: + return {"status_code": 401, "data": False} else: # Invalid query self.sys_log.info(f"{self.name}: Invalid {query}") From 78ff658e30f96418ed17db6ecaf147f5f1cc019d Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Mar 2024 16:48:05 +0000 Subject: [PATCH 054/128] #2356: optional dmz port + optional external acl rules --- .../hardware/nodes/network/firewall.py | 17 +- tests/assets/configs/basic_firewall.yaml | 174 ++++++++++++++++++ .../configuration_file_parsing/__init__.py | 2 + .../nodes/network/test_firewall_config.py | 26 ++- 4 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 tests/assets/configs/basic_firewall.yaml diff --git a/src/primaite/simulator/network/hardware/nodes/network/firewall.py b/src/primaite/simulator/network/hardware/nodes/network/firewall.py index f5ddcfad..d7b1dfd9 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/firewall.py +++ b/src/primaite/simulator/network/hardware/nodes/network/firewall.py @@ -500,7 +500,7 @@ class Firewall(Router): if "ports" in cfg: internal_port = cfg["ports"]["internal_port"] external_port = cfg["ports"]["external_port"] - dmz_port = cfg["ports"]["dmz_port"] + dmz_port = cfg["ports"].get("dmz_port") # configure internal port firewall.configure_internal_port( @@ -514,11 +514,12 @@ class Firewall(Router): subnet_mask=IPV4Address(external_port.get("subnet_mask", "255.255.255.0")), ) - # configure dmz port - firewall.configure_dmz_port( - ip_address=IPV4Address(dmz_port.get("ip_address")), - subnet_mask=IPV4Address(dmz_port.get("subnet_mask", "255.255.255.0")), - ) + # configure dmz port if not none + if dmz_port is not None: + firewall.configure_dmz_port( + ip_address=IPV4Address(dmz_port.get("ip_address")), + subnet_mask=IPV4Address(dmz_port.get("subnet_mask", "255.255.255.0")), + ) if "acl" in cfg: # acl rules for internal_inbound_acl if cfg["acl"]["internal_inbound_acl"]: @@ -573,7 +574,7 @@ class Firewall(Router): ) # acl rules for external_inbound_acl - if cfg["acl"]["external_inbound_acl"]: + if cfg["acl"].get("external_inbound_acl"): for r_num, r_cfg in cfg["acl"]["external_inbound_acl"].items(): firewall.external_inbound_acl.add_rule( action=ACLAction[r_cfg["action"]], @@ -586,7 +587,7 @@ class Firewall(Router): ) # acl rules for external_outbound_acl - if cfg["acl"]["external_outbound_acl"]: + if cfg["acl"].get("external_outbound_acl"): for r_num, r_cfg in cfg["acl"]["external_outbound_acl"].items(): firewall.external_outbound_acl.add_rule( action=ACLAction[r_cfg["action"]], diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml new file mode 100644 index 00000000..71dc31a7 --- /dev/null +++ b/tests/assets/configs/basic_firewall.yaml @@ -0,0 +1,174 @@ +# Basic Switched network +# +# -------------- -------------- -------------- +# | client_1 |------| switch_1 |------| client_2 | +# -------------- -------------- -------------- +# + +training_config: + rl_framework: SB3 + rl_algorithm: PPO + seed: 333 + n_learn_episodes: 1 + n_eval_episodes: 5 + max_steps_per_episode: 128 + deterministic_eval: false + n_agents: 1 + agent_references: + - defender + +io_settings: + save_checkpoints: true + checkpoint_interval: 5 + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + + +game: + max_episode_length: 256 + ports: + - ARP + - DNS + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + +agents: + - ref: client_2_green_user + team: GREEN + type: GreenWebBrowsingAgent + 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 + + agent_settings: + start_settings: + start_step: 5 + frequency: 4 + variance: 3 + +simulation: + network: + nodes: + + - ref: firewall + type: firewall + hostname: firewall + start_up_duration: 0 + shut_down_duration: 0 + ports: + external_port: # port 1 + ip_address: 192.168.20.1 + subnet_mask: 255.255.255.0 + internal_port: # port 2 + ip_address: 192.168.1.2 + subnet_mask: 255.255.255.0 + acl: + internal_inbound_acl: + 21: + action: PERMIT + protocol: TCP + 22: + action: PERMIT + protocol: UDP + 23: + action: PERMIT + protocol: ICMP + internal_outbound_acl: + 21: + action: PERMIT + protocol: TCP + 22: + action: PERMIT + protocol: UDP + 23: + action: PERMIT + protocol: ICMP + dmz_inbound_acl: + 21: + action: PERMIT + protocol: TCP + 22: + action: PERMIT + protocol: UDP + 23: + action: PERMIT + protocol: ICMP + dmz_outbound_acl: + 21: + action: PERMIT + protocol: TCP + 22: + action: PERMIT + protocol: UDP + 23: + action: PERMIT + protocol: ICMP + + - ref: switch_1 + type: switch + hostname: switch_1 + num_ports: 8 + - ref: switch_2 + type: switch + hostname: switch_2 + num_ports: 8 + + - ref: client_1 + type: computer + hostname: client_1 + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + # pre installed services and applications + - ref: client_2 + type: computer + hostname: client_2 + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + # pre installed services and applications + + links: + - ref: switch_1___client_1 + endpoint_a_ref: switch_1 + endpoint_a_port: 1 + endpoint_b_ref: client_1 + endpoint_b_port: 1 + - ref: switch_2___client_2 + endpoint_a_ref: switch_2 + endpoint_a_port: 1 + endpoint_b_ref: client_2 + endpoint_b_port: 1 + - ref: switch_1___firewall + endpoint_a_ref: switch_1 + endpoint_a_port: 2 + endpoint_b_ref: firewall + endpoint_b_port: 1 + - ref: switch_2___firewall + endpoint_a_ref: switch_2 + endpoint_a_port: 2 + endpoint_b_ref: firewall + endpoint_b_port: 2 diff --git a/tests/integration_tests/configuration_file_parsing/__init__.py b/tests/integration_tests/configuration_file_parsing/__init__.py index 1c8481d6..be21c036 100644 --- a/tests/integration_tests/configuration_file_parsing/__init__.py +++ b/tests/integration_tests/configuration_file_parsing/__init__.py @@ -10,6 +10,8 @@ BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" DMZ_NETWORK = TEST_ASSETS_ROOT / "configs/dmz_network.yaml" +BASIC_FIREWALL = TEST_ASSETS_ROOT / "configs/basic_firewall.yaml" + def load_config(config_path: Union[str, Path]) -> PrimaiteGame: """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" 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 2e0556e9..fc6e05ec 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,3 +1,5 @@ +from ipaddress import IPv4Address + import pytest from primaite.simulator.network.container import Network @@ -8,7 +10,7 @@ from primaite.simulator.network.hardware.nodes.network.firewall import Firewall from primaite.simulator.network.hardware.nodes.network.router import ACLAction from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from tests.integration_tests.configuration_file_parsing import DMZ_NETWORK, load_config +from tests.integration_tests.configuration_file_parsing import BASIC_FIREWALL, DMZ_NETWORK, load_config @pytest.fixture(scope="function") @@ -17,6 +19,12 @@ def dmz_config() -> Network: return game.simulation.network +@pytest.fixture(scope="function") +def basic_firewall_config() -> Network: + game = load_config(BASIC_FIREWALL) + return game.simulation.network + + def test_firewall_is_in_configuration(dmz_config): """Test that the firewall exists in the configuration file.""" network: Network = dmz_config @@ -109,3 +117,19 @@ def test_firewall_acl_rules_correctly_added(dmz_config): # external_outbound should have implicit action PERMIT # ICMP does not have a provided ACL Rule but implicit action should allow anything assert firewall.external_outbound_acl.implicit_action == ACLAction.PERMIT + + +def test_firewall_with_no_dmz_port(basic_firewall_config): + """ + Test to check that: + - the DMZ port can be ignored i.e. is optional. + - the external_outbound_acl and external_inbound_acl are optional + """ + network: Network = basic_firewall_config + + firewall: Firewall = network.get_node_by_hostname("firewall") + + assert firewall.dmz_port.ip_address == IPv4Address("127.0.0.1") + + assert firewall.external_outbound_acl.num_rules == 0 + assert firewall.external_inbound_acl.num_rules == 0 From af036f63f1961d136380ceaa4e1068aae20846f1 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 1 Mar 2024 22:37:51 +0000 Subject: [PATCH 055/128] #2357 - Allowed the config to not have nodes, links and agents and still be parsed --- src/primaite/game/game.py | 14 ++++++--- .../no_nodes_links_agents_network.yaml | 31 +++++++++++++++++++ .../test_no_nodes_links_agents_config.py | 19 ++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 tests/assets/configs/no_nodes_links_agents_network.yaml create mode 100644 tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 42d998c7..2659abef 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -222,8 +222,12 @@ class PrimaiteGame: sim = game.simulation net = sim.network - nodes_cfg = cfg["simulation"]["network"]["nodes"] - links_cfg = cfg["simulation"]["network"]["links"] + simulation_config = cfg.get("simulation", {}) + network_config = simulation_config.get("network", {}) + + nodes_cfg = network_config.get("nodes", []) + links_cfg = network_config.get("links", []) + for node_cfg in nodes_cfg: node_ref = node_cfg["ref"] n_type = node_cfg["type"] @@ -390,7 +394,7 @@ class PrimaiteGame: game.ref_map_links[link_cfg["ref"]] = new_link.uuid # 3. create agents - agents_cfg = cfg["agents"] + agents_cfg = cfg.get("agents", []) for agent_cfg in agents_cfg: agent_ref = agent_cfg["ref"] # noqa: F841 @@ -439,12 +443,12 @@ class PrimaiteGame: agent_settings=agent_settings, ) else: - msg(f"Configuration error: {agent_type} is not a valid agent type.") + 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 # Set the NMNE capture config - set_nmne_config(cfg["simulation"]["network"].get("nmne_config", {})) + set_nmne_config(network_config.get("nmne_config", {})) return game diff --git a/tests/assets/configs/no_nodes_links_agents_network.yaml b/tests/assets/configs/no_nodes_links_agents_network.yaml new file mode 100644 index 00000000..607a899a --- /dev/null +++ b/tests/assets/configs/no_nodes_links_agents_network.yaml @@ -0,0 +1,31 @@ +training_config: + rl_framework: SB3 + rl_algorithm: PPO + seed: 333 + n_learn_episodes: 1 + n_eval_episodes: 5 + max_steps_per_episode: 128 + deterministic_eval: false + n_agents: 1 + agent_references: + - defender + +io_settings: + save_checkpoints: true + checkpoint_interval: 5 + save_step_metadata: false + save_pcap_logs: true + save_sys_logs: true + + +game: + max_episode_length: 256 + ports: + - ARP + - DNS + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP 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 new file mode 100644 index 00000000..5c9b0cb9 --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/test_no_nodes_links_agents_config.py @@ -0,0 +1,19 @@ +import yaml + +from primaite.game.game import PrimaiteGame +from tests import TEST_ASSETS_ROOT + +CONFIG_FILE = TEST_ASSETS_ROOT / "configs" / "no_nodes_links_agents_network.yaml" + + +def test_no_nodes_links_agents_config(): + """Tests PrimaiteGame can be created from config file where there are no nodes, links, agents in the config file.""" + with open(CONFIG_FILE, "r") as f: + cfg = yaml.safe_load(f) + + game = PrimaiteGame.from_config(cfg) + + network = game.simulation.network + + assert len(network.nodes) == 0 + assert len(network.links) == 0 From 81fd43035d84273a121b8d9601f6b8ab8b8432ea Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 1 Mar 2024 22:51:01 +0000 Subject: [PATCH 056/128] #2358 - the node-specific properties in Network class now simply use node.__class__.__name__ to check their type for filtering by type. Tests updated to use the new property function names --- src/primaite/simulator/network/container.py | 39 +++++++++++-------- tests/conftest.py | 2 +- .../nodes/test_node_config.py | 13 ++++--- ...software_installation_and_configuration.py | 6 +-- .../_simulator/_network/test_container.py | 8 ++-- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index b5a16430..6c2f38c5 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -8,10 +8,6 @@ from prettytable import MARKDOWN, PrettyTable from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.network.hardware.base import Link, Node, WiredNetworkInterface -from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.hardware.nodes.network.router import Router -from primaite.simulator.network.hardware.nodes.network.switch import Switch from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.service import Service @@ -85,24 +81,29 @@ class Network(SimComponent): self.links[link_id].apply_timestep(timestep=timestep) @property - def routers(self) -> List[Router]: + def router_nodes(self) -> List[Node]: """The Routers in the Network.""" - return [node for node in self.nodes.values() if isinstance(node, Router)] + return [node for node in self.nodes.values() if node.__class__.__name__ == "Router"] @property - def switches(self) -> List[Switch]: + def switch_nodes(self) -> List[Node]: """The Switches in the Network.""" - return [node for node in self.nodes.values() if isinstance(node, Switch)] + return [node for node in self.nodes.values() if node.__class__.__name__ == "Switch"] @property - def computers(self) -> List[Computer]: + def computer_nodes(self) -> List[Node]: """The Computers in the Network.""" - return [node for node in self.nodes.values() if isinstance(node, Computer) and not isinstance(node, Server)] + return [node for node in self.nodes.values() if node.__class__.__name__ == "Computer"] @property - def servers(self) -> List[Server]: + def server_nodes(self) -> List[Node]: """The Servers in the Network.""" - return [node for node in self.nodes.values() if isinstance(node, Server)] + return [node for node in self.nodes.values() if node.__class__.__name__ == "Server"] + + @property + def firewall_nodes(self) -> List[Node]: + """The Firewalls in the Network.""" + return [node for node in self.nodes.values() if node.__class__.__name__ == "Firewall"] def show(self, nodes: bool = True, ip_addresses: bool = True, links: bool = True, markdown: bool = False): """ @@ -117,10 +118,11 @@ class Network(SimComponent): :param markdown: Use Markdown style in table output. Defaults to False. """ nodes_type_map = { - "Router": self.routers, - "Switch": self.switches, - "Server": self.servers, - "Computer": self.computers, + "Router": self.router_nodes, + "Firewall": self.firewall_nodes, + "Switch": self.switch_nodes, + "Server": self.server_nodes, + "Computer": self.computer_nodes, } if nodes: table = PrettyTable(["Node", "Type", "Operating State"]) @@ -143,7 +145,10 @@ class Network(SimComponent): for node in nodes: for i, port in node.network_interface.items(): if hasattr(port, "ip_address"): - table.add_row([node.hostname, i, port.ip_address, port.subnet_mask, node.default_gateway]) + port_str = port.port_name if port.port_name else port.port_num + table.add_row( + [node.hostname, port_str, port.ip_address, port.subnet_mask, node.default_gateway] + ) print(table) if links: diff --git a/tests/conftest.py b/tests/conftest.py index dbfff2f3..a8c3bdd1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -422,7 +422,7 @@ def install_stuff_to_sim(sim: Simulation): assert len(sim.network.nodes) == 6 assert len(sim.network.links) == 5 # 5.1: Assert the router is correctly configured - r = sim.network.routers[0] + r = sim.network.router_nodes[0] for i, acl_rule in enumerate(r.acl.acl): if i == 1: assert acl_rule.src_port == acl_rule.dst_port == Port.DNS 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 f23e7612..8797bf2e 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 @@ -11,9 +11,9 @@ def test_example_config(): network: Network = game.simulation.network assert len(network.nodes) == 10 # 10 nodes in example network - assert len(network.routers) == 1 # 1 router in network - assert len(network.switches) == 2 # 2 switches in network - assert len(network.servers) == 5 # 5 servers in network + assert len(network.router_nodes) == 1 # 1 router in network + assert len(network.switch_nodes) == 2 # 2 switches in network + assert len(network.server_nodes) == 5 # 5 servers in network def test_dmz_config(): @@ -23,9 +23,10 @@ def test_dmz_config(): network: Network = game.simulation.network assert len(network.nodes) == 9 # 9 nodes in network - assert len(network.routers) == 2 # 2 routers in network - assert len(network.switches) == 3 # 3 switches in network - assert len(network.servers) == 2 # 2 servers in network + assert len(network.router_nodes) == 1 # 1 router in network + assert len(network.firewall_nodes) == 1 # 1 firewall in network + assert len(network.switch_nodes) == 3 # 3 switches in network + assert len(network.server_nodes) == 2 # 2 servers in network def test_basic_config(): 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 f3dc51bd..7da66547 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 @@ -60,9 +60,9 @@ def test_example_config(): network: Network = game.simulation.network assert len(network.nodes) == 10 # 10 nodes in example network - assert len(network.routers) == 1 # 1 router in network - assert len(network.switches) == 2 # 2 switches in network - assert len(network.servers) == 5 # 5 servers in network + assert len(network.router_nodes) == 1 # 1 router in network + assert len(network.switch_nodes) == 2 # 2 switches in network + assert len(network.server_nodes) == 5 # 5 servers in network def test_node_software_install(): diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index 2cfc3f11..f0e386b8 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -26,10 +26,10 @@ def filter_keys_nested_item(data, keys): @pytest.fixture(scope="function") def network(example_network) -> Network: - assert len(example_network.routers) is 1 - assert len(example_network.switches) is 2 - assert len(example_network.computers) is 2 - assert len(example_network.servers) is 2 + assert len(example_network.router_nodes) is 1 + assert len(example_network.switch_nodes) is 2 + assert len(example_network.computer_nodes) is 2 + assert len(example_network.server_nodes) is 2 example_network.show() From 80158fd9b4e1b2beeff3c42843c1f50b0dbc6716 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 11:18:06 +0000 Subject: [PATCH 057/128] Make db manipulation bot work with db client --- src/primaite/game/game.py | 6 +-- .../network/transmission/network_layer.py | 2 + .../system/applications/database_client.py | 9 +++- .../red_applications/data_manipulation_bot.py | 48 +++++++++++++++---- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index cf21dd40..10c02b39 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -409,9 +409,6 @@ class PrimaiteGame: # CREATE REWARD FUNCTION reward_function = RewardFunction.from_config(reward_function_cfg) - # OTHER AGENT SETTINGS - agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) - # CREATE AGENT if agent_type == "probabilistic_agent": # TODO: implement non-random agents and fix this parsing @@ -424,6 +421,7 @@ class PrimaiteGame: 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, @@ -433,6 +431,8 @@ class PrimaiteGame: ) 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, diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index dc848ade..22d7f97d 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -15,6 +15,8 @@ class IPProtocol(Enum): .. _List of IPProtocols: """ + NONE = "none" + """Placeholder for a non-port.""" TCP = "tcp" """Transmission Control Protocol.""" UDP = "udp" diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 69065225..a8eac196 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -84,7 +84,14 @@ class DatabaseClient(Application): ) return self.connected - def check_connection(self, connection_id:str) -> bool: + def check_connection(self, connection_id: str) -> bool: + """Check whether the connection can be successfully re-established. + + :param connection_id: connection ID to check + :type connection_id: str + :return: Whether the connection was successfully re-established. + :rtype: bool + """ if not self._can_perform_action(): return False print(self.query("SELECT * FROM pg_stat_activity", connection_id=connection_id)) 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 5fe951b7..11eb71f5 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,10 +1,13 @@ from enum import IntEnum from ipaddress import IPv4Address -from typing import Optional +from typing import Dict, Optional from primaite import getLogger from primaite.game.science import simulate_trial from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient _LOGGER = getLogger(__name__) @@ -32,12 +35,12 @@ class DataManipulationAttackStage(IntEnum): "Signifies that the attack has failed." -class DataManipulationBot(DatabaseClient): +class DataManipulationBot(Application): """A bot that simulates a script which performs a SQL injection attack.""" - server_ip_address: Optional[IPv4Address] = None + # server_ip_address: Optional[IPv4Address] = None payload: Optional[str] = None - server_password: Optional[str] = None + # server_password: Optional[str] = None port_scan_p_of_success: float = 0.1 data_manipulation_p_of_success: float = 0.1 @@ -46,8 +49,31 @@ class DataManipulationBot(DatabaseClient): "Whether to repeat attacking once finished." def __init__(self, **kwargs): + kwargs["name"] = "DataManipulationBot" + kwargs["port"] = Port.NONE + kwargs["protocol"] = IPProtocol.NONE + super().__init__(**kwargs) - self.name = "DataManipulationBot" + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + return state + + @property + def _host_db_client(self) -> DatabaseClient: + """Return the database client that is installed on the same machine as the DataManipulationBot.""" + db_client = self.software_manager.software.get("DatabaseClient") + if db_client is None: + _LOGGER.info(f"{self.__class__.__name__} cannot find a database client on its host.") + return db_client def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() @@ -76,8 +102,8 @@ class DataManipulationBot(DatabaseClient): :param repeat: Whether to repeat attacking once finished. """ self.server_ip_address = server_ip_address - self.payload = payload self.server_password = server_password + self.payload = payload self.port_scan_p_of_success = port_scan_p_of_success self.data_manipulation_p_of_success = data_manipulation_p_of_success self.repeat = repeat @@ -123,15 +149,17 @@ class DataManipulationBot(DatabaseClient): :param p_of_success: Probability of successfully performing data manipulation, by default 0.1. """ + self._host_db_client.server_ip_address = self.server_ip_address + self._host_db_client.server_password = self.server_password if self.attack_stage == DataManipulationAttackStage.PORT_SCAN: # perform the actual data manipulation attack if simulate_trial(p_of_success): self.sys_log.info(f"{self.name}: Performing data manipulation") # perform the attack - if not len(self.connections): - self.connect() - if len(self.connections): - self.query(self.payload) + if not len(self._host_db_client.connections): + self._host_db_client.connect() + if len(self._host_db_client.connections): + self._host_db_client.query(self.payload) self.sys_log.info(f"{self.name} payload delivered: {self.payload}") attack_successful = True if attack_successful: From 4a292a6239d748baf224ef8a52519a1eed6a0f3c Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 11:23:24 +0000 Subject: [PATCH 058/128] Fix checking connection in db client --- src/primaite/simulator/system/applications/database_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index a8eac196..7b259ff4 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -94,8 +94,7 @@ class DatabaseClient(Application): """ if not self._can_perform_action(): return False - print(self.query("SELECT * FROM pg_stat_activity", connection_id=connection_id)) - return self.connected + return self.query("SELECT * FROM pg_stat_activity", connection_id=connection_id) def _connect( self, From 9762927289568d7b68da5214a436e68ebd4b61c4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 11:43:24 +0000 Subject: [PATCH 059/128] Update notebook with new changes --- src/primaite/notebooks/uc2_demo.ipynb | 620 ++++++++++++++++++++++++-- 1 file changed, 581 insertions(+), 39 deletions(-) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index 13fb7d80..36942b73 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -13,7 +13,7 @@ "source": [ "## Scenario\n", "\n", - "The network consists of an office subnet and a server subnet. Clients in the office access a website which fetches data from a database.\n", + "The network consists of an office subnet and a server subnet. Clients in the office access a website which fetches data from a database. Occasionally, admins need to access the database directly from the clients.\n", "\n", "[](_package_data/uc2_network.png)\n", "\n", @@ -46,7 +46,9 @@ "source": [ "## Green agent\n", "\n", - "There are green agents logged onto client 1 and client 2. They use the web browser to navigate to `http://arcd.com/users`. The web server replies with a status code 200 if the data is available on the database or 404 if not available." + "There are green agents logged onto client 1 and client 2. They use the web browser to navigate to `http://arcd.com/users`. The web server replies with a status code 200 if the data is available on the database or 404 if not available.\n", + "\n", + "Sometimes, the green agents send a request directly to the database to check that it is reachable." ] }, { @@ -68,7 +70,9 @@ "source": [ "## Blue agent\n", "\n", - "The blue agent can view the entire network, but the health statuses of components are not updated until a scan is performed. The blue agent should restore the database file from backup after it was compromised. It can also prevent further attacks by blocking the red agent client from sending the malicious SQL query to the database server. This can be done by implementing an ACL rule on the router." + "The blue agent can view the entire network, but the health statuses of components are not updated until a scan is performed. The blue agent should restore the database file from backup after it was compromised. It can also prevent further attacks by blocking the red agent client from sending the malicious SQL query to the database server. This can be done by implementing an ACL rule on the router.\n", + "\n", + "However, these rules will also impact greens' ability to check the database connection. The blue agent should only block the infected client, it should let the other client connect freely. Once the attack has begun, automated traffic monitoring will detect it as suspicious network traffic. The blue agent's observation space will show this as an increase in the number of malicious network events (NMNE) on one of the network interfaces. To achieve optimal reward, the agent should only block the client which has the non-zero outbound NMNE." ] }, { @@ -101,9 +105,11 @@ "The red agent does not use information about the state of the network to decide its action.\n", "\n", "### Green\n", - "The green agents use the web browser application to send requests to the web server. The schedule of each green agent is currently random, meaning it will request webpage with a 50% probability, and do nothing with a 50% probability.\n", + "The green agents use the web browser application to send requests to the web server. The schedule of each green agent is currently random, it will do nothing 30% of the time, send a web request 60% of the time, and send a db status check 10% of the time.\n", "\n", - "When a green agent is blocked from accessing the data through the webpage, this incurs a negative reward to the RL defender." + "When a green agent is blocked from accessing the data through the webpage, this incurs a negative reward to the RL defender.\n", + "\n", + "Also, when the green agent is blocked from checking the database status, it causes a small negative reward." ] }, { @@ -322,9 +328,10 @@ "source": [ "## Reward Function\n", "\n", - "The blue agent's reward is calculated using two measures:\n", + "The blue agent's reward is calculated using these measures:\n", "1. Whether the database file is in a good state (+1 for good, -1 for corrupted, 0 for any other state)\n", "2. Whether each green agents' most recent webpage request was successful (+1 for a `200` return code, -1 for a `404` return code and 0 otherwise).\n", + "3. Whether each green agents' most recent DB status check was successful (+1 for a successful connection, -1 for no connection).\n", "\n", "The file status reward and the two green-agent-related rewards are averaged to get a total step reward.\n" ] @@ -345,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [] }, @@ -357,7 +364,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "tags": [] }, @@ -382,9 +389,169 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Resetting environment, episode 0, avg. reward: 0.0\n", + "env created successfully\n", + "{'ACL': {1: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 2: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 3: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 4: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 5: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 6: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 7: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 8: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 9: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 10: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0}},\n", + " 'ICS': 0,\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 1}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", + " 'health_status': 1}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0,\n", + " 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}}\n" + ] + } + ], "source": [ "# create the env\n", "with open(example_config_path(), 'r') as f:\n", @@ -407,12 +574,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The red agent will start attacking at some point between step 20 and 30. When this happens, the reward will drop immediately, then drop to -1.0 when green agents try to access the webpage." + "The red agent will start attacking at some point between step 20 and 30. When this happens, the reward will drop immediately, then drop to -0.8 when green agents try to access the webpage." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -430,9 +597,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 1, Red action: DO NOTHING, Blue reward:0.77\n", + "step: 2, Red action: DO NOTHING, Blue reward:0.77\n", + "step: 3, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 4, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 5, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 6, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 7, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 8, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 9, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 10, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 11, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 12, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 13, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 14, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 15, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 16, Red action: DO NOTHING, Blue reward:1.10\n", + "step: 17, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 18, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 19, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 20, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 21, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 22, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 23, Red action: DO NOTHING, Blue reward:1.20\n", + "step: 24, Red action: ATTACK from client 2, Blue reward:0.52\n", + "step: 25, Red action: DO NOTHING, Blue reward:0.52\n", + "step: 26, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 27, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 28, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 29, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 30, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 31, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 32, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 33, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 34, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 35, Red action: DO NOTHING, Blue reward:-0.80\n" + ] + } + ], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -448,9 +657,65 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 1, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 1}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}\n" + ] + } + ], "source": [ "pprint(obs['NODES'])" ] @@ -464,9 +729,65 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 1, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", + " 'nmne': {'inbound': 0, 'outbound': 1}},\n", + " 2: {'nic_status': 0,\n", + " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -481,6 +802,13 @@ "File 1 in folder 1 on node 3 has `health_status = 2`, indicating that the database file is compromised." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Also, the NMNE outbound of either client 1 (node 6) or client 2 (node 7) has increased from 0 to 1. This tells us which client is being used by the red agent." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -490,9 +818,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 38\n", + "Red action: DONOTHING\n", + "Green action: DONOTHING\n", + "Green action: DONOTHING\n", + "Blue reward:-0.8\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -515,16 +855,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 52\n", + "Red action: DONOTHING\n", + "Green action: ('NODE_APPLICATION_EXECUTE', {'node_id': 0, 'application_id': 0})\n", + "Green action: ('NODE_APPLICATION_EXECUTE', {'node_id': 0, 'application_id': 0})\n", + "Blue reward:-0.80\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'][0]}\" )\n", "print(f\"Green action: {info['agent_actions']['client_2_green_user']}\" )\n", "print(f\"Green action: {info['agent_actions']['client_1_green_user']}\" )\n", - "print(f\"Blue reward:{reward}\" )" + "print(f\"Blue reward:{reward:.2f}\" )" ] }, { @@ -538,29 +890,69 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 53, Red action: DONOTHING, Blue reward:-0.80\n", + "step: 54, Red action: DONOTHING, Blue reward:-0.80\n", + "step: 55, Red action: DONOTHING, Blue reward:-0.80\n", + "step: 56, Red action: DONOTHING, Blue reward:0.54\n", + "step: 57, Red action: DONOTHING, Blue reward:1.20\n", + "step: 58, Red action: DONOTHING, Blue reward:1.20\n", + "step: 59, Red action: DONOTHING, Blue reward:1.20\n", + "step: 60, Red action: DONOTHING, Blue reward:1.20\n", + "step: 61, Red action: DONOTHING, Blue reward:1.20\n", + "step: 62, Red action: DONOTHING, Blue reward:1.20\n", + "step: 63, Red action: DONOTHING, Blue reward:1.20\n", + "step: 64, Red action: DONOTHING, Blue reward:1.20\n", + "step: 65, Red action: DONOTHING, Blue reward:1.00\n", + "step: 66, Red action: DONOTHING, Blue reward:1.00\n", + "step: 67, Red action: DONOTHING, Blue reward:1.00\n", + "step: 68, Red action: DONOTHING, Blue reward:1.00\n", + "step: 69, Red action: DONOTHING, Blue reward:1.00\n", + "step: 70, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.00\n", + "step: 71, Red action: DONOTHING, Blue reward:1.00\n", + "step: 72, Red action: DONOTHING, Blue reward:1.00\n", + "step: 73, Red action: DONOTHING, Blue reward:1.00\n", + "step: 74, Red action: DONOTHING, Blue reward:1.00\n", + "step: 75, Red action: DONOTHING, Blue reward:0.80\n", + "step: 76, Red action: DONOTHING, Blue reward:0.80\n", + "step: 77, Red action: DONOTHING, Blue reward:0.80\n", + "step: 78, Red action: DONOTHING, Blue reward:0.80\n", + "step: 79, Red action: DONOTHING, Blue reward:0.80\n", + "step: 80, Red action: DONOTHING, Blue reward:0.80\n", + "step: 81, Red action: DONOTHING, Blue reward:0.80\n", + "step: 82, Red action: DONOTHING, Blue reward:0.80\n", + "step: 83, Red action: DONOTHING, Blue reward:0.80\n", + "step: 84, Red action: DONOTHING, Blue reward:0.80\n", + "step: 85, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.80\n" + ] + } + ], "source": [ "env.step(13) # Patch the database\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", "\n", "env.step(26) # Block client 1\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", "\n", "env.step(27) # Block client 2\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", "\n", "for step in range(30):\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, even though the red agent executes an attack, the reward stays at 1.0" + "Now, even though the red agent executes an attack, the reward stays at 0.8." ] }, { @@ -572,11 +964,168 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 2,\n", + " 'source_node_id': 7,\n", + " 'source_port': 1,\n", + " 'dest_node_id': 4,\n", + " 'dest_port': 1,\n", + " 'protocol': 3},\n", + " 6: {'position': 5,\n", + " 'permission': 2,\n", + " 'source_node_id': 8,\n", + " 'source_port': 1,\n", + " 'dest_node_id': 4,\n", + " 'dest_port': 1,\n", + " 'protocol': 3},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0}}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "obs['ACL']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can slightly increase the reward by unblocking the client which isn't being used by the attacker. If node 6 has outbound NMNEs, let's unblock client 2, and if node 7 has outbound NMNEs, let's unblock client 1." + ] + }, + { + "cell_type": "code", + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ - "obs['ACL']" + "if obs['NODES'][6]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", + " # client 1 has NMNEs, let's unblock client 2\n", + " env.step(34) # remove ACL rule 6\n", + "elif obs['NODES'][7]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", + " env.step(33) # remove ACL rule 5\n", + "else:\n", + " print(\"something went wrong, neither client has NMNEs\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, the reward will eventually increase to 1.0, even after red agent attempts subsequent attacks." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 117, Red action: DONOTHING, Blue reward:1.00\n", + "step: 118, Red action: DONOTHING, Blue reward:1.00\n", + "step: 119, Red action: DONOTHING, Blue reward:1.00\n", + "step: 120, Red action: DONOTHING, Blue reward:1.00\n", + "step: 121, Red action: DONOTHING, Blue reward:1.00\n", + "step: 122, Red action: DONOTHING, Blue reward:1.00\n", + "step: 123, Red action: DONOTHING, Blue reward:1.00\n", + "step: 124, Red action: DONOTHING, Blue reward:1.00\n", + "step: 125, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.00\n", + "step: 126, Red action: DONOTHING, Blue reward:1.00\n", + "step: 127, Red action: DONOTHING, Blue reward:1.00\n", + "step: 128, Red action: DONOTHING, Blue reward:1.00\n", + "step: 129, Red action: DONOTHING, Blue reward:1.00\n", + "step: 130, Red action: DONOTHING, Blue reward:1.00\n", + "step: 131, Red action: DONOTHING, Blue reward:1.00\n", + "step: 132, Red action: DONOTHING, Blue reward:1.00\n", + "step: 133, Red action: DONOTHING, Blue reward:1.00\n", + "step: 134, Red action: DONOTHING, Blue reward:1.00\n", + "step: 135, Red action: DONOTHING, Blue reward:1.00\n", + "step: 136, Red action: DONOTHING, Blue reward:1.00\n", + "step: 137, Red action: DONOTHING, Blue reward:1.00\n", + "step: 138, Red action: DONOTHING, Blue reward:1.00\n", + "step: 139, Red action: DONOTHING, Blue reward:1.00\n", + "step: 140, Red action: DONOTHING, Blue reward:1.00\n", + "step: 141, Red action: DONOTHING, Blue reward:1.00\n", + "step: 142, Red action: DONOTHING, Blue reward:1.00\n", + "step: 143, Red action: DONOTHING, Blue reward:1.00\n", + "step: 144, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.00\n", + "step: 145, Red action: DONOTHING, Blue reward:1.00\n", + "step: 146, Red action: DONOTHING, Blue reward:1.00\n" + ] + } + ], + "source": [ + "for step in range(30):\n", + " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )" ] }, { @@ -594,13 +1143,6 @@ "source": [ "env.reset()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 070655cfce3da94db274377d404beb5d86955a8b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 11:47:50 +0000 Subject: [PATCH 060/128] Update data manipulation bot documentation --- .../system/applications/data_manipulation_bot.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 304621dd..9188733b 100644 --- a/docs/source/simulation_components/system/applications/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/applications/data_manipulation_bot.rst @@ -45,7 +45,7 @@ In a simulation, the bot can be controlled by using ``DataManipulationAgent`` wh Implementation ============== -The bot extends :ref:`DatabaseClient` and leverages its connectivity. +The bot connects to a :ref:`DatabaseClient` and leverages its connectivity. The host running ``DataManipulationBot`` must also have a :ref:`DatabaseClient` installed on it. - Uses the Application base class for lifecycle management. - Credentials, target IP and other options set via ``configure``. @@ -65,6 +65,7 @@ Python from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot + from primaite.simulator.system.applications.database_client import DatabaseClient client_1 = Computer( hostname="client_1", @@ -74,6 +75,7 @@ Python operating_state=NodeOperatingState.ON # initialise the computer in an ON state ) network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) + client_1.software_manager.install(DatabaseClient) client_1.software_manager.install(DataManipulationBot) data_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") data_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE") @@ -148,6 +150,10 @@ If not using the data manipulation bot manually, it needs to be used with a data data_manipulation_p_of_success: 0.1 payload: "DELETE" server_ip: 192.168.1.14 + - ref: web_server_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 Configuration ============= From 4d51b1a4146bb861f80101f4b013df093395a1b2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 14:57:28 +0000 Subject: [PATCH 061/128] Update configs to new db manipulation bot approach --- src/primaite/simulator/network/networks.py | 63 ++----------------- .../red_applications/data_manipulation_bot.py | 4 ++ tests/assets/configs/basic_firewall.yaml | 2 +- .../test_data_manipulation_bot.py | 15 ++++- 4 files changed, 23 insertions(+), 61 deletions(-) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index f82dee4a..fa9d86ef 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -146,6 +146,9 @@ def arcd_uc2_network() -> Network: ) client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) + db_client_1 = client_1.software_manager.install(DatabaseClient) + db_client_1 = client_1.software_manager.software.get("DatabaseClient") + db_client_1.run() client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") db_manipulation_bot.configure( @@ -165,6 +168,9 @@ def arcd_uc2_network() -> Network: start_up_duration=0, ) client_2.power_on() + client_2.software_manager.install(DatabaseClient) + db_client_2 = client_2.software_manager.software.get("DatabaseClient") + db_client_2.run() web_browser = client_2.software_manager.software.get("WebBrowser") web_browser.target_url = "http://arcd.com/users/" network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.network_interface[2]) @@ -194,67 +200,10 @@ def arcd_uc2_network() -> Network: database_server.power_on() network.connect(endpoint_b=database_server.network_interface[1], endpoint_a=switch_1.network_interface[3]) - ddl = """ - CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(50) NOT NULL, - email VARCHAR(50) NOT NULL, - age INT, - city VARCHAR(50), - occupation VARCHAR(50) - );""" - - user_insert_statements = [ - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('John Doe', 'johndoe@example.com', 32, 'New York', 'Engineer');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Alice Lee', 'alicelee@example.com', 22, 'San Francisco', 'Student');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('David Kim', 'davidkim@example.com', 38, 'Houston', 'Consultant');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Emily Chen', 'emilychen@example.com', 29, 'Seattle', 'Software Developer');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Frank Wang', 'frankwang@example.com', 55, 'New York', 'Entrepreneur');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Grace Park', 'gracepark@example.com', 31, 'Los Angeles', 'Marketing Specialist');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Henry Wu', 'henrywu@example.com', 40, 'Chicago', 'Accountant');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Isabella Kim', 'isabellakim@example.com', 26, 'San Francisco', 'Graphic Designer');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Jake Lee', 'jakelee@example.com', 33, 'Houston', 'Sales Manager');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Kelly Chen', 'kellychen@example.com', 28, 'Seattle', 'Web Developer');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Lucas Liu', 'lucasliu@example.com', 42, 'New York', 'Lawyer');", - # noqa - "INSERT INTO user (name, email, age, city, occupation) " - "VALUES ('Maggie Wang', 'maggiewang@example.com', 30, 'Los Angeles', 'Data Analyst');", - # noqa - ] database_server.software_manager.install(DatabaseService) database_service: DatabaseService = database_server.software_manager.software.get("DatabaseService") # noqa database_service.start() database_service.configure_backup(backup_server=IPv4Address("192.168.1.16")) - database_service._process_sql(ddl, None, None) # noqa - for insert_statement in user_insert_statements: - database_service._process_sql(insert_statement, None, None) # noqa # Web Server web_server = Server( diff --git a/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py b/src/primaite/simulator/system/applications/red_applications/data_manipulation_bot.py index 11eb71f5..961f82c2 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 @@ -149,6 +149,10 @@ class DataManipulationBot(Application): :param p_of_success: Probability of successfully performing data manipulation, by default 0.1. """ + if self._host_db_client is None: + self.attack_stage = DataManipulationAttackStage.FAILED + return + self._host_db_client.server_ip_address = self.server_ip_address self._host_db_client.server_password = self.server_password if self.attack_stage == DataManipulationAttackStage.PORT_SCAN: diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 71dc31a7..0a892650 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -40,7 +40,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: GreenWebBrowsingAgent + type: probabilistic_agent observation_space: type: UC2GreenObservation action_space: 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 2ca67119..6d00886a 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 @@ -26,8 +26,8 @@ def test_create_dm_bot(dm_client): data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software.get("DataManipulationBot") assert data_manipulation_bot.name == "DataManipulationBot" - assert data_manipulation_bot.port == Port.POSTGRES_SERVER - assert data_manipulation_bot.protocol == IPProtocol.TCP + assert data_manipulation_bot.port == Port.NONE + assert data_manipulation_bot.protocol == IPProtocol.NONE assert data_manipulation_bot.payload == "DELETE" @@ -70,4 +70,13 @@ def test_dm_bot_perform_data_manipulation_success(dm_bot): dm_bot._perform_data_manipulation(p_of_success=1.0) assert dm_bot.attack_stage in (DataManipulationAttackStage.SUCCEEDED, DataManipulationAttackStage.FAILED) - assert len(dm_bot.connections) + assert len(dm_bot._host_db_client.connections) + + +def test_dm_bot_fails_without_db_client(dm_client): + dm_client.software_manager.uninstall("DatabaseClient") + dm_bot = dm_client.software_manager.software.get("DataManipulationBot") + assert dm_bot._host_db_client is None + dm_bot.attack_stage = DataManipulationAttackStage.PORT_SCAN + dm_bot._perform_data_manipulation(p_of_success=1.0) + assert dm_bot.attack_stage is DataManipulationAttackStage.FAILED From afa775baff03a7754ff0818934563f9587a2a1cb Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 15:52:34 +0000 Subject: [PATCH 062/128] Add test for new reward --- .../game_layer/test_rewards.py | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index fd8a89a4..53753967 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,7 +1,10 @@ -from primaite.game.agent.rewards import WebpageUnavailablePenalty +from primaite.game.agent.rewards import GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty +from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.services.database.database_service import DatabaseService from tests.conftest import ControlledAgent @@ -35,3 +38,44 @@ def test_WebpageUnavailablePenalty(game_and_agent): agent.store_action(action) game.step() assert agent.reward_function.current_reward == -0.7 + + +def test_uc2_rewards(game_and_agent): + game, agent = game_and_agent + agent: ControlledAgent + + server_1: Server = game.simulation.network.get_node_by_hostname("server_1") + server_1.software_manager.install(DatabaseService) + db_service = server_1.software_manager.software.get("DatabaseService") + db_service.start() + + client_1 = game.simulation.network.get_node_by_hostname("client_1") + client_1.software_manager.install(DatabaseClient) + db_client: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client.configure(server_ip_address=server_1.network_interface[1].ip_address) + db_client.run() + + router: Router = game.simulation.network.get_node_by_hostname("router") + router.acl.add_rule(ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=2) + + comp = GreenAdminDatabaseUnreachablePenalty("client_1") + + db_client.apply_request( + [ + "execute", + ] + ) + state = game.get_sim_state() + reward_value = comp.calculate(state) + assert reward_value == 1.0 + + router.acl.remove_rule(position=2) + + db_client.apply_request( + [ + "execute", + ] + ) + state = game.get_sim_state() + reward_value = comp.calculate(state) + assert reward_value == -1.0 From ef1a2dc3f4635db875d7970ba859dce7bbc021df Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 16:00:10 +0000 Subject: [PATCH 063/128] clear uc2 notebook outputs --- src/primaite/notebooks/uc2_demo.ipynb | 539 ++------------------------ 1 file changed, 22 insertions(+), 517 deletions(-) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index 36942b73..94be8baa 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -352,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, @@ -364,7 +364,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, @@ -389,169 +389,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Resetting environment, episode 0, avg. reward: 0.0\n", - "env created successfully\n", - "{'ACL': {1: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 0,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 2: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 1,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 3: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 2,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 4: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 3,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 5: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 4,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 6: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 5,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 7: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 6,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 8: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 7,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 9: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 8,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 10: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 9,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0}},\n", - " 'ICS': 0,\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 1}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", - " 'health_status': 1}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0,\n", - " 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}}\n" - ] - } - ], + "outputs": [], "source": [ "# create the env\n", "with open(example_config_path(), 'r') as f:\n", @@ -579,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -597,51 +437,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 1, Red action: DO NOTHING, Blue reward:0.77\n", - "step: 2, Red action: DO NOTHING, Blue reward:0.77\n", - "step: 3, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 4, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 5, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 6, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 7, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 8, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 9, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 10, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 11, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 12, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 13, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 14, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 15, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 16, Red action: DO NOTHING, Blue reward:1.10\n", - "step: 17, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 18, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 19, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 20, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 21, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 22, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 23, Red action: DO NOTHING, Blue reward:1.20\n", - "step: 24, Red action: ATTACK from client 2, Blue reward:0.52\n", - "step: 25, Red action: DO NOTHING, Blue reward:0.52\n", - "step: 26, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 27, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 28, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 29, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 30, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 31, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 32, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 33, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 34, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 35, Red action: DO NOTHING, Blue reward:-0.80\n" - ] - } - ], + "outputs": [], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -657,65 +455,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 1, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 1}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}\n" - ] - } - ], + "outputs": [], "source": [ "pprint(obs['NODES'])" ] @@ -729,65 +471,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 1, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NETWORK_INTERFACES': {1: {'nic_status': 1,\n", - " 'nmne': {'inbound': 0, 'outbound': 1}},\n", - " 2: {'nic_status': 0,\n", - " 'nmne': {'inbound': 0, 'outbound': 0}}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -818,21 +504,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 38\n", - "Red action: DONOTHING\n", - "Green action: DONOTHING\n", - "Green action: DONOTHING\n", - "Blue reward:-0.8\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -855,21 +529,9 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 52\n", - "Red action: DONOTHING\n", - "Green action: ('NODE_APPLICATION_EXECUTE', {'node_id': 0, 'application_id': 0})\n", - "Green action: ('NODE_APPLICATION_EXECUTE', {'node_id': 0, 'application_id': 0})\n", - "Blue reward:-0.80\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -890,49 +552,9 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 53, Red action: DONOTHING, Blue reward:-0.80\n", - "step: 54, Red action: DONOTHING, Blue reward:-0.80\n", - "step: 55, Red action: DONOTHING, Blue reward:-0.80\n", - "step: 56, Red action: DONOTHING, Blue reward:0.54\n", - "step: 57, Red action: DONOTHING, Blue reward:1.20\n", - "step: 58, Red action: DONOTHING, Blue reward:1.20\n", - "step: 59, Red action: DONOTHING, Blue reward:1.20\n", - "step: 60, Red action: DONOTHING, Blue reward:1.20\n", - "step: 61, Red action: DONOTHING, Blue reward:1.20\n", - "step: 62, Red action: DONOTHING, Blue reward:1.20\n", - "step: 63, Red action: DONOTHING, Blue reward:1.20\n", - "step: 64, Red action: DONOTHING, Blue reward:1.20\n", - "step: 65, Red action: DONOTHING, Blue reward:1.00\n", - "step: 66, Red action: DONOTHING, Blue reward:1.00\n", - "step: 67, Red action: DONOTHING, Blue reward:1.00\n", - "step: 68, Red action: DONOTHING, Blue reward:1.00\n", - "step: 69, Red action: DONOTHING, Blue reward:1.00\n", - "step: 70, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.00\n", - "step: 71, Red action: DONOTHING, Blue reward:1.00\n", - "step: 72, Red action: DONOTHING, Blue reward:1.00\n", - "step: 73, Red action: DONOTHING, Blue reward:1.00\n", - "step: 74, Red action: DONOTHING, Blue reward:1.00\n", - "step: 75, Red action: DONOTHING, Blue reward:0.80\n", - "step: 76, Red action: DONOTHING, Blue reward:0.80\n", - "step: 77, Red action: DONOTHING, Blue reward:0.80\n", - "step: 78, Red action: DONOTHING, Blue reward:0.80\n", - "step: 79, Red action: DONOTHING, Blue reward:0.80\n", - "step: 80, Red action: DONOTHING, Blue reward:0.80\n", - "step: 81, Red action: DONOTHING, Blue reward:0.80\n", - "step: 82, Red action: DONOTHING, Blue reward:0.80\n", - "step: 83, Red action: DONOTHING, Blue reward:0.80\n", - "step: 84, Red action: DONOTHING, Blue reward:0.80\n", - "step: 85, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.80\n" - ] - } - ], + "outputs": [], "source": [ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", @@ -964,89 +586,9 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 2,\n", - " 'source_node_id': 7,\n", - " 'source_port': 1,\n", - " 'dest_node_id': 4,\n", - " 'dest_port': 1,\n", - " 'protocol': 3},\n", - " 6: {'position': 5,\n", - " 'permission': 2,\n", - " 'source_node_id': 8,\n", - " 'source_port': 1,\n", - " 'dest_node_id': 4,\n", - " 'dest_port': 1,\n", - " 'protocol': 3},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0}}" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "obs['ACL']" ] @@ -1060,7 +602,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -1082,46 +624,9 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 117, Red action: DONOTHING, Blue reward:1.00\n", - "step: 118, Red action: DONOTHING, Blue reward:1.00\n", - "step: 119, Red action: DONOTHING, Blue reward:1.00\n", - "step: 120, Red action: DONOTHING, Blue reward:1.00\n", - "step: 121, Red action: DONOTHING, Blue reward:1.00\n", - "step: 122, Red action: DONOTHING, Blue reward:1.00\n", - "step: 123, Red action: DONOTHING, Blue reward:1.00\n", - "step: 124, Red action: DONOTHING, Blue reward:1.00\n", - "step: 125, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.00\n", - "step: 126, Red action: DONOTHING, Blue reward:1.00\n", - "step: 127, Red action: DONOTHING, Blue reward:1.00\n", - "step: 128, Red action: DONOTHING, Blue reward:1.00\n", - "step: 129, Red action: DONOTHING, Blue reward:1.00\n", - "step: 130, Red action: DONOTHING, Blue reward:1.00\n", - "step: 131, Red action: DONOTHING, Blue reward:1.00\n", - "step: 132, Red action: DONOTHING, Blue reward:1.00\n", - "step: 133, Red action: DONOTHING, Blue reward:1.00\n", - "step: 134, Red action: DONOTHING, Blue reward:1.00\n", - "step: 135, Red action: DONOTHING, Blue reward:1.00\n", - "step: 136, Red action: DONOTHING, Blue reward:1.00\n", - "step: 137, Red action: DONOTHING, Blue reward:1.00\n", - "step: 138, Red action: DONOTHING, Blue reward:1.00\n", - "step: 139, Red action: DONOTHING, Blue reward:1.00\n", - "step: 140, Red action: DONOTHING, Blue reward:1.00\n", - "step: 141, Red action: DONOTHING, Blue reward:1.00\n", - "step: 142, Red action: DONOTHING, Blue reward:1.00\n", - "step: 143, Red action: DONOTHING, Blue reward:1.00\n", - "step: 144, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.00\n", - "step: 145, Red action: DONOTHING, Blue reward:1.00\n", - "step: 146, Red action: DONOTHING, Blue reward:1.00\n" - ] - } - ], + "outputs": [], "source": [ "for step in range(30):\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", From a6031d568d175a56a572f7e321cd29da2aad04a5 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 16:36:08 +0000 Subject: [PATCH 064/128] Remove unused import --- .../simulator/system/applications/red_applications/dos_bot.py | 1 - 1 file changed, 1 deletion(-) 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 1247bc99..202fd189 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -6,7 +6,6 @@ from primaite import getLogger from primaite.game.science import simulate_trial from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient _LOGGER = getLogger(__name__) From 0e8c60df4c8eb3f0727c4ef0b224c909d783a707 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 16:53:18 +0000 Subject: [PATCH 065/128] Update actions --- .../config/_package_data/example_config.yaml | 244 ++++++++++++------ src/primaite/notebooks/uc2_demo.ipynb | 26 +- 2 files changed, 184 insertions(+), 86 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 478124a9..b906bba8 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -238,99 +238,196 @@ agents: 3: action: "NODE_SERVICE_START" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 4: action: "NODE_SERVICE_PAUSE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 5: action: "NODE_SERVICE_RESUME" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 6: action: "NODE_SERVICE_RESTART" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 7: action: "NODE_SERVICE_DISABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 8: action: "NODE_SERVICE_ENABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 9: # check database.db file action: "NODE_FILE_SCAN" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 10: action: "NODE_FILE_CHECKHASH" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 11: action: "NODE_FILE_DELETE" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 12: action: "NODE_FILE_REPAIR" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 13: action: "NODE_SERVICE_PATCH" options: - node_id: 2 - service_id: 0 + node_id: 2 + service_id: 0 14: action: "NODE_FOLDER_SCAN" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 15: action: "NODE_FOLDER_CHECKHASH" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 18: action: "NODE_OS_SCAN" options: - node_id: 2 - 19: # shutdown client 1 + node_id: 0 + 19: action: "NODE_SHUTDOWN" options: - node_id: 5 + node_id: 0 20: - action: "NODE_STARTUP" + action: NODE_STARTUP options: - node_id: 5 + node_id: 0 21: - action: "NODE_RESET" + action: NODE_RESET options: - node_id: 5 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" action: "NETWORK_ACL_ADDRULE" options: position: 1 @@ -340,7 +437,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" action: "NETWORK_ACL_ADDRULE" options: position: 2 @@ -350,7 +447,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 24: # block tcp traffic from client 1 to web app + 48: # old action num: 24 # block tcp traffic from client 1 to web app action: "NETWORK_ACL_ADDRULE" options: position: 3 @@ -360,7 +457,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 25: # block tcp traffic from client 2 to web app + 49: # old action num: 25 # block tcp traffic from client 2 to web app action: "NETWORK_ACL_ADDRULE" options: position: 4 @@ -370,7 +467,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 26: + 50: # old action num: 26 action: "NETWORK_ACL_ADDRULE" options: position: 5 @@ -380,7 +477,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 27: + 51: # old action num: 27 action: "NETWORK_ACL_ADDRULE" options: position: 6 @@ -390,128 +487,129 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 28: + 52: # old action num: 28 action: "NETWORK_ACL_REMOVERULE" options: position: 0 - 29: + 53: # old action num: 29 action: "NETWORK_ACL_REMOVERULE" options: position: 1 - 30: + 54: # old action num: 30 action: "NETWORK_ACL_REMOVERULE" options: position: 2 - 31: + 55: # old action num: 31 action: "NETWORK_ACL_REMOVERULE" options: position: 3 - 32: + 56: # old action num: 32 action: "NETWORK_ACL_REMOVERULE" options: position: 4 - 33: + 57: # old action num: 33 action: "NETWORK_ACL_REMOVERULE" options: position: 5 - 34: + 58: # old action num: 34 action: "NETWORK_ACL_REMOVERULE" options: position: 6 - 35: + 59: # old action num: 35 action: "NETWORK_ACL_REMOVERULE" options: position: 7 - 36: + 60: # old action num: 36 action: "NETWORK_ACL_REMOVERULE" options: position: 8 - 37: + 61: # old action num: 37 action: "NETWORK_ACL_REMOVERULE" options: position: 9 - 38: + 62: # old action num: 38 action: "NETWORK_NIC_DISABLE" options: node_id: 0 nic_id: 0 - 39: + 63: # old action num: 39 action: "NETWORK_NIC_ENABLE" options: node_id: 0 nic_id: 0 - 40: + 64: # old action num: 40 action: "NETWORK_NIC_DISABLE" options: node_id: 1 nic_id: 0 - 41: + 65: # old action num: 41 action: "NETWORK_NIC_ENABLE" options: node_id: 1 nic_id: 0 - 42: + 66: # old action num: 42 action: "NETWORK_NIC_DISABLE" options: node_id: 2 nic_id: 0 - 43: + 67: # old action num: 43 action: "NETWORK_NIC_ENABLE" options: node_id: 2 nic_id: 0 - 44: + 68: # old action num: 44 action: "NETWORK_NIC_DISABLE" options: node_id: 3 nic_id: 0 - 45: + 69: # old action num: 45 action: "NETWORK_NIC_ENABLE" options: node_id: 3 nic_id: 0 - 46: + 70: # old action num: 46 action: "NETWORK_NIC_DISABLE" options: node_id: 4 nic_id: 0 - 47: + 71: # old action num: 47 action: "NETWORK_NIC_ENABLE" options: node_id: 4 nic_id: 0 - 48: + 72: # old action num: 48 action: "NETWORK_NIC_DISABLE" options: node_id: 4 nic_id: 1 - 49: + 73: # old action num: 49 action: "NETWORK_NIC_ENABLE" options: node_id: 4 nic_id: 1 - 50: + 74: # old action num: 50 action: "NETWORK_NIC_DISABLE" options: node_id: 5 nic_id: 0 - 51: + 75: # old action num: 51 action: "NETWORK_NIC_ENABLE" options: node_id: 5 nic_id: 0 - 52: + 76: # old action num: 52 action: "NETWORK_NIC_DISABLE" options: node_id: 6 nic_id: 0 - 53: + 77: # old action num: 53 action: "NETWORK_NIC_ENABLE" options: node_id: 6 nic_id: 0 + options: nodes: - node_name: domain_controller diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb index c8f2595b..ca06ea8a 100644 --- a/src/primaite/notebooks/uc2_demo.ipynb +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -301,17 +301,17 @@ "- `1`: Scan the web service - this refreshes the health status in the observation space\n", "- `9`: Scan the database file - this refreshes the health status of the database file\n", "- `13`: Patch the database service - This triggers the database to restore data from the backup server\n", - "- `19`: Shut down client 1\n", - "- `20`: Start up client 1\n", - "- `22`: Block outgoing traffic from client 1\n", - "- `23`: Block outgoing traffic from client 2\n", - "- `26`: Block TCP traffic from client 1 to the database node\n", - "- `27`: Block TCP traffic from client 2 to the database node\n", - "- `28-37`: Remove ACL rules 1-10\n", - "- `42`: Disconnect client 1 from the network\n", - "- `43`: Reconnect client 1 to the network\n", - "- `44`: Disconnect client 2 from the network\n", - "- `45`: Reconnect client 2 to the network\n", + "- `39`: Shut down client 1\n", + "- `40`: Start up client 1\n", + "- `46`: Block outgoing traffic from client 1\n", + "- `47`: Block outgoing traffic from client 2\n", + "- `50`: Block TCP traffic from client 1 to the database node\n", + "- `51`: Block TCP traffic from client 2 to the database node\n", + "- `52-61`: Remove ACL rules 1-10\n", + "- `66`: Disconnect client 1 from the network\n", + "- `67`: Reconnect client 1 to the network\n", + "- `68`: Disconnect client 2 from the network\n", + "- `69`: Reconnect client 2 to the network\n", "\n", "The other actions will either have no effect or will negatively impact the network, so the blue agent should avoid taking them." ] @@ -541,10 +541,10 @@ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", "\n", - "env.step(26) # Block client 1\n", + "env.step(50) # Block client 1\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", "\n", - "env.step(27) # Block client 2\n", + "env.step(51) # Block client 2\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward}\" )\n", "\n", "for step in range(30):\n", From afc3635bfe3cac06fdc4949bca0262c89cc5a1d1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 16:56:52 +0000 Subject: [PATCH 066/128] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d54af980..5416bb9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ SessionManager. - **ACLRule Wildcard Masking**: Updated the `ACLRule` class to support IP ranges using wildcard masking. This enhancement allows for more flexible and granular control over traffic filtering, enabling the specification of broader or more specific IP address ranges in ACL rules. - Updated `NetworkInterface` documentation to reflect the new NMNE capturing features and how to use them. - Integration of NMNE capturing functionality within the `NicObservation` class. +- Changed blue action set to enable applying node scan, reset, start, and shutdown to every host in data manipulation scenario ### Removed - Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol` From a4c723858b0d02430ebebeef4f6393707f508d2d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 16:57:53 +0000 Subject: [PATCH 067/128] Update action map in second --- .../example_config_2_rl_agents.yaml | 487 ++++++++++++------ 1 file changed, 341 insertions(+), 146 deletions(-) diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index b6b07afa..fd5a3092 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -240,99 +240,196 @@ agents: 3: action: "NODE_SERVICE_START" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 4: action: "NODE_SERVICE_PAUSE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 5: action: "NODE_SERVICE_RESUME" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 6: action: "NODE_SERVICE_RESTART" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 7: action: "NODE_SERVICE_DISABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 8: action: "NODE_SERVICE_ENABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 9: # check database.db file action: "NODE_FILE_SCAN" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 10: action: "NODE_FILE_CHECKHASH" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 11: action: "NODE_FILE_DELETE" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 12: action: "NODE_FILE_REPAIR" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 13: action: "NODE_SERVICE_PATCH" options: - node_id: 2 - service_id: 0 + node_id: 2 + service_id: 0 14: action: "NODE_FOLDER_SCAN" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 15: action: "NODE_FOLDER_CHECKHASH" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 18: action: "NODE_OS_SCAN" options: - node_id: 2 - 19: # shutdown client 1 + node_id: 0 + 19: action: "NODE_SHUTDOWN" options: - node_id: 5 + node_id: 0 20: - action: "NODE_STARTUP" + action: NODE_STARTUP options: - node_id: 5 + node_id: 0 21: - action: "NODE_RESET" + action: NODE_RESET options: - node_id: 5 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" action: "NETWORK_ACL_ADDRULE" options: position: 1 @@ -342,7 +439,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" action: "NETWORK_ACL_ADDRULE" options: position: 2 @@ -352,7 +449,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 24: # block tcp traffic from client 1 to web app + 48: # old action num: 24 # block tcp traffic from client 1 to web app action: "NETWORK_ACL_ADDRULE" options: position: 3 @@ -362,7 +459,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 25: # block tcp traffic from client 2 to web app + 49: # old action num: 25 # block tcp traffic from client 2 to web app action: "NETWORK_ACL_ADDRULE" options: position: 4 @@ -372,7 +469,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 26: + 50: # old action num: 26 action: "NETWORK_ACL_ADDRULE" options: position: 5 @@ -382,7 +479,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 27: + 51: # old action num: 27 action: "NETWORK_ACL_ADDRULE" options: position: 6 @@ -392,122 +489,122 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 28: + 52: # old action num: 28 action: "NETWORK_ACL_REMOVERULE" options: position: 0 - 29: + 53: # old action num: 29 action: "NETWORK_ACL_REMOVERULE" options: position: 1 - 30: + 54: # old action num: 30 action: "NETWORK_ACL_REMOVERULE" options: position: 2 - 31: + 55: # old action num: 31 action: "NETWORK_ACL_REMOVERULE" options: position: 3 - 32: + 56: # old action num: 32 action: "NETWORK_ACL_REMOVERULE" options: position: 4 - 33: + 57: # old action num: 33 action: "NETWORK_ACL_REMOVERULE" options: position: 5 - 34: + 58: # old action num: 34 action: "NETWORK_ACL_REMOVERULE" options: position: 6 - 35: + 59: # old action num: 35 action: "NETWORK_ACL_REMOVERULE" options: position: 7 - 36: + 60: # old action num: 36 action: "NETWORK_ACL_REMOVERULE" options: position: 8 - 37: + 61: # old action num: 37 action: "NETWORK_ACL_REMOVERULE" options: position: 9 - 38: + 62: # old action num: 38 action: "NETWORK_NIC_DISABLE" options: node_id: 0 nic_id: 0 - 39: + 63: # old action num: 39 action: "NETWORK_NIC_ENABLE" options: node_id: 0 nic_id: 0 - 40: + 64: # old action num: 40 action: "NETWORK_NIC_DISABLE" options: node_id: 1 nic_id: 0 - 41: + 65: # old action num: 41 action: "NETWORK_NIC_ENABLE" options: node_id: 1 nic_id: 0 - 42: + 66: # old action num: 42 action: "NETWORK_NIC_DISABLE" options: node_id: 2 nic_id: 0 - 43: + 67: # old action num: 43 action: "NETWORK_NIC_ENABLE" options: node_id: 2 nic_id: 0 - 44: + 68: # old action num: 44 action: "NETWORK_NIC_DISABLE" options: node_id: 3 nic_id: 0 - 45: + 69: # old action num: 45 action: "NETWORK_NIC_ENABLE" options: node_id: 3 nic_id: 0 - 46: + 70: # old action num: 46 action: "NETWORK_NIC_DISABLE" options: node_id: 4 nic_id: 0 - 47: + 71: # old action num: 47 action: "NETWORK_NIC_ENABLE" options: node_id: 4 nic_id: 0 - 48: + 72: # old action num: 48 action: "NETWORK_NIC_DISABLE" options: node_id: 4 nic_id: 1 - 49: + 73: # old action num: 49 action: "NETWORK_NIC_ENABLE" options: node_id: 4 nic_id: 1 - 50: + 74: # old action num: 50 action: "NETWORK_NIC_DISABLE" options: node_id: 5 nic_id: 0 - 51: + 75: # old action num: 51 action: "NETWORK_NIC_ENABLE" options: node_id: 5 nic_id: 0 - 52: + 76: # old action num: 52 action: "NETWORK_NIC_DISABLE" options: node_id: 6 nic_id: 0 - 53: + 77: # old action num: 53 action: "NETWORK_NIC_ENABLE" options: node_id: 6 @@ -694,99 +791,196 @@ agents: 3: action: "NODE_SERVICE_START" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 4: action: "NODE_SERVICE_PAUSE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 5: action: "NODE_SERVICE_RESUME" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 6: action: "NODE_SERVICE_RESTART" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 7: action: "NODE_SERVICE_DISABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 8: action: "NODE_SERVICE_ENABLE" options: - node_id: 1 - service_id: 0 + node_id: 1 + service_id: 0 9: # check database.db file action: "NODE_FILE_SCAN" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 10: action: "NODE_FILE_CHECKHASH" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 11: action: "NODE_FILE_DELETE" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 12: action: "NODE_FILE_REPAIR" options: - node_id: 2 - folder_id: 0 - file_id: 0 + node_id: 2 + folder_id: 0 + file_id: 0 13: action: "NODE_SERVICE_PATCH" options: - node_id: 2 - service_id: 0 + node_id: 2 + service_id: 0 14: action: "NODE_FOLDER_SCAN" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 15: action: "NODE_FOLDER_CHECKHASH" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 16: action: "NODE_FOLDER_REPAIR" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 17: action: "NODE_FOLDER_RESTORE" options: - node_id: 2 - folder_id: 0 + node_id: 2 + folder_id: 0 18: action: "NODE_OS_SCAN" options: - node_id: 2 - 19: # shutdown client 1 + node_id: 0 + 19: action: "NODE_SHUTDOWN" options: - node_id: 5 + node_id: 0 20: - action: "NODE_STARTUP" + action: NODE_STARTUP options: - node_id: 5 + node_id: 0 21: - action: "NODE_RESET" + action: NODE_RESET options: - node_id: 5 - 22: # "ACL: ADDRULE - Block outgoing traffic from client 1" + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" action: "NETWORK_ACL_ADDRULE" options: position: 1 @@ -796,7 +990,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 23: # "ACL: ADDRULE - Block outgoing traffic from client 2" + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" action: "NETWORK_ACL_ADDRULE" options: position: 2 @@ -806,7 +1000,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 1 - 24: # block tcp traffic from client 1 to web app + 48: # old action num: 24 # block tcp traffic from client 1 to web app action: "NETWORK_ACL_ADDRULE" options: position: 3 @@ -816,7 +1010,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 25: # block tcp traffic from client 2 to web app + 49: # old action num: 25 # block tcp traffic from client 2 to web app action: "NETWORK_ACL_ADDRULE" options: position: 4 @@ -826,7 +1020,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 26: + 50: # old action num: 26 action: "NETWORK_ACL_ADDRULE" options: position: 5 @@ -836,7 +1030,7 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 27: + 51: # old action num: 27 action: "NETWORK_ACL_ADDRULE" options: position: 6 @@ -846,128 +1040,129 @@ agents: source_port_id: 1 dest_port_id: 1 protocol_id: 3 - 28: + 52: # old action num: 28 action: "NETWORK_ACL_REMOVERULE" options: position: 0 - 29: + 53: # old action num: 29 action: "NETWORK_ACL_REMOVERULE" options: position: 1 - 30: + 54: # old action num: 30 action: "NETWORK_ACL_REMOVERULE" options: position: 2 - 31: + 55: # old action num: 31 action: "NETWORK_ACL_REMOVERULE" options: position: 3 - 32: + 56: # old action num: 32 action: "NETWORK_ACL_REMOVERULE" options: position: 4 - 33: + 57: # old action num: 33 action: "NETWORK_ACL_REMOVERULE" options: position: 5 - 34: + 58: # old action num: 34 action: "NETWORK_ACL_REMOVERULE" options: position: 6 - 35: + 59: # old action num: 35 action: "NETWORK_ACL_REMOVERULE" options: position: 7 - 36: + 60: # old action num: 36 action: "NETWORK_ACL_REMOVERULE" options: position: 8 - 37: + 61: # old action num: 37 action: "NETWORK_ACL_REMOVERULE" options: position: 9 - 38: + 62: # old action num: 38 action: "NETWORK_NIC_DISABLE" options: node_id: 0 nic_id: 0 - 39: + 63: # old action num: 39 action: "NETWORK_NIC_ENABLE" options: node_id: 0 nic_id: 0 - 40: + 64: # old action num: 40 action: "NETWORK_NIC_DISABLE" options: node_id: 1 nic_id: 0 - 41: + 65: # old action num: 41 action: "NETWORK_NIC_ENABLE" options: node_id: 1 nic_id: 0 - 42: + 66: # old action num: 42 action: "NETWORK_NIC_DISABLE" options: node_id: 2 nic_id: 0 - 43: + 67: # old action num: 43 action: "NETWORK_NIC_ENABLE" options: node_id: 2 nic_id: 0 - 44: + 68: # old action num: 44 action: "NETWORK_NIC_DISABLE" options: node_id: 3 nic_id: 0 - 45: + 69: # old action num: 45 action: "NETWORK_NIC_ENABLE" options: node_id: 3 nic_id: 0 - 46: + 70: # old action num: 46 action: "NETWORK_NIC_DISABLE" options: node_id: 4 nic_id: 0 - 47: + 71: # old action num: 47 action: "NETWORK_NIC_ENABLE" options: node_id: 4 nic_id: 0 - 48: + 72: # old action num: 48 action: "NETWORK_NIC_DISABLE" options: node_id: 4 nic_id: 1 - 49: + 73: # old action num: 49 action: "NETWORK_NIC_ENABLE" options: node_id: 4 nic_id: 1 - 50: + 74: # old action num: 50 action: "NETWORK_NIC_DISABLE" options: node_id: 5 nic_id: 0 - 51: + 75: # old action num: 51 action: "NETWORK_NIC_ENABLE" options: node_id: 5 nic_id: 0 - 52: + 76: # old action num: 52 action: "NETWORK_NIC_DISABLE" options: node_id: 6 nic_id: 0 - 53: + 77: # old action num: 53 action: "NETWORK_NIC_ENABLE" options: node_id: 6 nic_id: 0 + options: nodes: - node_name: domain_controller From 0d490d618cb70b9a194c041af09c2f8643d023f9 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 3 Mar 2024 16:59:14 +0000 Subject: [PATCH 068/128] Update MARL config --- .../example_config_2_rl_agents.yaml | 96 +++++++++++++++---- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index fd5a3092..e8f271df 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -36,6 +36,11 @@ agents: - ref: client_2_green_user team: GREEN type: probabilistic_agent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 observation_space: type: UC2GreenObservation action_space: @@ -47,24 +52,38 @@ agents: - 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: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 reward_function: reward_components: - type: DUMMY - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 - - ref: client_1_green_user team: GREEN type: probabilistic_agent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 observation_space: type: UC2GreenObservation action_space: @@ -76,10 +95,26 @@ agents: - 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: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + reward_function: reward_components: - type: DUMMY @@ -87,6 +122,7 @@ agents: + - ref: data_manipulation_attacker team: RED type: RedDatabaseCorruptingAgent @@ -671,6 +707,14 @@ agents: weight: 0.33 options: node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.1 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.1 + options: + node_hostname: client_2 agent_settings: @@ -1223,6 +1267,14 @@ agents: weight: 0.33 options: node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.1 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.1 + options: + node_hostname: client_2 agent_settings: @@ -1241,8 +1293,8 @@ simulation: nodes: - ref: router_1 - type: router hostname: router_1 + type: router num_ports: 5 ports: 1: @@ -1277,18 +1329,18 @@ simulation: protocol: ICMP - ref: switch_1 - type: switch hostname: switch_1 + type: switch num_ports: 8 - ref: switch_2 - type: switch hostname: switch_2 + type: switch num_ports: 8 - ref: domain_controller - type: server hostname: domain_controller + type: server ip_address: 192.168.1.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1300,8 +1352,8 @@ simulation: arcd.com: 192.168.1.12 # web server - ref: web_server - type: server hostname: web_server + type: server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1317,8 +1369,8 @@ simulation: - ref: database_server - type: server hostname: database_server + type: server ip_address: 192.168.1.14 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1332,8 +1384,8 @@ simulation: type: FTPClient - ref: backup_server - type: server hostname: backup_server + type: server ip_address: 192.168.1.16 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1343,8 +1395,8 @@ simulation: type: FTPServer - ref: security_suite - type: server hostname: security_suite + type: server ip_address: 192.168.1.110 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1355,8 +1407,8 @@ simulation: subnet_mask: 255.255.255.0 - ref: client_1 - type: computer hostname: client_1 + type: computer ip_address: 192.168.10.21 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 @@ -1373,13 +1425,17 @@ simulation: type: WebBrowser options: target_url: http://arcd.com/users/ + - ref: client_1_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - ref: client_1_dns_client type: DNSClient - ref: client_2 - type: computer hostname: client_2 + type: computer ip_address: 192.168.10.22 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 @@ -1396,6 +1452,10 @@ simulation: data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 + - ref: client_2_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - ref: client_2_dns_client type: DNSClient From d1480e4477f42f46a99776711f80c3a71ee90934 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 09:58:57 +0000 Subject: [PATCH 069/128] Apply suggestions from PR review. --- docs/source/configuration/agents.rst | 6 +++--- .../system/services/database_service.rst | 7 +++++++ src/primaite/config/_package_data/example_config.yaml | 4 ++-- .../config/_package_data/example_config_2_rl_agents.yaml | 4 ++-- src/primaite/game/game.py | 2 +- .../simulator/network/transmission/network_layer.py | 2 +- .../applications/red_applications/data_manipulation_bot.py | 2 -- tests/assets/configs/bad_primaite_session.yaml | 2 +- tests/assets/configs/basic_firewall.yaml | 2 +- tests/assets/configs/basic_switched_network.yaml | 2 +- tests/assets/configs/dmz_network.yaml | 2 +- tests/assets/configs/eval_only_primaite_session.yaml | 2 +- tests/assets/configs/multi_agent_session.yaml | 2 +- tests/assets/configs/test_primaite_session.yaml | 2 +- tests/assets/configs/train_only_primaite_session.yaml | 2 +- tests/integration_tests/game_layer/test_rewards.py | 1 + 16 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/source/configuration/agents.rst b/docs/source/configuration/agents.rst index ac67c365..b8912883 100644 --- a/docs/source/configuration/agents.rst +++ b/docs/source/configuration/agents.rst @@ -19,7 +19,7 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo ... - ref: green_agent_example team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: @@ -57,11 +57,11 @@ Specifies if the agent is malicious (``RED``), benign (``GREEN``), or defensive ``type`` -------- -Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``probabilistic_agent`` generate their own behaviour. +Specifies which class should be used for the agent. ``ProxyAgent`` is used for agents that receive instructions from an RL algorithm. Scripted agents like ``RedDatabaseCorruptingAgent`` and ``ProbabilisticAgent`` generate their own behaviour. Available agent types: -- ``probabilistic_agent`` +- ``ProbabilisticAgent`` - ``ProxyAgent`` - ``RedDatabaseCorruptingAgent`` diff --git a/docs/source/simulation_components/system/services/database_service.rst b/docs/source/simulation_components/system/services/database_service.rst index 2c962c0a..dd6dec41 100644 --- a/docs/source/simulation_components/system/services/database_service.rst +++ b/docs/source/simulation_components/system/services/database_service.rst @@ -25,6 +25,13 @@ Usage - Clients connect, execute queries, and disconnect. - Service runs on TCP port 5432 by default. +**Supported queries:** + +* ``SELECT``: As long as the database file is in a ``GOOD`` health state, the db service will respond with a 200 status code. +* ``DELETE``: This query represents an attack, it will cause the database file to enter a ``COMPROMISED`` state, and return a status code 200. +* ``INSERT``: If the database service is in a healthy state, this will return a 200 status, if it's not in a healthy state it will return 404. +* ``SELECT * FROM pg_stat_activity``: This query represents something an admin would send to check the status of the database. If the database service is in a healthy state, it returns a 200 status code, otherwise a 401 status code. + Implementation ============== diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 45d29b48..8d1b4293 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -33,7 +33,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent agent_settings: action_probabilities: 0: 0.3 @@ -76,7 +76,7 @@ agents: - ref: client_1_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent agent_settings: action_probabilities: 0: 0.3 diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index b6b07afa..260517b9 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -35,7 +35,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: @@ -64,7 +64,7 @@ agents: - ref: client_1_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index bfbefd3c..0749e5db 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -414,7 +414,7 @@ class PrimaiteGame: reward_function = RewardFunction.from_config(reward_function_cfg) # CREATE AGENT - if agent_type == "probabilistic_agent": + if agent_type == "ProbabilisticAgent": # TODO: implement non-random agents and fix this parsing settings = agent_cfg.get("agent_settings") new_agent = ProbabilisticAgent( diff --git a/src/primaite/simulator/network/transmission/network_layer.py b/src/primaite/simulator/network/transmission/network_layer.py index 22d7f97d..8ee0b4af 100644 --- a/src/primaite/simulator/network/transmission/network_layer.py +++ b/src/primaite/simulator/network/transmission/network_layer.py @@ -16,7 +16,7 @@ class IPProtocol(Enum): """ NONE = "none" - """Placeholder for a non-port.""" + """Placeholder for a non-protocol.""" TCP = "tcp" """Transmission Control Protocol.""" UDP = "udp" 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 961f82c2..ee98ea8e 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 @@ -38,9 +38,7 @@ class DataManipulationAttackStage(IntEnum): class DataManipulationBot(Application): """A bot that simulates a script which performs a SQL injection attack.""" - # server_ip_address: Optional[IPv4Address] = None payload: Optional[str] = None - # server_password: Optional[str] = None port_scan_p_of_success: float = 0.1 data_manipulation_p_of_success: float = 0.1 diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 017492ad..38d54ce3 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -21,7 +21,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/basic_firewall.yaml b/tests/assets/configs/basic_firewall.yaml index 0a892650..9d7b34cb 100644 --- a/tests/assets/configs/basic_firewall.yaml +++ b/tests/assets/configs/basic_firewall.yaml @@ -40,7 +40,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/basic_switched_network.yaml b/tests/assets/configs/basic_switched_network.yaml index 6c6b2845..9a0d5313 100644 --- a/tests/assets/configs/basic_switched_network.yaml +++ b/tests/assets/configs/basic_switched_network.yaml @@ -40,7 +40,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/dmz_network.yaml b/tests/assets/configs/dmz_network.yaml index 56a68410..95e09e16 100644 --- a/tests/assets/configs/dmz_network.yaml +++ b/tests/assets/configs/dmz_network.yaml @@ -65,7 +65,7 @@ game: agents: - ref: client_1_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index e70814f5..f2815578 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -25,7 +25,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 6401bcda..8bbddb76 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -31,7 +31,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index c2616001..199cf8cc 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -29,7 +29,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/assets/configs/train_only_primaite_session.yaml b/tests/assets/configs/train_only_primaite_session.yaml index 8ef4b8fd..71a23989 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -25,7 +25,7 @@ game: agents: - ref: client_2_green_user team: GREEN - type: probabilistic_agent + type: ProbabilisticAgent observation_space: type: UC2GreenObservation action_space: diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 53753967..8edbf0ac 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -41,6 +41,7 @@ def test_WebpageUnavailablePenalty(game_and_agent): def test_uc2_rewards(game_and_agent): + """Test that the reward component correctly applies a penalty when the selected client cannot reach the database.""" game, agent = game_and_agent agent: ControlledAgent From ac9d550e9b2f3ff48a5f93f5612f34395dba9a6d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 10:43:38 +0000 Subject: [PATCH 070/128] Change get_action signature for agents --- .../game/agent/data_manipulation_bot.py | 14 ++++++------- src/primaite/game/agent/interface.py | 20 +++++++++---------- src/primaite/game/agent/rewards.py | 2 +- src/primaite/game/agent/scripted_agents.py | 12 +++++------ src/primaite/game/game.py | 3 +-- tests/conftest.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 2 +- 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/data_manipulation_bot.py index b5de9a5a..c758c926 100644 --- a/src/primaite/game/agent/data_manipulation_bot.py +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -1,5 +1,5 @@ import random -from typing import Dict, Optional, Tuple +from typing import Dict, Tuple from gymnasium.core import ObsType @@ -26,14 +26,14 @@ class DataManipulationAgent(AbstractScriptedAgent): ) self.next_execution_timestep = timestep + random_timestep_increment - def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: - """Randomly sample an action from the action space. + def get_action(self, obs: ObsType, timestep: int) -> Tuple[str, Dict]: + """Waits until a specific timestep, then attempts to execute its data manipulation application. - :param obs: _description_ + :param obs: Current observation for this agent, not used in DataManipulationAgent :type obs: ObsType - :param reward: _description_, defaults to None - :type reward: float, optional - :return: _description_ + :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: diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 4f434bad..88848479 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -112,7 +112,7 @@ class AbstractAgent(ABC): return self.reward_function.update(state) @abstractmethod - def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ Return an action to be taken in the environment. @@ -152,14 +152,14 @@ class AbstractScriptedAgent(AbstractAgent): class RandomAgent(AbstractScriptedAgent): """Agent that ignores its observation and acts completely at random.""" - def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: - """Randomly sample an action from the action space. + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: + """Sample the action space randomly. - :param obs: _description_ + :param obs: Current observation for this agent, not used in RandomAgent :type obs: ObsType - :param reward: _description_, defaults to None - :type reward: float, optional - :return: _description_ + :param timestep: The current simulation timestep, not used in RandomAgent + :type timestep: int + :return: Action formatted in CAOS format :rtype: Tuple[str, Dict] """ return self.action_manager.get_action(self.action_manager.space.sample()) @@ -185,14 +185,14 @@ class ProxyAgent(AbstractAgent): self.most_recent_action: ActType self.flatten_obs: bool = agent_settings.flatten_obs if agent_settings else False - def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ Return the agent's most recent action, formatted in CAOS format. :param obs: Observation for the agent. Not used by ProxyAgents, but required by the interface. :type obs: ObsType - :param reward: Reward value for the agent. Not used by ProxyAgents, defaults to None. - :type reward: float, optional + :param timestep: Current simulation timestep. Not used by ProxyAgents, bur required for the interface. + :type timestep: int :return: Action to be taken in CAOS format. :rtype: Tuple[str, Dict] """ diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 882ad024..8c8e36ad 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -270,7 +270,7 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): return -1.0 elif last_connection_successful is True: return 1.0 - return 0 + return 0.0 @classmethod def from_config(cls, config: Dict) -> AbstractReward: diff --git a/src/primaite/game/agent/scripted_agents.py b/src/primaite/game/agent/scripted_agents.py index 28d94062..5111df32 100644 --- a/src/primaite/game/agent/scripted_agents.py +++ b/src/primaite/game/agent/scripted_agents.py @@ -70,17 +70,17 @@ class ProbabilisticAgent(AbstractScriptedAgent): super().__init__(agent_name, action_space, observation_space, reward_function) - def get_action(self, obs: ObsType, reward: float = 0.0, timestep: Optional[int] = None) -> Tuple[str, Dict]: + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: """ - Choose a random action from the action space. + Sample the action space randomly. The probability of each action is given by the corresponding index in ``self.probabilities``. - :param obs: Current observation of the simulation + :param obs: Current observation for this agent, not used in ProbabilisticAgent :type obs: ObsType - :param reward: Reward for the last step, not used for scripted agents, defaults to 0 - :type reward: float, optional - :return: Action to be taken in CAOS format. + :param timestep: The current simulation timestep, not used in ProbabilisticAgent + :type timestep: int + :return: Action formatted in CAOS format :rtype: Tuple[str, Dict] """ choice = self.rng.choice(len(self.action_manager.action_map), p=self.probabilities) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 0749e5db..cd88d832 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -165,8 +165,7 @@ class PrimaiteGame: agent_actions = {} for _, agent in self.agents.items(): obs = agent.observation_manager.current_observation - rew = agent.reward_function.current_reward - action_choice, options = agent.get_action(obs, rew, timestep=self.step_counter) + action_choice, options = agent.get_action(obs, timestep=self.step_counter) agent_actions[agent.agent_name] = (action_choice, options) request = agent.format_request(action_choice, options) self.simulation.apply_request(request) diff --git a/tests/conftest.py b/tests/conftest.py index b60de730..a117a1ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -328,7 +328,7 @@ class ControlledAgent(AbstractAgent): ) self.most_recent_action: Tuple[str, Dict] - def get_action(self, obs: None, reward: float = 0.0, timestep: Optional[int] = None) -> 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.""" return self.most_recent_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 f0b37cac..73228e36 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -69,7 +69,7 @@ def test_probabilistic_agent(): node_application_execute_count = 0 node_file_delete_count = 0 for _ in range(N_TRIALS): - a = pa.get_action(0, timestep=0) + a = pa.get_action(0) if a == ("DONOTHING", {}): do_nothing_count += 1 elif a == ("NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0}): From 2c3652979bfd11c6e322c256b64658ec5f404847 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 11:17:54 +0000 Subject: [PATCH 071/128] Add helpful error messages to action index errors --- src/primaite/game/agent/actions.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 392d07c6..84bd3f39 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -812,6 +812,13 @@ class ActionManager: :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]: @@ -825,6 +832,13 @@ class ActionManager: :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]: @@ -840,6 +854,17 @@ class ActionManager: 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]: @@ -852,6 +877,13 @@ class ActionManager: :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]: @@ -864,6 +896,13 @@ class ActionManager: :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: @@ -874,6 +913,13 @@ class ActionManager: :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: @@ -885,6 +931,13 @@ class ActionManager: :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_port_by_idx(self, port_idx: int) -> str: @@ -896,6 +949,13 @@ class ActionManager: :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: From 2f456e7ae07660b6f9382f951cb59fe9b066fe31 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 18:47:50 +0000 Subject: [PATCH 072/128] Move IO to environments from session and add agent logging --- .../config/_package_data/example_config.yaml | 1 + .../example_config_2_rl_agents.yaml | 81 ++++++++++--- .../game/agent/data_manipulation_bot.py | 2 +- src/primaite/game/game.py | 7 +- src/primaite/session/environment.py | 28 ++++- src/primaite/session/io.py | 108 +++++++++++++----- src/primaite/session/policy/sb3.py | 4 +- src/primaite/session/session.py | 16 ++- 8 files changed, 183 insertions(+), 64 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 8d1b4293..77296529 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -13,6 +13,7 @@ training_config: io_settings: save_checkpoints: true checkpoint_interval: 5 + save_agent_actions: true save_step_metadata: false save_pcap_logs: false save_sys_logs: true diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 260517b9..a5a1d08f 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -15,6 +15,7 @@ training_config: io_settings: save_checkpoints: true checkpoint_interval: 5 + save_agent_actions: true save_step_metadata: false save_pcap_logs: false save_sys_logs: true @@ -36,6 +37,11 @@ agents: - ref: client_2_green_user team: GREEN type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 observation_space: type: UC2GreenObservation action_space: @@ -47,24 +53,38 @@ agents: - 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: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 reward_function: reward_components: - type: DUMMY - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 - - ref: client_1_green_user team: GREEN type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 observation_space: type: UC2GreenObservation action_space: @@ -76,10 +96,26 @@ agents: - 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: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + reward_function: reward_components: - type: DUMMY @@ -1036,7 +1072,6 @@ agents: - simulation: network: nmne_config: @@ -1046,8 +1081,8 @@ simulation: nodes: - ref: router_1 - type: router hostname: router_1 + type: router num_ports: 5 ports: 1: @@ -1082,18 +1117,18 @@ simulation: protocol: ICMP - ref: switch_1 - type: switch hostname: switch_1 + type: switch num_ports: 8 - ref: switch_2 - type: switch hostname: switch_2 + type: switch num_ports: 8 - ref: domain_controller - type: server hostname: domain_controller + type: server ip_address: 192.168.1.10 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1105,8 +1140,8 @@ simulation: arcd.com: 192.168.1.12 # web server - ref: web_server - type: server hostname: web_server + type: server ip_address: 192.168.1.12 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1122,8 +1157,8 @@ simulation: - ref: database_server - type: server hostname: database_server + type: server ip_address: 192.168.1.14 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1137,8 +1172,8 @@ simulation: type: FTPClient - ref: backup_server - type: server hostname: backup_server + type: server ip_address: 192.168.1.16 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1148,8 +1183,8 @@ simulation: type: FTPServer - ref: security_suite - type: server hostname: security_suite + type: server ip_address: 192.168.1.110 subnet_mask: 255.255.255.0 default_gateway: 192.168.1.1 @@ -1160,8 +1195,8 @@ simulation: subnet_mask: 255.255.255.0 - ref: client_1 - type: computer hostname: client_1 + type: computer ip_address: 192.168.10.21 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 @@ -1178,13 +1213,17 @@ simulation: type: WebBrowser options: target_url: http://arcd.com/users/ + - ref: client_1_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - ref: client_1_dns_client type: DNSClient - ref: client_2 - type: computer hostname: client_2 + type: computer ip_address: 192.168.10.22 subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 @@ -1201,6 +1240,10 @@ simulation: data_manipulation_p_of_success: 0.8 payload: "DELETE" server_ip: 192.168.1.14 + - ref: client_2_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 services: - ref: client_2_dns_client type: DNSClient diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/data_manipulation_bot.py index c758c926..16453433 100644 --- a/src/primaite/game/agent/data_manipulation_bot.py +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -37,7 +37,7 @@ class DataManipulationAgent(AbstractScriptedAgent): :rtype: Tuple[str, Dict] """ if timestep < self.next_execution_timestep: - return "DONOTHING", {"dummy": 0} + return "DONOTHING", {} self._set_next_execution_timestep(timestep + self.agent_settings.start_settings.frequency) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index cd88d832..394a8154 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -11,7 +11,6 @@ from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAge from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.game.agent.scripted_agents import ProbabilisticAgent -from primaite.session.io import SessionIO, SessionIOSettings from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC @@ -210,10 +209,6 @@ class PrimaiteGame: :return: A PrimaiteGame object. :rtype: PrimaiteGame """ - io_settings = cfg.get("io_settings", {}) - _ = SessionIO(SessionIOSettings(**io_settings)) - # Instantiating this ensures that the game saves to the correct output dir even without being part of a session - game = cls() game.options = PrimaiteGameOptions(**cfg["game"]) game.save_step_metadata = cfg.get("io_settings", {}).get("save_step_metadata") or False @@ -415,7 +410,7 @@ class PrimaiteGame: # CREATE AGENT if agent_type == "ProbabilisticAgent": # TODO: implement non-random agents and fix this parsing - settings = agent_cfg.get("agent_settings") + settings = agent_cfg.get("agent_settings", {}) new_agent = ProbabilisticAgent( agent_name=agent_cfg["ref"], action_space=action_space, diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index d54503a3..72d5ac9c 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -8,6 +8,7 @@ from ray.rllib.env.multi_agent_env import MultiAgentEnv from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame +from primaite.session.io import PrimaiteIO from primaite.simulator import SIM_OUTPUT @@ -32,6 +33,9 @@ class PrimaiteGymEnv(gymnasium.Env): self.episode_counter: int = 0 """Current episode number.""" + self.io = PrimaiteIO.from_config(game_config.get("io_settings", {})) + """Handles IO for the environment. This produces sys logs, agent logs, etc.""" + @property def agent(self) -> ProxyAgent: """Grab a fresh reference to the agent object because it will be reinstantiated each episode.""" @@ -55,6 +59,10 @@ class PrimaiteGymEnv(gymnasium.Env): info = {"agent_actions": agent_actions} # tell us what all the agents did for convenience. if self.game.save_step_metadata: self._write_step_metadata_json(action, state, reward) + if self.io.settings.save_agent_actions: + self.io.store_agent_actions( + agent_actions=agent_actions, episode=self.episode_counter, timestep=self.game.step_counter + ) return next_obs, reward, terminated, truncated, info def _write_step_metadata_json(self, action: int, state: Dict, reward: int): @@ -79,6 +87,9 @@ class PrimaiteGymEnv(gymnasium.Env): f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {self.agent.reward_function.total_reward}" ) + if self.io.settings.save_agent_actions: + self.io.write_agent_actions(episode=self.episode_counter) + self.io.clear_agent_actions() self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=copy.deepcopy(self.game_config)) self.game.setup_for_episode(episode=self.episode_counter) self.episode_counter += 1 @@ -146,7 +157,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): """ self.game_config: Dict = env_config """PrimaiteGame definition. This can be changed between episodes to enable curriculum learning.""" - self.game: PrimaiteGame = PrimaiteGame.from_config(self.game_config) + self.game: PrimaiteGame = PrimaiteGame.from_config(copy.deepcopy(self.game_config)) """Reference to the primaite game""" self._agent_ids = list(self.game.rl_agents.keys()) """Agent ids. This is a list of strings of agent names.""" @@ -164,6 +175,10 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): self.action_space = gymnasium.spaces.Dict( {name: agent.action_manager.space for name, agent in self.agents.items()} ) + + self.io = PrimaiteIO.from_config(env_config.get("io_settings")) + """Handles IO for the environment. This produces sys logs, agent logs, etc.""" + super().__init__() @property @@ -173,7 +188,10 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" - self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=self.game_config) + if self.io.settings.save_agent_actions: + self.io.write_agent_actions(episode=self.episode_counter) + self.io.clear_agent_actions() + self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=copy.deepcopy(self.game_config)) self.game.setup_for_episode(episode=self.episode_counter) self.episode_counter += 1 state = self.game.get_sim_state() @@ -196,7 +214,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): # 1. Perform actions for agent_name, action in actions.items(): self.agents[agent_name].store_action(action) - self.game.apply_agent_actions() + agent_actions = self.game.apply_agent_actions() # 2. Advance timestep self.game.advance_timestep() @@ -215,6 +233,10 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): truncateds["__all__"] = self.game.calculate_truncated() if self.game.save_step_metadata: self._write_step_metadata_json(actions, state, rewards) + if self.io.settings.save_agent_actions: + self.io.store_agent_actions( + agent_actions=agent_actions, episode=self.episode_counter, timestep=self.game.step_counter + ) return next_obs, rewards, terminateds, truncateds, infos def _write_step_metadata_json(self, actions: Dict, state: Dict, rewards: Dict): diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index b4b740e9..22d9dbeb 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -1,53 +1,50 @@ +import json from datetime import datetime from pathlib import Path -from typing import Optional +from typing import Dict, List, Optional from pydantic import BaseModel, ConfigDict -from primaite import PRIMAITE_PATHS +from primaite import getLogger, PRIMAITE_PATHS from primaite.simulator import SIM_OUTPUT - -class SessionIOSettings(BaseModel): - """Schema for session IO settings.""" - - model_config = ConfigDict(extra="forbid") - - save_final_model: bool = True - """Whether to save the final model right at the end of training.""" - save_checkpoints: bool = False - """Whether to save a checkpoint model every `checkpoint_interval` episodes""" - checkpoint_interval: int = 10 - """How often to save a checkpoint model (if save_checkpoints is True).""" - save_logs: bool = True - """Whether to save logs""" - save_transactions: bool = True - """Whether to save transactions, If true, the session path will have a transactions folder.""" - save_tensorboard_logs: bool = False - """Whether to save tensorboard logs. If true, the session path will have a tensorboard_logs folder.""" - save_step_metadata: bool = False - """Whether to save the RL agents' action, environment state, and other data at every single step.""" - save_pcap_logs: bool = False - """Whether to save PCAP logs.""" - save_sys_logs: bool = False - """Whether to save system logs.""" +_LOGGER = getLogger(__name__) -class SessionIO: +class PrimaiteIO: """ Class for managing session IO. Currently it's handling path generation, but could expand to handle loading, transaction, tensorboard, and so on. """ - def __init__(self, settings: SessionIOSettings = SessionIOSettings()) -> None: - self.settings: SessionIOSettings = settings + class Settings(BaseModel): + """Config schema for PrimaiteIO object.""" + + model_config = ConfigDict(extra="forbid") + + save_logs: bool = True + """Whether to save logs""" + save_agent_actions: bool = True + """Whether to save a log of all agents' actions every step.""" + save_transactions: bool = True + """Whether to save transactions, If true, the session path will have a transactions folder.""" + save_step_metadata: bool = False + """Whether to save the RL agents' action, environment state, and other data at every single step.""" + save_pcap_logs: bool = False + """Whether to save PCAP logs.""" + save_sys_logs: bool = False + """Whether to save system logs.""" + + def __init__(self, settings: Optional[Settings] = None) -> None: + self.settings = settings or PrimaiteIO.Settings() self.session_path: Path = self.generate_session_path() # set global SIM_OUTPUT path SIM_OUTPUT.path = self.session_path / "simulation_output" SIM_OUTPUT.save_pcap_logs = self.settings.save_pcap_logs SIM_OUTPUT.save_sys_logs = self.settings.save_sys_logs + self.agent_action_log: List[Dict] = [] # warning TODO: must be careful not to re-initialise sessionIO because it will create a new path each time it's # possible refactor needed @@ -68,3 +65,56 @@ class SessionIO: def generate_checkpoint_save_path(self, agent_name: str, episode: int) -> Path: """Return the path where the checkpoint model will be saved (excluding filename extension).""" return self.session_path / "checkpoints" / f"{agent_name}_checkpoint_{episode}.pt" + + def generate_agent_actions_save_path(self, episode: int) -> Path: + """Return the path where agent actions will be saved.""" + return self.session_path / "agent_actions" / f"episode_{episode}.json" + + def store_agent_actions(self, agent_actions: Dict, episode: int, timestep: int) -> None: + """Cache agent actions for a particular step. + + :param agent_actions: Dictionary describing actions for any agents that acted in this timestep. The expected + format contains agent identifiers as keys. The keys should map to a tuple of [CAOS action, parameters] + CAOS action is a string representing one the CAOS actions. + parameters is a dict of parameter names and values for that particular CAOS action. + For example: + { + 'green1' : ('NODE_APPLICATION_EXECUTE', {'node_id':1, 'application_id':0}), + 'defender': ('DO_NOTHING', {}) + } + :type agent_actions: Dict + :param timestep: Simulation timestep when these actions occurred. + :type timestep: int + """ + self.agent_action_log.append( + [ + { + "episode": episode, + "timestep": timestep, + "agent_actions": {k: {"action": v[0], "parameters": v[1]} for k, v in agent_actions.items()}, + } + ] + ) + + def write_agent_actions(self, episode: int) -> None: + """Take the contents of the agent action log and write it to a file. + + :param episode: Episode number + :type episode: int + """ + path = self.generate_agent_actions_save_path(episode=episode) + path.parent.mkdir(exist_ok=True, parents=True) + path.touch() + _LOGGER.info(f"Saving agent action log to {path}") + with open(path, "w") as file: + json.dump(self.agent_action_log, fp=file, indent=1) + + def clear_agent_actions(self) -> None: + """Reset the agent action log back to an empty dictionary.""" + self.agent_action_log = [] + + @classmethod + def from_config(cls, config: Dict) -> "PrimaiteIO": + """Create an instance of PrimaiteIO based on a configuration dict.""" + new = cls() + return new diff --git a/src/primaite/session/policy/sb3.py b/src/primaite/session/policy/sb3.py index 254baf4d..6220371d 100644 --- a/src/primaite/session/policy/sb3.py +++ b/src/primaite/session/policy/sb3.py @@ -39,9 +39,9 @@ class SB3Policy(PolicyABC, identifier="SB3"): def learn(self, n_episodes: int, timesteps_per_episode: int) -> None: """Train the agent.""" - if self.session.io_manager.settings.save_checkpoints: + if self.session.save_checkpoints: checkpoint_callback = CheckpointCallback( - save_freq=timesteps_per_episode * self.session.io_manager.settings.checkpoint_interval, + save_freq=timesteps_per_episode * self.session.checkpoint_interval, save_path=self.session.io_manager.generate_model_save_path("sb3"), name_prefix="sb3_model", ) diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index d244f6b0..84dd9b2f 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -1,3 +1,4 @@ +# raise DeprecationWarning("This module is deprecated") from enum import Enum from pathlib import Path from typing import Dict, List, Literal, Optional, Union @@ -5,7 +6,7 @@ from typing import Dict, List, Literal, Optional, Union from pydantic import BaseModel, ConfigDict from primaite.session.environment import PrimaiteGymEnv, PrimaiteRayEnv, PrimaiteRayMARLEnv -from primaite.session.io import SessionIO, SessionIOSettings +from primaite.session.io import PrimaiteIO # from primaite.game.game import PrimaiteGame from primaite.session.policy.policy import PolicyABC @@ -53,12 +54,18 @@ class PrimaiteSession: self.policy: PolicyABC """The reinforcement learning policy.""" - self.io_manager: Optional["SessionIO"] = None + self.io_manager: Optional["PrimaiteIO"] = None """IO manager for the session.""" self.game_cfg: Dict = game_cfg """Primaite Game object for managing main simulation loop and agents.""" + self.save_checkpoints: bool = False + """Whether to save chcekpoints.""" + + self.checkpoint_interval: int = 10 + """If save_checkpoints is true, checkpoints will be saved every checkpoint_interval episodes.""" + def start_session(self) -> None: """Commence the training/eval session.""" print("Starting Primaite Session") @@ -89,12 +96,13 @@ class PrimaiteSession: def from_config(cls, cfg: Dict, agent_load_path: Optional[str] = None) -> "PrimaiteSession": """Create a PrimaiteSession object from a config dictionary.""" # READ IO SETTINGS (this sets the global session path as well) # TODO: GLOBAL SIDE EFFECTS... - io_settings = cfg.get("io_settings", {}) - io_manager = SessionIO(SessionIOSettings(**io_settings)) + io_manager = PrimaiteIO.from_config(cfg.get("io_settings", {})) sess = cls(game_cfg=cfg) sess.io_manager = io_manager sess.training_options = TrainingOptions(**cfg["training_config"]) + sess.save_checkpoints = cfg.get("io_settings", {}).get("save_checkpoints") + sess.checkpoint_interval = cfg.get("io_settings", {}).get("checkpoint_interval") # CREATE ENVIRONMENT if sess.training_options.rl_framework == "RLLIB_single_agent": From c3010ff816882e47809bd4889b2b9c15253c2ce9 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 18:59:03 +0000 Subject: [PATCH 073/128] Update changelog and docs --- CHANGELOG.md | 1 + docs/source/configuration/io_settings.rst | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d54af980..48998d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the order of service health state - Fixed an issue where starting a node didn't start the services on it - Added support for SQL INSERT command. +- Added ability to log each agent's action choices each step to a JSON file. diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index 96cc28fe..e5c6d2ce 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -19,6 +19,7 @@ This section configures how PrimAITE saves data during simulation and training. # save_logs: True # save_transactions: False # save_tensorboard_logs: False + save_agent_actions: True save_step_metadata: False save_pcap_logs: False save_sys_logs: False @@ -65,6 +66,12 @@ Defines how often to save the policy during training. *currently unused*. +``save_agent_actions`` + +Optional. Default value is ``True``. + +If ``True``, this will create a JSON file each episode detailing every agent's action each step in that episode, formatted according to the CAOS format. This includes scripted, RL, and red agents. + ``save_step_metadata`` ---------------------- From 1e8dfa40cf0214585f69834d3d6326be40799db7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 19:36:54 +0000 Subject: [PATCH 074/128] Give uc2 notebook a meaningful name --- .../{uc2_demo.ipynb => Data-Manipulation-E2E-Demonstration.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/primaite/notebooks/{uc2_demo.ipynb => Data-Manipulation-E2E-Demonstration.ipynb} (100%) diff --git a/src/primaite/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb similarity index 100% rename from src/primaite/notebooks/uc2_demo.ipynb rename to src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb From a222a8c58fa8fa3b8a13043327b4211a9ed63d2b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 19:43:51 +0000 Subject: [PATCH 075/128] Give the UC2 config load function a meaningful name --- src/primaite/cli.py | 4 ++-- src/primaite/config/load.py | 2 +- src/primaite/main.py | 4 ++-- .../notebooks/Data-Manipulation-E2E-Demonstration.ipynb | 4 ++-- .../notebooks/training_example_ray_single_agent.ipynb | 4 ++-- src/primaite/notebooks/training_example_sb3.ipynb | 4 ++-- .../environments/test_rllib_multi_agent_environment.py | 4 ++-- .../environments/test_rllib_single_agent_environment.py | 4 ++-- .../environments/test_sb3_environment.py | 4 ++-- .../configuration_file_parsing/nodes/test_node_config.py | 4 ++-- .../software_installation_and_configuration.py | 4 ++-- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/primaite/cli.py b/src/primaite/cli.py index 81ab2792..18d21f7b 100644 --- a/src/primaite/cli.py +++ b/src/primaite/cli.py @@ -127,10 +127,10 @@ def session( :param config: The path to the config file. Optional, if None, the example config will be used. :type config: Optional[str] """ - from primaite.config.load import example_config_path + from primaite.config.load import data_manipulation_config_path from primaite.main import run if not config: - config = example_config_path() + config = data_manipulation_config_path() print(config) run(config_path=config, agent_load_path=agent_load_file) diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index b01eb129..6bd0d80d 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -30,7 +30,7 @@ def load(file_path: Union[str, Path]) -> Dict: return config -def example_config_path() -> Path: +def data_manipulation_config_path() -> Path: """ Get the path to the example config. diff --git a/src/primaite/main.py b/src/primaite/main.py index b63227a7..053ed65b 100644 --- a/src/primaite/main.py +++ b/src/primaite/main.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional, Union from primaite import getLogger -from primaite.config.load import example_config_path, load +from primaite.config.load import data_manipulation_config_path, load from primaite.session.session import PrimaiteSession # from primaite.primaite_session import PrimaiteSession @@ -42,6 +42,6 @@ if __name__ == "__main__": args = parser.parse_args() if not args.config: - args.config = example_config_path() + args.config = data_manipulation_config_path() run(args.config) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 85061b2b..e35e6126 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -371,7 +371,7 @@ "outputs": [], "source": [ "# Imports\n", - "from primaite.config.load import example_config_path\n", + "from primaite.config.load import data_manipulation_config_path\n", "from primaite.session.environment import PrimaiteGymEnv\n", "from primaite.game.game import PrimaiteGame\n", "import yaml\n", @@ -394,7 +394,7 @@ "outputs": [], "source": [ "# create the env\n", - "with open(example_config_path(), 'r') as f:\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n", " # set success probability to 1.0 to avoid rerunning cells.\n", " cfg['simulation']['network']['nodes'][8]['applications'][0]['options']['data_manipulation_p_of_success'] = 1.0\n", diff --git a/src/primaite/notebooks/training_example_ray_single_agent.ipynb b/src/primaite/notebooks/training_example_ray_single_agent.ipynb index 3c27bdc6..2fe84655 100644 --- a/src/primaite/notebooks/training_example_ray_single_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_single_agent.ipynb @@ -16,7 +16,7 @@ "source": [ "from primaite.game.game import PrimaiteGame\n", "import yaml\n", - "from primaite.config.load import example_config_path\n", + "from primaite.config.load import data_manipulation_config_path\n", "\n", "from primaite.session.environment import PrimaiteRayEnv\n", "from ray.rllib.algorithms import ppo\n", @@ -26,7 +26,7 @@ "\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(example_config_path(), 'r') as f:\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n", "\n", "ray.init(local_mode=True)\n" diff --git a/src/primaite/notebooks/training_example_sb3.ipynb b/src/primaite/notebooks/training_example_sb3.ipynb index 0472854e..cefcc429 100644 --- a/src/primaite/notebooks/training_example_sb3.ipynb +++ b/src/primaite/notebooks/training_example_sb3.ipynb @@ -17,7 +17,7 @@ "metadata": {}, "outputs": [], "source": [ - "from primaite.config.load import example_config_path" + "from primaite.config.load import data_manipulation_config_path" ] }, { @@ -26,7 +26,7 @@ "metadata": {}, "outputs": [], "source": [ - "with open(example_config_path(), 'r') as f:\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n" ] }, 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 3934ce5b..84897f9a 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 @@ -4,7 +4,7 @@ import yaml from ray import air, tune from ray.rllib.algorithms.ppo import PPOConfig -from primaite.config.load import example_config_path +from primaite.config.load import data_manipulation_config_path from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteRayMARLEnv @@ -13,7 +13,7 @@ from primaite.session.environment import PrimaiteRayMARLEnv def test_rllib_multi_agent_compatibility(): """Test that the PrimaiteRayEnv class can be used with a multi agent RLLIB system.""" - with open(example_config_path(), "r") as f: + with open(data_manipulation_config_path(), "r") as f: cfg = yaml.safe_load(f) game = PrimaiteGame.from_config(cfg) 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 2b12ad98..4c4b8d8d 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 @@ -6,7 +6,7 @@ import ray import yaml from ray.rllib.algorithms import ppo -from primaite.config.load import example_config_path +from primaite.config.load import data_manipulation_config_path from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteRayEnv @@ -14,7 +14,7 @@ from primaite.session.environment import PrimaiteRayEnv @pytest.mark.skip(reason="Slow, reenable later") def test_rllib_single_agent_compatibility(): """Test that the PrimaiteRayEnv class can be used with a single agent RLLIB system.""" - with open(example_config_path(), "r") as f: + with open(data_manipulation_config_path(), "r") as f: cfg = yaml.safe_load(f) game = PrimaiteGame.from_config(cfg) diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index c48ddbc9..83965191 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -6,14 +6,14 @@ import pytest import yaml from stable_baselines3 import PPO -from primaite.config.load import example_config_path +from primaite.config.load import data_manipulation_config_path from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv def test_sb3_compatibility(): """Test that the Gymnasium environment can be used with an SB3 agent.""" - with open(example_config_path(), "r") as f: + with open(data_manipulation_config_path(), "r") as f: cfg = yaml.safe_load(f) gym = PrimaiteGymEnv(game_config=cfg) 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 8797bf2e..174bd0c0 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 @@ -from primaite.config.load import example_config_path +from primaite.config.load import data_manipulation_config_path from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -7,7 +7,7 @@ from tests.integration_tests.configuration_file_parsing import BASIC_CONFIG, DMZ def test_example_config(): """Test that the example config can be parsed properly.""" - game = load_config(example_config_path()) + game = load_config(data_manipulation_config_path()) network: Network = game.simulation.network assert len(network.nodes) == 10 # 10 nodes in example 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 7da66547..3aff59af 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 @@ -4,7 +4,7 @@ from typing import Union import yaml -from primaite.config.load import example_config_path +from primaite.config.load import data_manipulation_config_path from primaite.game.agent.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.interface import ProxyAgent, RandomAgent from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING @@ -37,7 +37,7 @@ def load_config(config_path: Union[str, Path]) -> PrimaiteGame: def test_example_config(): """Test that the example config can be parsed properly.""" - game = load_config(example_config_path()) + game = load_config(data_manipulation_config_path()) assert len(game.agents) == 4 # red, blue and 2 green agents From 758f892b74a1e05db3de8e254670bcfec47efd8b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Mar 2024 21:04:27 +0000 Subject: [PATCH 076/128] Make notebook for varying red agent behaviour in uc2 --- docs/index.rst | 1 + docs/source/customising_scenarios.rst | 4 + .../config/_package_data/example_config.yaml | 3 - ...a-Manipulation-Customising-Red-Agent.ipynb | 444 ++++++++++++++++++ src/primaite/session/environment.py | 5 +- 5 files changed, 453 insertions(+), 4 deletions(-) create mode 100644 docs/source/customising_scenarios.rst create mode 100644 src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb diff --git a/docs/index.rst b/docs/index.rst index 08e0ac21..cf17b1c5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -109,6 +109,7 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/game_layer source/config source/environment + source/customising_scenarios .. toctree:: :caption: Developer information: diff --git a/docs/source/customising_scenarios.rst b/docs/source/customising_scenarios.rst new file mode 100644 index 00000000..709f032a --- /dev/null +++ b/docs/source/customising_scenarios.rst @@ -0,0 +1,4 @@ +Customising Agents +****************** + +For an example of how to customise red agent behaviour in the Data Manipulation scenario, please refer to the notebook ``Data-Manipulation-Customising-Red-Agent.ipynb``. diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index fbc12686..aea5d4fd 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -134,9 +134,6 @@ agents: 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 diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb new file mode 100644 index 00000000..6fee18b1 --- /dev/null +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -0,0 +1,444 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Customising red agents\n", + "\n", + "This notebook will go over some examples of how red agent behaviour can be varied by changing its configuration parameters.\n", + "\n", + "First, let's load the standard Data Manipulation config file, and see what the red agent does.\n", + "\n", + "*(For a full explanation of the Data Manipulation scenario, check out the notebook `Data-Manipulation-E2E-Demonstration.ipynb`)*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Imports\n", + "\n", + "from primaite.config.load import data_manipulation_config_path\n", + "from primaite.session.environment import PrimaiteGymEnv\n", + "import yaml\n", + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def make_cfg_have_flat_obs(cfg):\n", + " for agent in cfg['agents']:\n", + " if agent['type'] == \"ProxyAgent\":\n", + " agent['agent_settings']['flatten_obs'] = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + " make_cfg_have_flat_obs(cfg)\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", + "obs, info = env.reset()\n", + "print('env created successfully')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def friendly_output_red_action(info):\n", + " # parse the info dict form step output and write out what the red agent is doing\n", + " red_info = info['agent_actions']['data_manipulation_attacker']\n", + " red_action = red_info[0]\n", + " if red_action == 'DONOTHING':\n", + " red_str = 'DO NOTHING'\n", + " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", + " client = \"client 1\" if red_info[1]['node_id'] == 0 else \"client 2\"\n", + " red_str = f\"ATTACK from {client}\"\n", + " return red_str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, the red agent can start on client 1 or client 2. It starts its attack on a random step between 20 and 30, and it repeats its attack every 15-25 steps.\n", + "\n", + "It also has a 20% chance to fail to perform the port scan, and a 20% chance to fail launching the SQL attack. However it will continue where it left off after a failed step. I.e. if lucky, it can perform the port scan and SQL attack on the first try. If the port scan works, but the sql attack fails the first time it tries to attack, the next time it will not need to port scan again, it can go straight to trying to use SQL attack again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for step in range(35):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " print(f\"step: {step_num:3}, Red action: {friendly_output_red_action(info)}, Blue reward:{reward:.2f}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since the agent does nothing most of the time, let's only print the steps where it performs an attack." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.reset()\n", + "for step in range(100):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " if red.startswith(\"ATTACK\"):\n", + " print(f\"step: {step_num:3}, Red action: {friendly_output_red_action(info)}, Blue reward:{reward:.2f}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Red Configuration\n", + "\n", + "There are two important parts of the YAML config for varying red agent behaviour.\n", + "\n", + "#### The red agent settings\n", + "Here is an annotated config for the red agent in the data manipulation scenario.\n", + "```yaml\n", + " - ref: data_manipulation_attacker # name of agent\n", + " team: RED # not used, just for human reference\n", + " type: RedDatabaseCorruptingAgent # type of agent - this lets primaite know which agent class to use\n", + "\n", + " # Since the agent does not need to react to what is happening in the environment, the observation space is empty.\n", + " observation_space:\n", + " type: UC2RedObservation\n", + " options:\n", + " nodes: {}\n", + "\n", + " action_space:\n", + "\n", + " # The agent has 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", + " # The agent has access to the DataManipulationBoth on clients 1 and 2.\n", + " options:\n", + " nodes:\n", + " - node_name: client_1 # The network should have a node called client_1\n", + " applications:\n", + " - application_name: DataManipulationBot # The node client_1 should have DataManipulationBot configured on it\n", + " - node_name: client_2 # The network should have a node called client_2\n", + " applications:\n", + " - application_name: DataManipulationBot # The node client_2 should have DataManipulationBot configured on it\n", + "\n", + " # not important\n", + " max_folders_per_node: 1\n", + " max_files_per_folder: 1\n", + " max_services_per_node: 1\n", + "\n", + " # red agent does not need a reward function\n", + " reward_function:\n", + " reward_components:\n", + " - type: DUMMY\n", + "\n", + " # These actions are passed to the RedDatabaseCorruptingAgent init method, they dictate the schedule of attacks\n", + " agent_settings:\n", + " start_settings:\n", + " start_step: 25 # first attack at step 25\n", + " frequency: 20 # attacks will happen every 20 steps (on average)\n", + " variance: 5 # the timing of attacks will vary by up to 5 steps earlier or later\n", + "```\n", + "\n", + "#### The settings of the red agent's attack application\n", + "The red agent uses an application called `DataManipulationBot` which leverages a node's `DatabaseClient` to send a malicious SQL query to the database server. Here's an annotated example of how this is configured in the yaml *(with impertinent config items omitted)*:\n", + "```yaml\n", + "simulation:\n", + " network:\n", + " nodes:\n", + " - ref: client_1\n", + " hostname: client_1\n", + " type: computer\n", + " ip_address: 192.168.10.21\n", + " subnet_mask: 255.255.255.0\n", + " default_gateway: 192.168.10.1\n", + " \n", + " # \n", + " applications:\n", + " - ref: data_manipulation_bot\n", + " type: DataManipulationBot\n", + " options:\n", + " port_scan_p_of_success: 0.8 # Probability that port scan is successful\n", + " data_manipulation_p_of_success: 0.8 # Probability that SQL attack is successful\n", + " payload: \"DELETE\" # The SQL query which causes the attack (this has to be DELETE)\n", + " server_ip: 192.168.1.14 # IP address of server hosting the database\n", + " - ref: client_1_database_client\n", + " type: DatabaseClient # Database client must be installed in order for DataManipulationBot to function\n", + " options:\n", + " db_server_ip: 192.168.1.14 # IP address of server hosting the database\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Removing randomness from attack timing\n", + "\n", + "We can make the attacks happen at completely predictable intervals if we set the variance parameter of the red agent to 0." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "change = yaml.safe_load(\"\"\"\n", + "start_settings:\n", + " start_step: 25\n", + " frequency: 20\n", + " variance: 0\n", + "\"\"\")\n", + "\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + " for agent in cfg['agents']:\n", + " if agent['ref'] == \"data_manipulation_attacker\":\n", + " agent['agent_settings'] = change\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", + "env.reset()\n", + "for step in range(100):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " if red.startswith(\"ATTACK\"):\n", + " print(f\"step: {step_num:3}, Red action: {friendly_output_red_action(info)}, Blue reward:{reward:.2f}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Making the start node always the same\n", + "\n", + "Normally, the agent randomly chooses between the nodes in its action space to send attacks from:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Open the config without changing anyhing\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", + "env.reset()\n", + "for ep in range(12):\n", + " env.reset()\n", + " for step in range(31):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " if red.startswith(\"ATTACK\"):\n", + " print(f\"Episode: {ep:2}, step: {step_num:3}, Red action: {friendly_output_red_action(info)}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can make the agent always start on a node of our choice letting that be the only node in the agent's action space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "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", + "\"\"\")\n", + "\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + " for agent in cfg['agents']:\n", + " if agent['ref'] == \"data_manipulation_attacker\":\n", + " agent.update(change)\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", + "env.reset()\n", + "for ep in range(12):\n", + " env.reset()\n", + " for step in range(31):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " if red.startswith(\"ATTACK\"):\n", + " print(f\"Episode: {ep:2}, step: {step_num:3}, Red action: {friendly_output_red_action(info)}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Make the attack less likely to succeed.\n", + "\n", + "We can change the success probabilities within the data manipulation bot application. When the attack succeeds, the reward goes down.\n", + "\n", + "Setting the probabilities to 1.0 means the attack always succeeds - the reward will always drop\n", + "\n", + "Setting the probabilities to 0.0 means the attack always fails - the reward will never drop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Make attack always succeed.\n", + "change = yaml.safe_load(\"\"\"\n", + " applications:\n", + " - ref: data_manipulation_bot\n", + " type: DataManipulationBot\n", + " options:\n", + " port_scan_p_of_success: 1.0\n", + " data_manipulation_p_of_success: 1.0\n", + " payload: \"DELETE\"\n", + " server_ip: 192.168.1.14\n", + " - ref: client_1_web_browser\n", + " type: WebBrowser\n", + " options:\n", + " target_url: http://arcd.com/users/\n", + " - ref: client_1_database_client\n", + " type: DatabaseClient\n", + " options:\n", + " db_server_ip: 192.168.1.14\n", + "\"\"\")\n", + "\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + " cfg['simulation']['network']\n", + " for node in cfg['simulation']['network']['nodes']:\n", + " if node['ref'] in ['client_1', 'client_2']:\n", + " node['applications'] = change['applications']\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", + "env.reset()\n", + "for ep in range(5):\n", + " env.reset()\n", + " for step in range(36):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " if step_num == 35:\n", + " print(f\"Episode: {ep:2}, step: {step_num:3}, Reward: {reward:.2f}\" )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Make attack always fail.\n", + "change = yaml.safe_load(\"\"\"\n", + " applications:\n", + " - ref: data_manipulation_bot\n", + " type: DataManipulationBot\n", + " options:\n", + " port_scan_p_of_success: 0.0\n", + " data_manipulation_p_of_success: 0.0\n", + " payload: \"DELETE\"\n", + " server_ip: 192.168.1.14\n", + " - ref: client_1_web_browser\n", + " type: WebBrowser\n", + " options:\n", + " target_url: http://arcd.com/users/\n", + " - ref: client_1_database_client\n", + " type: DatabaseClient\n", + " options:\n", + " db_server_ip: 192.168.1.14\n", + "\"\"\")\n", + "\n", + "with open(data_manipulation_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + " cfg['simulation']['network']\n", + " for node in cfg['simulation']['network']['nodes']:\n", + " if node['ref'] in ['client_1', 'client_2']:\n", + " node['applications'] = change['applications']\n", + "\n", + "env = PrimaiteGymEnv(game_config = cfg)\n", + "env.reset()\n", + "for ep in range(5):\n", + " env.reset()\n", + " for step in range(36):\n", + " step_num = env.game.step_counter\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + " red = friendly_output_red_action(info)\n", + " if step_num == 35:\n", + " print(f\"Episode: {ep:2}, step: {step_num:3}, Reward: {reward:.2f}\" )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index d54503a3..86bc52cb 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -6,10 +6,13 @@ import gymnasium from gymnasium.core import ActType, ObsType from ray.rllib.env.multi_agent_env import MultiAgentEnv +from primaite import getLogger from primaite.game.agent.interface import ProxyAgent from primaite.game.game import PrimaiteGame from primaite.simulator import SIM_OUTPUT +_LOGGER = getLogger(__name__) + class PrimaiteGymEnv(gymnasium.Env): """ @@ -75,7 +78,7 @@ class PrimaiteGymEnv(gymnasium.Env): def reset(self, seed: Optional[int] = None) -> Tuple[ObsType, Dict[str, Any]]: """Reset the environment.""" - print( + _LOGGER.info( f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {self.agent.reward_function.total_reward}" ) From 3e495c4622a89e02501fe25ebb972ea1f0f95e80 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 5 Mar 2024 09:28:22 +0000 Subject: [PATCH 077/128] Cosmetic changes to notebook --- ...Data-Manipulation-Customising-Red-Agent.ipynb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 6fee18b1..779d89f6 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -120,11 +120,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Red Configuration\n", + "## Red Configuration\n", "\n", "There are two important parts of the YAML config for varying red agent behaviour.\n", "\n", - "#### The red agent settings\n", + "### Red agent settings\n", "Here is an annotated config for the red agent in the data manipulation scenario.\n", "```yaml\n", " - ref: data_manipulation_attacker # name of agent\n", @@ -172,7 +172,7 @@ " variance: 5 # the timing of attacks will vary by up to 5 steps earlier or later\n", "```\n", "\n", - "#### The settings of the red agent's attack application\n", + "### Malicious application settings\n", "The red agent uses an application called `DataManipulationBot` which leverages a node's `DatabaseClient` to send a malicious SQL query to the database server. Here's an annotated example of how this is configured in the yaml *(with impertinent config items omitted)*:\n", "```yaml\n", "simulation:\n", @@ -205,9 +205,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "## Editing red agent settings\n", + "\n", "### Removing randomness from attack timing\n", "\n", - "We can make the attacks happen at completely predictable intervals if we set the variance parameter of the red agent to 0." + "We can make the attacks happen at completely predictable intervals if we edit the red agent's settings to set variance to 0." ] }, { @@ -243,7 +245,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Making the start node always the same\n", + "### Making the start node always the same\n", "\n", "Normally, the agent randomly chooses between the nodes in its action space to send attacks from:" ] @@ -254,7 +256,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Open the config without changing anyhing\n", + "# Open the config without changing anything\n", "with open(data_manipulation_config_path(), 'r') as f:\n", " cfg = yaml.safe_load(f)\n", "\n", @@ -320,7 +322,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Make the attack less likely to succeed.\n", + "### Make the attack less likely to succeed.\n", "\n", "We can change the success probabilities within the data manipulation bot application. When the attack succeeds, the reward goes down.\n", "\n", From a7bfc56b98bd93f8c4043ffae678e72faa34f8f3 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 5 Mar 2024 11:21:49 +0000 Subject: [PATCH 078/128] Apply documentation changes based on PR review. --- CHANGELOG.md | 2 +- docs/source/configuration/io_settings.rst | 12 +----------- src/primaite/session/io.py | 12 +++++++----- src/primaite/session/session.py | 2 +- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8064a18e..cdf7b5c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the order of service health state - Fixed an issue where starting a node didn't start the services on it - Added support for SQL INSERT command. -- Added ability to log each agent's action choices each step to a JSON file. +- Added ability to log each agent's action choices in each step to a JSON file. diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index e5c6d2ce..f9704541 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -18,7 +18,6 @@ This section configures how PrimAITE saves data during simulation and training. checkpoint_interval: 10 # save_logs: True # save_transactions: False - # save_tensorboard_logs: False save_agent_actions: True save_step_metadata: False save_pcap_logs: False @@ -56,21 +55,12 @@ Defines how often to save the policy during training. *currently unused*. -``save_transactions`` ---------------------- - -*currently unused*. - -``save_tensorboard_logs`` -------------------------- - -*currently unused*. ``save_agent_actions`` Optional. Default value is ``True``. -If ``True``, this will create a JSON file each episode detailing every agent's action each step in that episode, formatted according to the CAOS format. This includes scripted, RL, and red agents. +If ``True``, this will create a JSON file each episode detailing every agent's action in each step of that episode, formatted according to the CAOS format. This includes scripted, RL, and red agents. ``save_step_metadata`` ---------------------- diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 22d9dbeb..3e21ed16 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -15,7 +15,7 @@ class PrimaiteIO: """ Class for managing session IO. - Currently it's handling path generation, but could expand to handle loading, transaction, tensorboard, and so on. + Currently it's handling path generation, but could expand to handle loading, transaction, and so on. """ class Settings(BaseModel): @@ -27,8 +27,6 @@ class PrimaiteIO: """Whether to save logs""" save_agent_actions: bool = True """Whether to save a log of all agents' actions every step.""" - save_transactions: bool = True - """Whether to save transactions, If true, the session path will have a transactions folder.""" save_step_metadata: bool = False """Whether to save the RL agents' action, environment state, and other data at every single step.""" save_pcap_logs: bool = False @@ -37,6 +35,12 @@ class PrimaiteIO: """Whether to save system logs.""" def __init__(self, settings: Optional[Settings] = None) -> None: + """ + Init the PrimaiteIO object. + + Note: Instantiating this object creates a new directory for outputs, and sets the global SIM_OUTPUT variable. + It is intended that this object is instantiated when a new environment is created. + """ self.settings = settings or PrimaiteIO.Settings() self.session_path: Path = self.generate_session_path() # set global SIM_OUTPUT path @@ -45,8 +49,6 @@ class PrimaiteIO: SIM_OUTPUT.save_sys_logs = self.settings.save_sys_logs self.agent_action_log: List[Dict] = [] - # warning TODO: must be careful not to re-initialise sessionIO because it will create a new path each time it's - # possible refactor needed def generate_session_path(self, timestamp: Optional[datetime] = None) -> Path: """Create a folder for the session and return the path to it.""" diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index 84dd9b2f..9c935ae3 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -61,7 +61,7 @@ class PrimaiteSession: """Primaite Game object for managing main simulation loop and agents.""" self.save_checkpoints: bool = False - """Whether to save chcekpoints.""" + """Whether to save checkpoints.""" self.checkpoint_interval: int = 10 """If save_checkpoints is true, checkpoints will be saved every checkpoint_interval episodes.""" From e117f94f43ad52f0064e0ecaf2fe46d606bbc209 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 5 Mar 2024 15:46:30 +0000 Subject: [PATCH 079/128] Minor doc fix --- docs/source/configuration/io_settings.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/configuration/io_settings.rst b/docs/source/configuration/io_settings.rst index f9704541..979dbfae 100644 --- a/docs/source/configuration/io_settings.rst +++ b/docs/source/configuration/io_settings.rst @@ -57,6 +57,7 @@ Defines how often to save the policy during training. ``save_agent_actions`` +---------------------- Optional. Default value is ``True``. From a900d59f7b24124739dcb8be02a241220319d0b1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 7 Mar 2024 12:15:30 +0000 Subject: [PATCH 080/128] Update NMNE to only count MNEs in the last step. --- .../simulator/network/hardware/base.py | 3 +- .../network/test_capture_nmne.py | 100 +++++++++++++++--- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 991913dd..36716f27 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -136,7 +136,8 @@ class NetworkInterface(SimComponent, ABC): } ) if CAPTURE_NMNE: - state.update({"nmne": self.nmne}) + state.update({"nmne": {k: v for k, v in self.nmne.items()}}) + self.nmne.clear() return state @abstractmethod diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index 85ac23e8..d48b3784 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -32,36 +32,106 @@ def test_capture_nmne(uc2_network): set_nmne_config(nmne_config) # Assert that initially, there are no captured MNEs on both web and database servers - assert web_server_nic.describe_state()["nmne"] == {} - assert db_server_nic.describe_state()["nmne"] == {} + assert web_server_nic.nmne == {} + assert db_server_nic.nmne == {} # Perform a "SELECT" query db_client.query("SELECT") # Check that it does not trigger an MNE capture. - assert web_server_nic.describe_state()["nmne"] == {} - assert db_server_nic.describe_state()["nmne"] == {} + assert web_server_nic.nmne == {} + assert db_server_nic.nmne == {} # Perform a "DELETE" query db_client.query("DELETE") # Check that the web server's outbound interface and the database server's inbound interface register the MNE - assert web_server_nic.describe_state()["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} - assert db_server_nic.describe_state()["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} + assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 1}}}} # Perform another "SELECT" query db_client.query("SELECT") # Check that no additional MNEs are captured - assert web_server_nic.describe_state()["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} - assert db_server_nic.describe_state()["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} + assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 1}}}} # Perform another "DELETE" query db_client.query("DELETE") # Check that the web server and database server interfaces register an additional MNE - assert web_server_nic.describe_state()["nmne"] == {"direction": {"outbound": {"keywords": {"*": 2}}}} - assert db_server_nic.describe_state()["nmne"] == {"direction": {"inbound": {"keywords": {"*": 2}}}} + assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 2}}}} + assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 2}}}} + + +def test_describe_state_nmne(uc2_network): + """ + Conducts a test to verify that Malicious Network Events (MNEs) are correctly represented in the nic state. + + This test involves a web server querying a database server and checks if the MNEs are captured + based on predefined keywords in the network configuration. Specifically, it checks the capture + of the "DELETE" SQL command as a malicious network event. It also checks that running describe_state + only shows MNEs since the last time describe_state was called. + """ + web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa + db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa + db_client.connect() + + db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa + + web_server_nic = web_server.network_interface[1] + db_server_nic = db_server.network_interface[1] + + # Set the NMNE configuration to capture DELETE queries as MNEs + nmne_config = { + "capture_nmne": True, # Enable the capture of MNEs + "nmne_capture_keywords": ["DELETE"], # Specify "DELETE" SQL command as a keyword for MNE detection + } + + # Apply the NMNE configuration settings + set_nmne_config(nmne_config) + + # Assert that initially, there are no captured MNEs on both web and database servers + web_server_nic_state = web_server_nic.describe_state() + db_server_nic_state = db_server_nic.describe_state() + assert web_server_nic_state["nmne"] == {} + assert db_server_nic_state["nmne"] == {} + + # Perform a "SELECT" query + db_client.query("SELECT") + + # Check that it does not trigger an MNE capture. + web_server_nic_state = web_server_nic.describe_state() + db_server_nic_state = db_server_nic.describe_state() + assert web_server_nic_state["nmne"] == {} + assert db_server_nic_state["nmne"] == {} + + # Perform a "DELETE" query + db_client.query("DELETE") + + # Check that the web server's outbound interface and the database server's inbound interface register the MNE + web_server_nic_state = web_server_nic.describe_state() + db_server_nic_state = db_server_nic.describe_state() + assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} + + # Perform another "SELECT" query + db_client.query("SELECT") + + # Check that no additional MNEs are captured + web_server_nic_state = web_server_nic.describe_state() + db_server_nic_state = db_server_nic.describe_state() + assert web_server_nic_state["nmne"] == {} + assert db_server_nic_state["nmne"] == {} + + # Perform another "DELETE" query + db_client.query("DELETE") + + # Check that the web server and database server interfaces register an additional MNE + web_server_nic_state = web_server_nic.describe_state() + db_server_nic_state = db_server_nic.describe_state() + assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} def test_capture_nmne_observations(uc2_network): @@ -97,13 +167,15 @@ def test_capture_nmne_observations(uc2_network): web_server_nic_obs = NicObservation(where=["network", "nodes", "web_server", "NICs", 1]) # Iterate through a set of test cases to simulate multiple DELETE queries - for i in range(1, 20): + for i in range(0, 20): # Perform a "DELETE" query each iteration - db_client.query("DELETE") + for j in range(i): + db_client.query("DELETE") # Observe the current state of NMNEs from the NICs of both the database and web servers - db_nic_obs = db_server_nic_obs.observe(sim.describe_state())["nmne"] - web_nic_obs = web_server_nic_obs.observe(sim.describe_state())["nmne"] + state = sim.describe_state() + db_nic_obs = db_server_nic_obs.observe(state)["nmne"] + web_nic_obs = web_server_nic_obs.observe(state)["nmne"] # Define expected NMNE values based on the iteration count if i > 10: From 2547361dafdd4340d734de9c3ca045bea99c1c77 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 7 Mar 2024 13:52:26 +0000 Subject: [PATCH 081/128] Change default reward weights --- .../config/_package_data/example_config.yaml | 10 ++++----- .../example_config_2_rl_agents.yaml | 21 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index d0ba61b0..dffb40ea 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -691,25 +691,25 @@ agents: reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.34 + weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.33 + weight: 0.25 options: node_hostname: client_1 - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.33 + weight: 0.25 options: node_hostname: client_2 - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.1 + weight: 0.05 options: node_hostname: client_1 - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.1 + weight: 0.05 options: node_hostname: client_2 diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml index 575182a8..f7288cb0 100644 --- a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml +++ b/src/primaite/config/_package_data/example_config_2_rl_agents.yaml @@ -695,25 +695,25 @@ agents: reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.34 + weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.33 + weight: 0.25 options: node_hostname: client_1 - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.33 + weight: 0.25 options: node_hostname: client_2 - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.1 + weight: 0.05 options: node_hostname: client_1 - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.1 + weight: 0.05 options: node_hostname: client_2 @@ -1251,29 +1251,28 @@ agents: - node_name: security_suite nic_num: 2 - reward_function: reward_components: - type: DATABASE_FILE_INTEGRITY - weight: 0.34 + weight: 0.40 options: node_hostname: database_server folder_name: database file_name: database.db - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.33 + weight: 0.25 options: node_hostname: client_1 - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.33 + weight: 0.25 options: node_hostname: client_2 - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.1 + weight: 0.05 options: node_hostname: client_1 - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.1 + weight: 0.05 options: node_hostname: client_2 From 17d4807660b4a5deebd02f82970cb711c50c9601 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 7 Mar 2024 14:33:21 +0000 Subject: [PATCH 082/128] Rename configs --- .../{example_config.yaml => data_manipulation.yaml} | 0 ...mple_config_2_rl_agents.yaml => data_manipulation_marl.yaml} | 0 src/primaite/config/load.py | 2 +- src/primaite/notebooks/training_example_ray_multi_agent.ipynb | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/primaite/config/_package_data/{example_config.yaml => data_manipulation.yaml} (100%) rename src/primaite/config/_package_data/{example_config_2_rl_agents.yaml => data_manipulation_marl.yaml} (100%) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/data_manipulation.yaml similarity index 100% rename from src/primaite/config/_package_data/example_config.yaml rename to src/primaite/config/_package_data/data_manipulation.yaml diff --git a/src/primaite/config/_package_data/example_config_2_rl_agents.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml similarity index 100% rename from src/primaite/config/_package_data/example_config_2_rl_agents.yaml rename to src/primaite/config/_package_data/data_manipulation_marl.yaml diff --git a/src/primaite/config/load.py b/src/primaite/config/load.py index 6bd0d80d..d5acd690 100644 --- a/src/primaite/config/load.py +++ b/src/primaite/config/load.py @@ -37,7 +37,7 @@ def data_manipulation_config_path() -> Path: :return: Path to the example config. :rtype: Path """ - path = _EXAMPLE_CFG / "example_config.yaml" + path = _EXAMPLE_CFG / "data_manipulation.yaml" if not path.exists(): msg = f"Example config does not exist: {path}. Have you run `primaite setup`?" _LOGGER.error(msg) diff --git a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb index 4ef02443..76623697 100644 --- a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb @@ -35,7 +35,7 @@ "\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/example_config_2_rl_agents.yaml', 'r') as f:\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", "ray.init(local_mode=True)" From 76752fd9af1d7a2d5b37247963e4d35873c10755 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 7 Mar 2024 14:44:44 +0000 Subject: [PATCH 083/128] Change the nmne clear to happen at apply_timestep instead of within describe_state --- src/primaite/simulator/network/hardware/base.py | 13 ++++++++++++- .../integration_tests/network/test_capture_nmne.py | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 36716f27..82fae164 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -137,7 +137,6 @@ class NetworkInterface(SimComponent, ABC): ) if CAPTURE_NMNE: state.update({"nmne": {k: v for k, v in self.nmne.items()}}) - self.nmne.clear() return state @abstractmethod @@ -254,6 +253,15 @@ class NetworkInterface(SimComponent, ABC): """ return f"Port {self.port_name if self.port_name else self.port_num}: {self.mac_address}" + def apply_timestep(self, timestep: int) -> None: + """ + Apply a timestep evolution to this component. + + This just clears the nmne count back to 0.tests/integration_tests/network/test_capture_nmne.py + """ + super().apply_timestep(timestep=timestep) + self.nmne.clear() + class WiredNetworkInterface(NetworkInterface, ABC): """ @@ -884,6 +892,9 @@ class Node(SimComponent): """ super().apply_timestep(timestep=timestep) + for network_interface in self.network_interfaces.values(): + network_interface.apply_timestep(timestep=timestep) + # count down to boot up if self.start_up_countdown > 0: self.start_up_countdown -= 1 diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index d48b3784..698bfc72 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -94,6 +94,7 @@ def test_describe_state_nmne(uc2_network): # Assert that initially, there are no captured MNEs on both web and database servers web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() + uc2_network.apply_timestep(timestep=0) assert web_server_nic_state["nmne"] == {} assert db_server_nic_state["nmne"] == {} @@ -103,6 +104,7 @@ def test_describe_state_nmne(uc2_network): # Check that it does not trigger an MNE capture. web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() + uc2_network.apply_timestep(timestep=0) assert web_server_nic_state["nmne"] == {} assert db_server_nic_state["nmne"] == {} @@ -112,6 +114,7 @@ def test_describe_state_nmne(uc2_network): # Check that the web server's outbound interface and the database server's inbound interface register the MNE web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() + uc2_network.apply_timestep(timestep=0) assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} @@ -121,6 +124,7 @@ def test_describe_state_nmne(uc2_network): # Check that no additional MNEs are captured web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() + uc2_network.apply_timestep(timestep=0) assert web_server_nic_state["nmne"] == {} assert db_server_nic_state["nmne"] == {} @@ -130,6 +134,7 @@ def test_describe_state_nmne(uc2_network): # Check that the web server and database server interfaces register an additional MNE web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() + uc2_network.apply_timestep(timestep=0) assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} @@ -190,3 +195,4 @@ def test_capture_nmne_observations(uc2_network): # Assert that the observed NMNEs match the expected values for both NICs assert web_nic_obs["outbound"] == expected_nmne assert db_nic_obs["inbound"] == expected_nmne + uc2_network.apply_timestep(timestep=0) From 618da8abe9ebb0b471c8b88ca49db87090ae4855 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 7 Mar 2024 15:25:11 +0000 Subject: [PATCH 084/128] Rename notebooks --- ..._ray_multi_agent.ipynb => Training-an-RLLIB-MARL-System.ipynb} | 0 ...ample_ray_single_agent.ipynb => Training-an-RLLib-Agent.ipynb} | 0 .../{training_example_sb3.ipynb => Training-an-SB3-Agent.ipynb} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/primaite/notebooks/{training_example_ray_multi_agent.ipynb => Training-an-RLLIB-MARL-System.ipynb} (100%) rename src/primaite/notebooks/{training_example_ray_single_agent.ipynb => Training-an-RLLib-Agent.ipynb} (100%) rename src/primaite/notebooks/{training_example_sb3.ipynb => Training-an-SB3-Agent.ipynb} (100%) diff --git a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb b/src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb similarity index 100% rename from src/primaite/notebooks/training_example_ray_multi_agent.ipynb rename to src/primaite/notebooks/Training-an-RLLIB-MARL-System.ipynb diff --git a/src/primaite/notebooks/training_example_ray_single_agent.ipynb b/src/primaite/notebooks/Training-an-RLLib-Agent.ipynb similarity index 100% rename from src/primaite/notebooks/training_example_ray_single_agent.ipynb rename to src/primaite/notebooks/Training-an-RLLib-Agent.ipynb diff --git a/src/primaite/notebooks/training_example_sb3.ipynb b/src/primaite/notebooks/Training-an-SB3-Agent.ipynb similarity index 100% rename from src/primaite/notebooks/training_example_sb3.ipynb rename to src/primaite/notebooks/Training-an-SB3-Agent.ipynb From e9eef2b4c09d12d7f42624a9667b7be1597f6b80 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 8 Mar 2024 11:16:27 +0000 Subject: [PATCH 085/128] #2350: add num_access, num_file_deletions and num_creations to file system --- src/primaite/simulator/file_system/file.py | 19 +++++++ .../simulator/file_system/file_system.py | 25 +++++++-- src/primaite/simulator/file_system/folder.py | 1 + .../system/applications/application.py | 10 ++++ .../system/applications/database_client.py | 2 + .../red_applications/data_manipulation_bot.py | 2 + .../system/applications/web_browser.py | 2 + .../_file_system/test_file_system.py | 52 ++++++++++++++++++- 8 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index d9b02e8e..0897178d 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -38,6 +38,8 @@ class File(FileSystemItemABC): "The Path if real is True." sim_root: Optional[Path] = None "Root path of the simulation." + num_access: int = 0 + "Number of times the file was accessed in the current step." def __init__(self, **kwargs): """ @@ -93,11 +95,23 @@ class File(FileSystemItemABC): return os.path.getsize(self.sim_path) return self.sim_size + def apply_timestep(self, timestep: int) -> None: + """ + Apply a timestep to the file. + + :param timestep: The current timestep of the simulation. + """ + super().apply_timestep(timestep=timestep) + + # reset the number of accesses to 0 + self.num_access = 0 + def describe_state(self) -> Dict: """Produce a dictionary describing the current state of this object.""" state = super().describe_state() state["size"] = self.size state["file_type"] = self.file_type.name + state["num_access"] = self.num_access return state def scan(self) -> None: @@ -106,6 +120,7 @@ class File(FileSystemItemABC): self.sys_log.error(f"Unable to scan deleted file {self.folder_name}/{self.name}") return + self.num_access += 1 # file was accessed path = self.folder.name + "/" + self.name self.sys_log.info(f"Scanning file {self.sim_path if self.sim_path else path}") self.visible_health_status = self.health_status @@ -160,6 +175,7 @@ class File(FileSystemItemABC): if self.health_status == FileSystemItemHealthStatus.CORRUPT: self.health_status = FileSystemItemHealthStatus.GOOD + self.num_access += 1 # file was accessed path = self.folder.name + "/" + self.name self.sys_log.info(f"Repaired file {self.sim_path if self.sim_path else path}") @@ -173,6 +189,7 @@ class File(FileSystemItemABC): if self.health_status == FileSystemItemHealthStatus.GOOD: self.health_status = FileSystemItemHealthStatus.CORRUPT + self.num_access += 1 # file was accessed path = self.folder.name + "/" + self.name self.sys_log.info(f"Corrupted file {self.sim_path if self.sim_path else path}") @@ -185,6 +202,7 @@ class File(FileSystemItemABC): if self.health_status == FileSystemItemHealthStatus.CORRUPT: self.health_status = FileSystemItemHealthStatus.GOOD + self.num_access += 1 # file was accessed path = self.folder.name + "/" + self.name self.sys_log.info(f"Restored file {self.sim_path if self.sim_path else path}") @@ -194,5 +212,6 @@ class File(FileSystemItemABC): self.sys_log.error(f"Unable to delete an already deleted file {self.folder_name}/{self.name}") return + self.num_access += 1 # file was accessed self.deleted = True self.sys_log.info(f"File deleted {self.folder_name}/{self.name}") diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 8fd4e5d7..52144c72 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -27,6 +27,10 @@ class FileSystem(SimComponent): "Instance of SysLog used to create system logs." sim_root: Path "Root path of the simulation." + num_file_creations: int = 0 + "Number of file creations in the current step." + num_file_deletions: int = 0 + "Number of file deletions in the current step." def __init__(self, **kwargs): super().__init__(**kwargs) @@ -248,6 +252,8 @@ class FileSystem(SimComponent): ) folder.add_file(file) self._file_request_manager.add_request(name=file.name, request_type=RequestType(func=file._request_manager)) + # increment file creation + self.num_file_creations += 1 return file def get_file(self, folder_name: str, file_name: str, include_deleted: Optional[bool] = False) -> Optional[File]: @@ -308,6 +314,8 @@ class FileSystem(SimComponent): if folder: file = folder.get_file(file_name) if file: + # increment file creation + self.num_file_deletions += 1 folder.remove_file(file) def delete_file_by_id(self, folder_uuid: str, file_uuid: str): @@ -337,15 +345,14 @@ class FileSystem(SimComponent): """ file = self.get_file(folder_name=src_folder_name, file_name=src_file_name) if file: - src_folder = file.folder - # remove file from src - src_folder.remove_file(file) + self.delete_file(folder_name=file.folder_name, file_name=file.name) dst_folder = self.get_folder(folder_name=dst_folder_name) if not dst_folder: dst_folder = self.create_folder(dst_folder_name) # add file to dst dst_folder.add_file(file) + self.num_file_creations += 1 if file.real: old_sim_path = file.sim_path file.sim_path = file.sim_root / file.path @@ -373,6 +380,10 @@ class FileSystem(SimComponent): folder_name=dst_folder.name, **file.model_dump(exclude={"uuid", "folder_id", "folder_name", "sim_path"}), ) + self.num_file_creations += 1 + # increment access counter + file.num_access += 1 + dst_folder.add_file(file_copy, force=True) if file.real: @@ -390,12 +401,20 @@ class FileSystem(SimComponent): state = super().describe_state() state["folders"] = {folder.name: folder.describe_state() for folder in self.folders.values()} state["deleted_folders"] = {folder.name: folder.describe_state() for folder in self.deleted_folders.values()} + state["num_file_creations"] = self.num_file_creations + state["num_file_deletions"] = self.num_file_deletions return state def apply_timestep(self, timestep: int) -> None: """Apply time step to FileSystem and its child folders and files.""" super().apply_timestep(timestep=timestep) + # reset number of file creations + self.num_file_creations = 0 + + # reset number of file deletions + self.num_file_deletions = 0 + # apply timestep to folders for folder_id in self.folders: self.folders[folder_id].apply_timestep(timestep=timestep) diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 771dc7a0..3ddc1e5f 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -131,6 +131,7 @@ class Folder(FileSystemItemABC): file.scan() if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: self.visible_health_status = FileSystemItemHealthStatus.CORRUPT + self.visible_health_status = self.health_status def _reveal_to_red_timestep(self) -> None: """Apply reveal to red timestep.""" diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 513606a9..74013681 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -59,6 +59,16 @@ class Application(IOSoftware): ) return state + def apply_timestep(self, timestep: int) -> None: + """ + Apply a timestep to the application. + + :param timestep: The current timestep of the simulation. + """ + super().apply_timestep(timestep=timestep) + + self.num_executions = 0 # reset number of executions + def _can_perform_action(self) -> bool: """ Checks if the application can perform actions. diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 7b259ff4..302aca7e 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -76,6 +76,8 @@ class DatabaseClient(Application): if not self._can_perform_action(): return False + self.num_executions += 1 # trying to connect counts as an execution + if not connection_id: connection_id = str(uuid4()) 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 ee98ea8e..cce9fe8d 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 @@ -194,6 +194,8 @@ class DataManipulationBot(Application): """ if not self._can_perform_action(): return + + self.num_executions += 1 if self.server_ip_address and self.payload: self.sys_log.info(f"{self.name}: Running") self._logon() diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 9fa86328..90eda426 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -80,6 +80,8 @@ class WebBrowser(Application): if not self._can_perform_action(): return False + self.num_executions += 1 # trying to connect counts as an execution + # reset latest response self.latest_response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND) 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 4defc80c..05824834 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,7 +1,9 @@ import pytest +from primaite.simulator.file_system.file import File from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.file_system.file_type import FileType +from primaite.simulator.file_system.folder import Folder def test_create_folder_and_file(file_system): @@ -14,8 +16,15 @@ def test_create_folder_and_file(file_system): assert len(file_system.get_folder("test_folder").files) == 1 + assert file_system.num_file_creations == 1 + assert file_system.get_folder("test_folder").get_file("test_file.txt") + file_system.apply_timestep(0) + + # num file creations should reset + assert file_system.num_file_creations == 0 + file_system.show(full=True) @@ -23,24 +32,37 @@ def test_create_file_no_folder(file_system): """Tests that creating a file without a folder creates a folder and sets that as the file's parent.""" file = file_system.create_file(file_name="test_file.txt", size=10) assert len(file_system.folders) is 1 + assert file_system.num_file_creations == 1 assert file_system.get_folder("root").get_file("test_file.txt") == file assert file_system.get_folder("root").get_file("test_file.txt").file_type == FileType.TXT assert file_system.get_folder("root").get_file("test_file.txt").size == 10 + file_system.apply_timestep(0) + + # num file creations should reset + assert file_system.num_file_creations == 0 + file_system.show(full=True) def test_delete_file(file_system): """Tests that a file can be deleted.""" - file_system.create_file(file_name="test_file.txt") + file = file_system.create_file(file_name="test_file.txt") assert len(file_system.folders) == 1 assert len(file_system.get_folder("root").files) == 1 file_system.delete_file(folder_name="root", file_name="test_file.txt") + assert file.num_access == 1 + assert file_system.num_file_deletions == 1 assert len(file_system.folders) == 1 assert len(file_system.get_folder("root").files) == 0 assert len(file_system.get_folder("root").deleted_files) == 1 + file_system.apply_timestep(0) + + # num file deletions should reset + assert file_system.num_file_deletions == 0 + file_system.show(full=True) @@ -54,6 +76,7 @@ def test_delete_non_existent_file(file_system): # deleting should not change how many files are in folder file_system.delete_file(folder_name="root", file_name="does_not_exist!") + assert file_system.num_file_deletions == 0 # should still only be one folder assert len(file_system.folders) == 1 @@ -96,6 +119,7 @@ def test_create_duplicate_file(file_system): assert len(file_system.folders) is 2 file_system.create_file(file_name="test_file.txt", folder_name="test_folder") + assert file_system.num_file_creations == 1 assert len(file_system.get_folder("test_folder").files) == 1 @@ -103,6 +127,7 @@ def test_create_duplicate_file(file_system): file_system.create_file(file_name="test_file.txt", folder_name="test_folder") assert len(file_system.get_folder("test_folder").files) == 1 + assert file_system.num_file_creations == 1 file_system.show(full=True) @@ -136,13 +161,24 @@ def test_move_file(file_system): assert len(file_system.get_folder("src_folder").files) == 1 assert len(file_system.get_folder("dst_folder").files) == 0 + assert file_system.num_file_deletions == 0 + assert file_system.num_file_creations == 1 file_system.move_file(src_folder_name="src_folder", src_file_name="test_file.txt", dst_folder_name="dst_folder") + assert file_system.num_file_deletions == 1 + assert file_system.num_file_creations == 2 + assert file.num_access == 1 assert len(file_system.get_folder("src_folder").files) == 0 assert len(file_system.get_folder("dst_folder").files) == 1 assert file_system.get_file("dst_folder", "test_file.txt").uuid == original_uuid + file_system.apply_timestep(0) + + # num file creations and deletions should reset + assert file_system.num_file_creations == 0 + assert file_system.num_file_deletions == 0 + file_system.show(full=True) @@ -152,17 +188,25 @@ def test_copy_file(file_system): file_system.create_folder(folder_name="dst_folder") file = file_system.create_file(file_name="test_file.txt", size=10, folder_name="src_folder", real=True) + assert file_system.num_file_creations == 1 original_uuid = file.uuid assert len(file_system.get_folder("src_folder").files) == 1 assert len(file_system.get_folder("dst_folder").files) == 0 file_system.copy_file(src_folder_name="src_folder", src_file_name="test_file.txt", dst_folder_name="dst_folder") + assert file_system.num_file_creations == 2 + assert file.num_access == 1 assert len(file_system.get_folder("src_folder").files) == 1 assert len(file_system.get_folder("dst_folder").files) == 1 assert file_system.get_file("dst_folder", "test_file.txt").uuid != original_uuid + file_system.apply_timestep(0) + + # num file creations should reset + assert file_system.num_file_creations == 0 + file_system.show(full=True) @@ -172,13 +216,17 @@ def test_get_file(file_system): file1: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder") file2: File = file_system.create_file(file_name="test_file2.txt", folder_name="test_folder") - folder.remove_file(file2) + file_system.delete_file("test_folder", "test_file2.txt") + # file 2 was accessed before being deleted + assert file2.num_access == 1 assert file_system.get_file_by_id(file_uuid=file1.uuid, folder_uuid=folder.uuid) is not None assert file_system.get_file_by_id(file_uuid=file2.uuid, folder_uuid=folder.uuid) is None assert file_system.get_file_by_id(file_uuid=file2.uuid, folder_uuid=folder.uuid, include_deleted=True) is not None assert file_system.get_file_by_id(file_uuid=file2.uuid, include_deleted=True) is not None + assert file2.num_access == 1 # cannot access deleted file + file_system.delete_folder(folder_name="test_folder") assert file_system.get_file_by_id(file_uuid=file2.uuid, include_deleted=True) is not None From d331224b455efb8a7da0b58b2f5e847e0011c00b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 8 Mar 2024 12:42:22 +0000 Subject: [PATCH 086/128] Start introducing RequestResponse --- src/primaite/interface/__init__.py | 0 src/primaite/interface/request.py | 29 +++++++++++++++++++++ src/primaite/simulator/core.py | 24 ++++++++++------- src/primaite/simulator/domain/controller.py | 1 + src/primaite/simulator/sim_container.py | 4 ++- 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 src/primaite/interface/__init__.py create mode 100644 src/primaite/interface/request.py diff --git a/src/primaite/interface/__init__.py b/src/primaite/interface/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py new file mode 100644 index 00000000..10ce6254 --- /dev/null +++ b/src/primaite/interface/request.py @@ -0,0 +1,29 @@ +from typing import Dict, Literal + +from pydantic import BaseModel, ConfigDict + + +class RequestResponse(BaseModel): + """Schema for generic request responses.""" + + model_config = ConfigDict(extra="forbid") + """Cannot have extra fields in the response. Anything custom goes into the data field.""" + + status: Literal["pending", "success", "failure"] = "pending" + """ + What is the current status of the request: + - pending - the request has not been received yet, or it has been received but it's still being processed. + - success - the request has successfully been received and processed. + - failure - the request could not reach it's intended target or it was rejected. + + Note that the failure status should only be used when the request cannot be processed, for instance when the + target SimComponent doesn't exist, or is in an OFF state that prevents it from accepting requests. If the + request is received by the target and the associated action is executed, but couldn't be completed due to + downstream factors, the request was still successfully received, it's just that the result wasn't what was + intended. + """ + + data: Dict = {} + """Catch-all place to provide any additional data that was generated as a response to the request.""" + # TODO: currently, status and data have default values, because I don't want to interrupt existing functionality too + # much. However, in the future we might consider making them mandatory. diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 6ab7c6e3..9ea59305 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -1,12 +1,13 @@ # flake8: noqa """Core of the PrimAITE Simulator.""" from abc import abstractmethod -from typing import Callable, Dict, List, Optional, Union +from typing import Callable, Dict, List, Literal, Optional, Union from uuid import uuid4 -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, validate_call from primaite import getLogger +from primaite.interface.request import RequestResponse _LOGGER = getLogger(__name__) @@ -22,7 +23,7 @@ class RequestPermissionValidator(BaseModel): @abstractmethod def __call__(self, request: List[str], context: Dict) -> bool: - """Use the request and context paramters to decide whether the request should be permitted.""" + """Use the request and context parameters to decide whether the request should be permitted.""" pass @@ -42,7 +43,7 @@ class RequestType(BaseModel): the request can be performed or not. """ - func: Callable[[List[str], Dict], None] + func: Callable[[List[str], Dict], RequestResponse] """ ``func`` is a function that accepts a request and a context dict. Typically this would be a lambda function that invokes a class method of your SimComponent. For example if the component is a node and the request type is for @@ -71,7 +72,8 @@ class RequestManager(BaseModel): request_types: Dict[str, RequestType] = {} """maps request name to an RequestType object.""" - def __call__(self, request: Callable[[List[str], Dict], None], context: Dict) -> None: + @validate_call + def __call__(self, request: List[str], context: Dict) -> RequestResponse: """ Process an request request. @@ -84,23 +86,25 @@ class RequestManager(BaseModel): :raises RuntimeError: If the request parameter does not have a valid request name as the first item. """ request_key = request[0] + request_options = request[1:] if request_key not in self.request_types: msg = ( f"Request {request} could not be processed because {request_key} is not a valid request name", "within this RequestManager", ) - _LOGGER.error(msg) - raise RuntimeError(msg) + # _LOGGER.error(msg) + # raise RuntimeError(msg) + _LOGGER.debug(msg) + return RequestResponse(status="failure", data={"reason": msg}) request_type = self.request_types[request_key] - request_options = request[1:] if not request_type.validator(request_options, context): _LOGGER.debug(f"Request {request} was denied due to insufficient permissions") - return + return RequestResponse(status="failure", data={"reason": "request validation failed"}) - request_type.func(request_options, context) + return request_type.func(request_options, context) def add_request(self, name: str, request_type: RequestType) -> None: """ diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index bc428743..0936b5f8 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -87,6 +87,7 @@ class DomainController(SimComponent): "account", RequestType( func=lambda request, context: self.accounts[request.pop(0)].apply_request(request, context), + # TODO: not sure what should get returned here, revisit validator=GroupMembershipValidator(allowed_groups=[AccountGroup.DOMAIN_ADMIN]), ), ) diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index a2285d92..2f603f3a 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -1,5 +1,6 @@ from typing import Dict +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.domain.controller import DomainController from primaite.simulator.network.container import Network @@ -31,7 +32,8 @@ class Simulation(SimComponent): rm.add_request("network", RequestType(func=self.network._request_manager)) # pass through domain requests to the domain object rm.add_request("domain", RequestType(func=self.domain._request_manager)) - rm.add_request("do_nothing", RequestType(func=lambda request, context: ())) + # if 'do_nothing' is requested, just return a success + rm.add_request("do_nothing", RequestType(func=lambda request, context: RequestResponse(status="success"))) return rm def describe_state(self) -> Dict: From b13725721d2a636e77334954a1f59de16c11fcbb Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 8 Mar 2024 13:49:00 +0000 Subject: [PATCH 087/128] #2350: splitting observations into separate files --- src/primaite/game/agent/interface.py | 21 +- .../game/agent/observations/__init__.py | 0 .../agent/observations/agent_observations.py | 188 ++++++++++++++ .../agent/observations/observation_manager.py | 73 ++++++ .../agent/{ => observations}/observations.py | 234 ------------------ .../game/agent/scripted_agents/__init__.py | 0 .../data_manipulation_bot.py | 0 .../probabilistic_agent.py} | 2 +- .../agent/scripted_agents/random_agent.py | 21 ++ src/primaite/game/game.py | 6 +- tests/conftest.py | 3 +- ...software_installation_and_configuration.py | 2 +- .../game_layer/test_actions.py | 18 +- .../game_layer/test_observations.py | 2 +- .../network/test_capture_nmne.py | 2 +- .../_game/_agent/test_probabilistic_agent.py | 5 +- 16 files changed, 300 insertions(+), 277 deletions(-) create mode 100644 src/primaite/game/agent/observations/__init__.py create mode 100644 src/primaite/game/agent/observations/agent_observations.py create mode 100644 src/primaite/game/agent/observations/observation_manager.py rename src/primaite/game/agent/{ => observations}/observations.py (79%) create mode 100644 src/primaite/game/agent/scripted_agents/__init__.py rename src/primaite/game/agent/{ => scripted_agents}/data_manipulation_bot.py (100%) rename src/primaite/game/agent/{scripted_agents.py => scripted_agents/probabilistic_agent.py} (97%) create mode 100644 src/primaite/game/agent/scripted_agents/random_agent.py diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 88848479..e641fabb 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -6,7 +6,7 @@ from gymnasium.core import ActType, ObsType from pydantic import BaseModel, model_validator from primaite.game.agent.actions import ActionManager -from primaite.game.agent.observations import ObservationManager +from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction if TYPE_CHECKING: @@ -146,23 +146,10 @@ class AbstractAgent(ABC): class AbstractScriptedAgent(AbstractAgent): """Base class for actors which generate their own behaviour.""" - pass - - -class RandomAgent(AbstractScriptedAgent): - """Agent that ignores its observation and acts completely at random.""" - + @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: - """Sample the action space randomly. - - :param obs: Current observation for this agent, not used in RandomAgent - :type obs: ObsType - :param timestep: The current simulation timestep, not used in RandomAgent - :type timestep: int - :return: Action formatted in CAOS format - :rtype: Tuple[str, Dict] - """ - return self.action_manager.get_action(self.action_manager.space.sample()) + """Return an action to be taken in the environment.""" + return super().get_action(obs=obs, timestep=timestep) class ProxyAgent(AbstractAgent): diff --git a/src/primaite/game/agent/observations/__init__.py b/src/primaite/game/agent/observations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/game/agent/observations/agent_observations.py b/src/primaite/game/agent/observations/agent_observations.py new file mode 100644 index 00000000..522cdb59 --- /dev/null +++ b/src/primaite/game/agent/observations/agent_observations.py @@ -0,0 +1,188 @@ +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING + +from gymnasium import spaces + +from primaite.game.agent.observations.observations import ( + AbstractObservation, + AclObservation, + ICSObservation, + LinkObservation, + NodeObservation, + NullObservation, +) + +if TYPE_CHECKING: + from primaite.game.game import PrimaiteGame + + +class UC2BlueObservation(AbstractObservation): + """Container for all observations used by the blue agent in UC2. + + TODO: there's no real need for a UC2 blue container class, we should be able to simply use the observation handler + for the purpose of compiling several observation components. + """ + + def __init__( + self, + nodes: List[NodeObservation], + links: List[LinkObservation], + acl: AclObservation, + ics: ICSObservation, + where: Optional[List[str]] = None, + ) -> None: + """Initialise UC2 blue observation. + + :param nodes: List of node observations + :type nodes: List[NodeObservation] + :param links: List of link observations + :type links: List[LinkObservation] + :param acl: The Access Control List observation + :type acl: AclObservation + :param ics: The ICS observation + :type ics: ICSObservation + :param where: Where in the simulation state dict to find information. Not used in this particular observation + because it only compiles other observations and doesn't contribute any new information, defaults to None + :type where: Optional[List[str]], optional + """ + super().__init__() + self.where: Optional[Tuple[str]] = where + + self.nodes: List[NodeObservation] = nodes + self.links: List[LinkObservation] = links + self.acl: AclObservation = acl + self.ics: ICSObservation = ics + + self.default_observation: Dict = { + "NODES": {i + 1: n.default_observation for i, n in enumerate(self.nodes)}, + "LINKS": {i + 1: l.default_observation for i, l in enumerate(self.links)}, + "ACL": self.acl.default_observation, + "ICS": self.ics.default_observation, + } + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + + obs = {} + obs["NODES"] = {i + 1: node.observe(state) for i, node in enumerate(self.nodes)} + obs["LINKS"] = {i + 1: link.observe(state) for i, link in enumerate(self.links)} + obs["ACL"] = self.acl.observe(state) + obs["ICS"] = self.ics.observe(state) + + return obs + + @property + def space(self) -> spaces.Space: + """ + Gymnasium space object describing the observation space shape. + + :return: Space + :rtype: spaces.Space + """ + return spaces.Dict( + { + "NODES": spaces.Dict({i + 1: node.space for i, node in enumerate(self.nodes)}), + "LINKS": spaces.Dict({i + 1: link.space for i, link in enumerate(self.links)}), + "ACL": self.acl.space, + "ICS": self.ics.space, + } + ) + + @classmethod + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "UC2BlueObservation": + """Create UC2 blue observation from a config. + + :param config: Dictionary containing the configuration for this UC2 blue observation. This includes the nodes, + links, ACL and ICS observations. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + :return: Constructed UC2 blue observation + :rtype: UC2BlueObservation + """ + node_configs = config["nodes"] + + num_services_per_node = config["num_services_per_node"] + num_folders_per_node = config["num_folders_per_node"] + num_files_per_folder = config["num_files_per_folder"] + num_nics_per_node = config["num_nics_per_node"] + nodes = [ + NodeObservation.from_config( + config=n, + game=game, + num_services_per_node=num_services_per_node, + num_folders_per_node=num_folders_per_node, + num_files_per_folder=num_files_per_folder, + num_nics_per_node=num_nics_per_node, + ) + for n in node_configs + ] + + link_configs = config["links"] + links = [LinkObservation.from_config(config=link, game=game) for link in link_configs] + + acl_config = config["acl"] + acl = AclObservation.from_config(config=acl_config, game=game) + + ics_config = config["ics"] + ics = ICSObservation.from_config(config=ics_config, game=game) + new = cls(nodes=nodes, links=links, acl=acl, ics=ics, where=["network"]) + return new + + +class UC2RedObservation(AbstractObservation): + """Container for all observations used by the red agent in UC2.""" + + def __init__(self, nodes: List[NodeObservation], where: Optional[List[str]] = None) -> None: + super().__init__() + self.where: Optional[List[str]] = where + self.nodes: List[NodeObservation] = nodes + + self.default_observation: Dict = { + "NODES": {i + 1: n.default_observation for i, n in enumerate(self.nodes)}, + } + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation.""" + if self.where is None: + return self.default_observation + + obs = {} + obs["NODES"] = {i + 1: node.observe(state) for i, node in enumerate(self.nodes)} + return obs + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape.""" + return spaces.Dict( + { + "NODES": spaces.Dict({i + 1: node.space for i, node in enumerate(self.nodes)}), + } + ) + + @classmethod + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "UC2RedObservation": + """ + Create UC2 red observation from a config. + + :param config: Dictionary containing the configuration for this UC2 red observation. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + """ + node_configs = config["nodes"] + nodes = [NodeObservation.from_config(config=cfg, game=game) for cfg in node_configs] + return cls(nodes=nodes, where=["network"]) + + +class UC2GreenObservation(NullObservation): + """Green agent observation. As the green agent's actions don't depend on the observation, this is empty.""" + + pass diff --git a/src/primaite/game/agent/observations/observation_manager.py b/src/primaite/game/agent/observations/observation_manager.py new file mode 100644 index 00000000..400345fa --- /dev/null +++ b/src/primaite/game/agent/observations/observation_manager.py @@ -0,0 +1,73 @@ +from typing import Dict, TYPE_CHECKING + +from gymnasium.core import ObsType + +from primaite.game.agent.observations.agent_observations import ( + UC2BlueObservation, + UC2GreenObservation, + UC2RedObservation, +) +from primaite.game.agent.observations.observations import AbstractObservation + +if TYPE_CHECKING: + from primaite.game.game import PrimaiteGame + + +class ObservationManager: + """ + Manage the observations of an Agent. + + The observation space has the purpose of: + 1. Reading the outputted state from the PrimAITE Simulation. + 2. Selecting parts of the simulation state that are requested by the simulation config + 3. Formatting this information so an agent can use it to make decisions. + """ + + # TODO: Dear code reader: This class currently doesn't do much except hold an observation object. It will be changed + # to have more of it's own behaviour, and it will replace UC2BlueObservation and UC2RedObservation during the next + # refactor. + + def __init__(self, observation: AbstractObservation) -> None: + """Initialise observation space. + + :param observation: Observation object + :type observation: AbstractObservation + """ + self.obs: AbstractObservation = observation + self.current_observation: ObsType + + def update(self, state: Dict) -> Dict: + """ + Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + """ + self.current_observation = self.obs.observe(state) + return self.current_observation + + @property + def space(self) -> None: + """Gymnasium space object describing the observation space shape.""" + return self.obs.space + + @classmethod + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "ObservationManager": + """Create observation space from a config. + + :param config: Dictionary containing the configuration for this observation space. + It should contain the key 'type' which selects which observation class to use (from a choice of: + UC2BlueObservation, UC2RedObservation, UC2GreenObservation) + The other key is 'options' which are passed to the constructor of the selected observation class. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + """ + if config["type"] == "UC2BlueObservation": + return cls(UC2BlueObservation.from_config(config.get("options", {}), game=game)) + elif config["type"] == "UC2RedObservation": + return cls(UC2RedObservation.from_config(config.get("options", {}), game=game)) + elif config["type"] == "UC2GreenObservation": + return cls(UC2GreenObservation.from_config(config.get("options", {}), game=game)) + else: + raise ValueError("Observation space type invalid") diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations/observations.py similarity index 79% rename from src/primaite/game/agent/observations.py rename to src/primaite/game/agent/observations/observations.py index 82e11fe0..6d6614f4 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -4,7 +4,6 @@ from ipaddress import IPv4Address from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING from gymnasium import spaces -from gymnasium.core import ObsType from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE @@ -822,236 +821,3 @@ class ICSObservation(NullObservation): """ICS observation placeholder, currently not implemented so always returns a single 0.""" pass - - -class UC2BlueObservation(AbstractObservation): - """Container for all observations used by the blue agent in UC2. - - TODO: there's no real need for a UC2 blue container class, we should be able to simply use the observation handler - for the purpose of compiling several observation components. - """ - - def __init__( - self, - nodes: List[NodeObservation], - links: List[LinkObservation], - acl: AclObservation, - ics: ICSObservation, - where: Optional[List[str]] = None, - ) -> None: - """Initialise UC2 blue observation. - - :param nodes: List of node observations - :type nodes: List[NodeObservation] - :param links: List of link observations - :type links: List[LinkObservation] - :param acl: The Access Control List observation - :type acl: AclObservation - :param ics: The ICS observation - :type ics: ICSObservation - :param where: Where in the simulation state dict to find information. Not used in this particular observation - because it only compiles other observations and doesn't contribute any new information, defaults to None - :type where: Optional[List[str]], optional - """ - super().__init__() - self.where: Optional[Tuple[str]] = where - - self.nodes: List[NodeObservation] = nodes - self.links: List[LinkObservation] = links - self.acl: AclObservation = acl - self.ics: ICSObservation = ics - - self.default_observation: Dict = { - "NODES": {i + 1: n.default_observation for i, n in enumerate(self.nodes)}, - "LINKS": {i + 1: l.default_observation for i, l in enumerate(self.links)}, - "ACL": self.acl.default_observation, - "ICS": self.ics.default_observation, - } - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - :return: Observation - :rtype: Dict - """ - if self.where is None: - return self.default_observation - - obs = {} - obs["NODES"] = {i + 1: node.observe(state) for i, node in enumerate(self.nodes)} - obs["LINKS"] = {i + 1: link.observe(state) for i, link in enumerate(self.links)} - obs["ACL"] = self.acl.observe(state) - obs["ICS"] = self.ics.observe(state) - - return obs - - @property - def space(self) -> spaces.Space: - """ - Gymnasium space object describing the observation space shape. - - :return: Space - :rtype: spaces.Space - """ - return spaces.Dict( - { - "NODES": spaces.Dict({i + 1: node.space for i, node in enumerate(self.nodes)}), - "LINKS": spaces.Dict({i + 1: link.space for i, link in enumerate(self.links)}), - "ACL": self.acl.space, - "ICS": self.ics.space, - } - ) - - @classmethod - def from_config(cls, config: Dict, game: "PrimaiteGame") -> "UC2BlueObservation": - """Create UC2 blue observation from a config. - - :param config: Dictionary containing the configuration for this UC2 blue observation. This includes the nodes, - links, ACL and ICS observations. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - :return: Constructed UC2 blue observation - :rtype: UC2BlueObservation - """ - node_configs = config["nodes"] - - num_services_per_node = config["num_services_per_node"] - num_folders_per_node = config["num_folders_per_node"] - num_files_per_folder = config["num_files_per_folder"] - num_nics_per_node = config["num_nics_per_node"] - nodes = [ - NodeObservation.from_config( - config=n, - game=game, - num_services_per_node=num_services_per_node, - num_folders_per_node=num_folders_per_node, - num_files_per_folder=num_files_per_folder, - num_nics_per_node=num_nics_per_node, - ) - for n in node_configs - ] - - link_configs = config["links"] - links = [LinkObservation.from_config(config=link, game=game) for link in link_configs] - - acl_config = config["acl"] - acl = AclObservation.from_config(config=acl_config, game=game) - - ics_config = config["ics"] - ics = ICSObservation.from_config(config=ics_config, game=game) - new = cls(nodes=nodes, links=links, acl=acl, ics=ics, where=["network"]) - return new - - -class UC2RedObservation(AbstractObservation): - """Container for all observations used by the red agent in UC2.""" - - def __init__(self, nodes: List[NodeObservation], where: Optional[List[str]] = None) -> None: - super().__init__() - self.where: Optional[List[str]] = where - self.nodes: List[NodeObservation] = nodes - - self.default_observation: Dict = { - "NODES": {i + 1: n.default_observation for i, n in enumerate(self.nodes)}, - } - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation.""" - if self.where is None: - return self.default_observation - - obs = {} - obs["NODES"] = {i + 1: node.observe(state) for i, node in enumerate(self.nodes)} - return obs - - @property - def space(self) -> spaces.Space: - """Gymnasium space object describing the observation space shape.""" - return spaces.Dict( - { - "NODES": spaces.Dict({i + 1: node.space for i, node in enumerate(self.nodes)}), - } - ) - - @classmethod - def from_config(cls, config: Dict, game: "PrimaiteGame") -> "UC2RedObservation": - """ - Create UC2 red observation from a config. - - :param config: Dictionary containing the configuration for this UC2 red observation. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - """ - node_configs = config["nodes"] - nodes = [NodeObservation.from_config(config=cfg, game=game) for cfg in node_configs] - return cls(nodes=nodes, where=["network"]) - - -class UC2GreenObservation(NullObservation): - """Green agent observation. As the green agent's actions don't depend on the observation, this is empty.""" - - pass - - -class ObservationManager: - """ - Manage the observations of an Agent. - - The observation space has the purpose of: - 1. Reading the outputted state from the PrimAITE Simulation. - 2. Selecting parts of the simulation state that are requested by the simulation config - 3. Formatting this information so an agent can use it to make decisions. - """ - - # TODO: Dear code reader: This class currently doesn't do much except hold an observation object. It will be changed - # to have more of it's own behaviour, and it will replace UC2BlueObservation and UC2RedObservation during the next - # refactor. - - def __init__(self, observation: AbstractObservation) -> None: - """Initialise observation space. - - :param observation: Observation object - :type observation: AbstractObservation - """ - self.obs: AbstractObservation = observation - self.current_observation: ObsType - - def update(self, state: Dict) -> Dict: - """ - Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - """ - self.current_observation = self.obs.observe(state) - return self.current_observation - - @property - def space(self) -> None: - """Gymnasium space object describing the observation space shape.""" - return self.obs.space - - @classmethod - def from_config(cls, config: Dict, game: "PrimaiteGame") -> "ObservationManager": - """Create observation space from a config. - - :param config: Dictionary containing the configuration for this observation space. - It should contain the key 'type' which selects which observation class to use (from a choice of: - UC2BlueObservation, UC2RedObservation, UC2GreenObservation) - The other key is 'options' which are passed to the constructor of the selected observation class. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - """ - if config["type"] == "UC2BlueObservation": - return cls(UC2BlueObservation.from_config(config.get("options", {}), game=game)) - elif config["type"] == "UC2RedObservation": - return cls(UC2RedObservation.from_config(config.get("options", {}), game=game)) - elif config["type"] == "UC2GreenObservation": - return cls(UC2GreenObservation.from_config(config.get("options", {}), game=game)) - else: - raise ValueError("Observation space type invalid") diff --git a/src/primaite/game/agent/scripted_agents/__init__.py b/src/primaite/game/agent/scripted_agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/scripted_agents/data_manipulation_bot.py similarity index 100% rename from src/primaite/game/agent/data_manipulation_bot.py rename to src/primaite/game/agent/scripted_agents/data_manipulation_bot.py diff --git a/src/primaite/game/agent/scripted_agents.py b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py similarity index 97% rename from src/primaite/game/agent/scripted_agents.py rename to src/primaite/game/agent/scripted_agents/probabilistic_agent.py index 5111df32..9cddc978 100644 --- a/src/primaite/game/agent/scripted_agents.py +++ b/src/primaite/game/agent/scripted_agents/probabilistic_agent.py @@ -7,7 +7,7 @@ from gymnasium.core import ObsType from primaite.game.agent.actions import ActionManager from primaite.game.agent.interface import AbstractScriptedAgent -from primaite.game.agent.observations import ObservationManager +from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction diff --git a/src/primaite/game/agent/scripted_agents/random_agent.py b/src/primaite/game/agent/scripted_agents/random_agent.py new file mode 100644 index 00000000..34a4b5ac --- /dev/null +++ b/src/primaite/game/agent/scripted_agents/random_agent.py @@ -0,0 +1,21 @@ +from typing import Dict, Tuple + +from gymnasium.core import ObsType + +from primaite.game.agent.interface import AbstractScriptedAgent + + +class RandomAgent(AbstractScriptedAgent): + """Agent that ignores its observation and acts completely at random.""" + + def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: + """Sample the action space randomly. + + :param obs: Current observation for this agent, not used in RandomAgent + :type obs: ObsType + :param timestep: The current simulation timestep, not used in RandomAgent + :type timestep: int + :return: Action formatted in CAOS format + :rtype: Tuple[str, Dict] + """ + return self.action_manager.get_action(self.action_manager.space.sample()) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 394a8154..33f9186b 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -6,11 +6,11 @@ from pydantic import BaseModel, ConfigDict from primaite import getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent -from primaite.game.agent.observations import ObservationManager +from primaite.game.agent.observations.observation_manager import ObservationManager from primaite.game.agent.rewards import RewardFunction -from primaite.game.agent.scripted_agents import ProbabilisticAgent +from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent +from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent from primaite.simulator.network.hardware.base import NodeOperatingState 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/conftest.py b/tests/conftest.py index a117a1ef..20600e73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,8 @@ from _pytest.monkeypatch import MonkeyPatch 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 import ICSObservation, ObservationManager +from primaite.game.agent.observations.observation_manager import ObservationManager +from primaite.game.agent.observations.observations import ICSObservation from primaite.game.agent.rewards import RewardFunction from primaite.game.game import PrimaiteGame from primaite.session.session import PrimaiteSession 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 3aff59af..f993af5f 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 @@ -5,8 +5,8 @@ from typing import Union import yaml from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.interface import ProxyAgent, RandomAgent +from primaite.game.agent.scripted_agents.data_manipulation_bot import DataManipulationAgent from primaite.game.game import APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer diff --git a/tests/integration_tests/game_layer/test_actions.py b/tests/integration_tests/game_layer/test_actions.py index 8911632c..740fb491 100644 --- a/tests/integration_tests/game_layer/test_actions.py +++ b/tests/integration_tests/game_layer/test_actions.py @@ -10,28 +10,14 @@ # 4. Check that the simulation has changed in the way that I expect. # 5. Repeat for all actions. -from typing import Dict, Tuple +from typing import Tuple import pytest -from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, ProxyAgent -from primaite.game.agent.observations import ICSObservation, ObservationManager -from primaite.game.agent.rewards import RewardFunction +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.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.host.computer import Computer -from primaite.simulator.network.hardware.nodes.host.server import Server -from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router -from primaite.simulator.network.hardware.nodes.network.switch import Switch -from primaite.simulator.network.transmission.network_layer import IPProtocol -from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.sim_container import Simulation from primaite.simulator.system.applications.web_browser import WebBrowser -from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.simulator.system.services.dns.dns_server import DNSServer -from primaite.simulator.system.services.web_server.web_server import WebServer from primaite.simulator.system.software import SoftwareHealthState diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index d1301759..b6aed30b 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -1,6 +1,6 @@ from gymnasium import spaces -from primaite.game.agent.observations import FileObservation +from primaite.game.agent.observations.observations import FileObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.sim_container import Simulation diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index 698bfc72..4bbde32f 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,4 +1,4 @@ -from primaite.game.agent.observations import NicObservation +from primaite.game.agent.observations.observations import NicObservation from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.nmne import set_nmne_config from primaite.simulator.sim_container import Simulation 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 73228e36..c556cfad 100644 --- a/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py +++ b/tests/unit_tests/_primaite/_game/_agent/test_probabilistic_agent.py @@ -1,7 +1,8 @@ from primaite.game.agent.actions import ActionManager -from primaite.game.agent.observations import ICSObservation, ObservationManager +from primaite.game.agent.observations.observation_manager import ObservationManager +from primaite.game.agent.observations.observations import ICSObservation from primaite.game.agent.rewards import RewardFunction -from primaite.game.agent.scripted_agents import ProbabilisticAgent +from primaite.game.agent.scripted_agents.probabilistic_agent import ProbabilisticAgent def test_probabilistic_agent(): From ba58204542ffce55d49a1a8107c543f9ebc99ad0 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 8 Mar 2024 14:08:35 +0000 Subject: [PATCH 088/128] #2350: split observations into smaller files --- .../agent/observations/agent_observations.py | 2 +- .../observations/file_system_observations.py | 177 ++++++++ .../agent/observations/node_observations.py | 199 +++++++++ .../game/agent/observations/observations.py | 412 ------------------ .../observations/software_observation.py | 71 +++ .../game_layer/test_observations.py | 2 +- 6 files changed, 449 insertions(+), 414 deletions(-) create mode 100644 src/primaite/game/agent/observations/file_system_observations.py create mode 100644 src/primaite/game/agent/observations/node_observations.py create mode 100644 src/primaite/game/agent/observations/software_observation.py diff --git a/src/primaite/game/agent/observations/agent_observations.py b/src/primaite/game/agent/observations/agent_observations.py index 522cdb59..70a83881 100644 --- a/src/primaite/game/agent/observations/agent_observations.py +++ b/src/primaite/game/agent/observations/agent_observations.py @@ -2,12 +2,12 @@ from typing import Dict, List, Optional, Tuple, TYPE_CHECKING from gymnasium import spaces +from primaite.game.agent.observations.node_observations import NodeObservation from primaite.game.agent.observations.observations import ( AbstractObservation, AclObservation, ICSObservation, LinkObservation, - NodeObservation, NullObservation, ) diff --git a/src/primaite/game/agent/observations/file_system_observations.py b/src/primaite/game/agent/observations/file_system_observations.py new file mode 100644 index 00000000..277bc51f --- /dev/null +++ b/src/primaite/game/agent/observations/file_system_observations.py @@ -0,0 +1,177 @@ +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING + +from gymnasium import spaces + +from primaite import getLogger +from primaite.game.agent.observations.observations import AbstractObservation +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE + +_LOGGER = getLogger(__name__) + +if TYPE_CHECKING: + from primaite.game.game import PrimaiteGame + + +class FileObservation(AbstractObservation): + """Observation of a file on a node in the network.""" + + def __init__(self, where: Optional[Tuple[str]] = None) -> None: + """ + Initialise file observation. + + :param where: Store information about where in the simulation state dictionary to find the relevant information. + Optional. If None, this corresponds that the file does not exist and the observation will be populated with + zeroes. + + A typical location for a file looks like this: + ['network','nodes',,'file_system', 'folders',,'files',] + :type where: Optional[List[str]] + """ + super().__init__() + self.where: Optional[Tuple[str]] = where + self.default_observation: spaces.Space = {"health_status": 0} + "Default observation is what should be returned when the file doesn't exist, e.g. after it has been deleted." + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + file_state = access_from_nested_dict(state, self.where) + if file_state is NOT_PRESENT_IN_STATE: + return self.default_observation + return {"health_status": file_state["visible_status"]} + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape. + + :return: Gymnasium space + :rtype: spaces.Space + """ + return spaces.Dict({"health_status": spaces.Discrete(6)}) + + @classmethod + def from_config(cls, config: Dict, game: "PrimaiteGame", parent_where: List[str] = None) -> "FileObservation": + """Create file observation from a config. + + :param config: Dictionary containing the configuration for this file observation. + :type config: Dict + :param game: _description_ + :type game: PrimaiteGame + :param parent_where: _description_, defaults to None + :type parent_where: _type_, optional + :return: _description_ + :rtype: _type_ + """ + return cls(where=parent_where + ["files", config["file_name"]]) + + +class FolderObservation(AbstractObservation): + """Folder observation, including files inside of the folder.""" + + def __init__( + self, where: Optional[Tuple[str]] = None, files: List[FileObservation] = [], num_files_per_folder: int = 2 + ) -> None: + """Initialise folder Observation, including files inside the folder. + + :param where: Where in the simulation state dictionary to find the relevant information for this folder. + A typical location for a file looks like this: + ['network','nodes',,'file_system', 'folders',] + :type where: Optional[List[str]] + :param max_files: As size of the space must remain static, define max files that can be in this folder + , defaults to 5 + :type max_files: int, optional + :param file_positions: Defines the positioning within the observation space of particular files. This ensures + that even if new files are created, the existing files will always occupy the same space in the observation + space. The keys must be between 1 and max_files. Providing file_positions will reserve a spot in the + observation space for a file with that name, even if it's temporarily deleted, if it reappears with the same + name, it will take the position defined in this dict. Defaults to {} + :type file_positions: Dict[int, str], optional + """ + super().__init__() + + self.where: Optional[Tuple[str]] = where + + self.files: List[FileObservation] = files + while len(self.files) < num_files_per_folder: + self.files.append(FileObservation()) + while len(self.files) > num_files_per_folder: + truncated_file = self.files.pop() + msg = f"Too many files in folder observation. Truncating file {truncated_file}" + _LOGGER.warning(msg) + + self.default_observation = { + "health_status": 0, + "FILES": {i + 1: f.default_observation for i, f in enumerate(self.files)}, + } + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + folder_state = access_from_nested_dict(state, self.where) + if folder_state is NOT_PRESENT_IN_STATE: + return self.default_observation + + health_status = folder_state["health_status"] + + obs = {} + + obs["health_status"] = health_status + obs["FILES"] = {i + 1: file.observe(state) for i, file in enumerate(self.files)} + + return obs + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape. + + :return: Gymnasium space + :rtype: spaces.Space + """ + return spaces.Dict( + { + "health_status": spaces.Discrete(6), + "FILES": spaces.Dict({i + 1: f.space for i, f in enumerate(self.files)}), + } + ) + + @classmethod + def from_config( + cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]], num_files_per_folder: int = 2 + ) -> "FolderObservation": + """Create folder observation from a config. Also creates child file observations. + + :param config: Dictionary containing the configuration for this folder observation. Includes the name of the + folder and the files inside of it. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + :param parent_where: Where in the simulation state dictionary to find the information about this folder's + parent node. A typical location for a node ``where`` can be: + ['network','nodes',,'file_system'] + :type parent_where: Optional[List[str]] + :param num_files_per_folder: How many spaces for files are in this folder observation (to preserve static + observation size) , defaults to 2 + :type num_files_per_folder: int, optional + :return: Constructed folder observation + :rtype: FolderObservation + """ + where = parent_where + ["folders", config["folder_name"]] + + file_configs = config["files"] + files = [FileObservation.from_config(config=f, game=game, parent_where=where) for f in file_configs] + + return cls(where=where, files=files, num_files_per_folder=num_files_per_folder) diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py new file mode 100644 index 00000000..93c6765b --- /dev/null +++ b/src/primaite/game/agent/observations/node_observations.py @@ -0,0 +1,199 @@ +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING + +from gymnasium import spaces + +from primaite import getLogger +from primaite.game.agent.observations.file_system_observations import FolderObservation +from primaite.game.agent.observations.observations import AbstractObservation, NicObservation +from primaite.game.agent.observations.software_observation import ServiceObservation +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE + +_LOGGER = getLogger(__name__) + +if TYPE_CHECKING: + from primaite.game.game import PrimaiteGame + + +class NodeObservation(AbstractObservation): + """Observation of a node in the network. Includes services, folders and NICs.""" + + def __init__( + self, + where: Optional[Tuple[str]] = None, + services: List[ServiceObservation] = [], + folders: List[FolderObservation] = [], + network_interfaces: List[NicObservation] = [], + logon_status: bool = False, + num_services_per_node: int = 2, + num_folders_per_node: int = 2, + num_files_per_folder: int = 2, + num_nics_per_node: int = 2, + ) -> None: + """ + Configurable observation for a node in the simulation. + + :param where: Where in the simulation state dictionary for find relevant information for this observation. + A typical location for a node looks like this: + ['network','nodes',]. If empty list, a default null observation will be output, defaults to [] + :type where: List[str], optional + :param services: Mapping between position in observation space and service name, defaults to {} + :type services: Dict[int,str], optional + :param max_services: Max number of services that can be presented in observation space for this node + , defaults to 2 + :type max_services: int, optional + :param folders: Mapping between position in observation space and folder name, defaults to {} + :type folders: Dict[int,str], optional + :param max_folders: Max number of folders in this node's obs space, defaults to 2 + :type max_folders: int, optional + :param network_interfaces: Mapping between position in observation space and NIC idx, defaults to {} + :type network_interfaces: Dict[int,str], optional + :param max_nics: Max number of network interfaces in this node's obs space, defaults to 5 + :type max_nics: int, optional + """ + super().__init__() + self.where: Optional[Tuple[str]] = where + + self.services: List[ServiceObservation] = services + while len(self.services) < num_services_per_node: + # add empty service observation without `where` parameter so it always returns default (blank) observation + self.services.append(ServiceObservation()) + while len(self.services) > num_services_per_node: + truncated_service = self.services.pop() + msg = f"Too many services in Node observation space for node. Truncating service {truncated_service.where}" + _LOGGER.warning(msg) + # truncate service list + + self.folders: List[FolderObservation] = folders + # add empty folder observation without `where` parameter that will always return default (blank) observations + while len(self.folders) < num_folders_per_node: + self.folders.append(FolderObservation(num_files_per_folder=num_files_per_folder)) + while len(self.folders) > num_folders_per_node: + truncated_folder = self.folders.pop() + msg = f"Too many folders in Node observation for node. Truncating service {truncated_folder.where[-1]}" + _LOGGER.warning(msg) + + self.network_interfaces: List[NicObservation] = network_interfaces + while len(self.network_interfaces) < num_nics_per_node: + self.network_interfaces.append(NicObservation()) + while len(self.network_interfaces) > num_nics_per_node: + truncated_nic = self.network_interfaces.pop() + msg = f"Too many NICs in Node observation for node. Truncating service {truncated_nic.where[-1]}" + _LOGGER.warning(msg) + + self.logon_status: bool = logon_status + + self.default_observation: Dict = { + "SERVICES": {i + 1: s.default_observation for i, s in enumerate(self.services)}, + "FOLDERS": {i + 1: f.default_observation for i, f in enumerate(self.folders)}, + "NETWORK_INTERFACES": {i + 1: n.default_observation for i, n in enumerate(self.network_interfaces)}, + "operating_status": 0, + } + if self.logon_status: + self.default_observation["logon_status"] = 0 + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + + node_state = access_from_nested_dict(state, self.where) + if node_state is NOT_PRESENT_IN_STATE: + return self.default_observation + + obs = {} + obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)} + obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)} + obs["operating_status"] = node_state["operating_state"] + obs["NETWORK_INTERFACES"] = { + i + 1: network_interface.observe(state) for i, network_interface in enumerate(self.network_interfaces) + } + + if self.logon_status: + obs["logon_status"] = 0 + + return obs + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape.""" + space_shape = { + "SERVICES": spaces.Dict({i + 1: service.space for i, service in enumerate(self.services)}), + "FOLDERS": spaces.Dict({i + 1: folder.space for i, folder in enumerate(self.folders)}), + "operating_status": spaces.Discrete(5), + "NETWORK_INTERFACES": spaces.Dict( + {i + 1: network_interface.space for i, network_interface in enumerate(self.network_interfaces)} + ), + } + if self.logon_status: + space_shape["logon_status"] = spaces.Discrete(3) + + return spaces.Dict(space_shape) + + @classmethod + def from_config( + cls, + config: Dict, + game: "PrimaiteGame", + parent_where: Optional[List[str]] = None, + num_services_per_node: int = 2, + num_folders_per_node: int = 2, + num_files_per_folder: int = 2, + num_nics_per_node: int = 2, + ) -> "NodeObservation": + """Create node observation from a config. Also creates child service, folder and NIC observations. + + :param config: Dictionary containing the configuration for this node observation. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + :param parent_where: Where in the simulation state dictionary to find the information about this node's parent + network. A typical location for it would be: ['network',] + :type parent_where: Optional[List[str]] + :param num_services_per_node: How many spaces for services are in this node observation (to preserve static + observation size) , defaults to 2 + :type num_services_per_node: int, optional + :param num_folders_per_node: How many spaces for folders are in this node observation (to preserve static + observation size) , defaults to 2 + :type num_folders_per_node: int, optional + :param num_files_per_folder: How many spaces for files are in the folder observations (to preserve static + observation size) , defaults to 2 + :type num_files_per_folder: int, optional + :return: Constructed node observation + :rtype: NodeObservation + """ + node_hostname = config["node_hostname"] + if parent_where is None: + where = ["network", "nodes", node_hostname] + else: + where = parent_where + ["nodes", node_hostname] + + svc_configs = config.get("services", {}) + services = [ServiceObservation.from_config(config=c, game=game, parent_where=where) for c in svc_configs] + folder_configs = config.get("folders", {}) + folders = [ + FolderObservation.from_config( + config=c, game=game, parent_where=where + ["file_system"], num_files_per_folder=num_files_per_folder + ) + for c in folder_configs + ] + # create some configs for the NIC observation in the format {"nic_num":1}, {"nic_num":2}, {"nic_num":3}, etc. + nic_configs = [{"nic_num": i for i in range(num_nics_per_node)}] + network_interfaces = [NicObservation.from_config(config=c, game=game, parent_where=where) for c in nic_configs] + logon_status = config.get("logon_status", False) + return cls( + where=where, + services=services, + folders=folders, + network_interfaces=network_interfaces, + logon_status=logon_status, + num_services_per_node=num_services_per_node, + num_folders_per_node=num_folders_per_node, + num_files_per_folder=num_files_per_folder, + num_nics_per_node=num_nics_per_node, + ) diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index 6d6614f4..10e69ea5 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -46,128 +46,6 @@ class AbstractObservation(ABC): pass -class FileObservation(AbstractObservation): - """Observation of a file on a node in the network.""" - - def __init__(self, where: Optional[Tuple[str]] = None) -> None: - """ - Initialise file observation. - - :param where: Store information about where in the simulation state dictionary to find the relevant information. - Optional. If None, this corresponds that the file does not exist and the observation will be populated with - zeroes. - - A typical location for a file looks like this: - ['network','nodes',,'file_system', 'folders',,'files',] - :type where: Optional[List[str]] - """ - super().__init__() - self.where: Optional[Tuple[str]] = where - self.default_observation: spaces.Space = {"health_status": 0} - "Default observation is what should be returned when the file doesn't exist, e.g. after it has been deleted." - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - :return: Observation - :rtype: Dict - """ - if self.where is None: - return self.default_observation - file_state = access_from_nested_dict(state, self.where) - if file_state is NOT_PRESENT_IN_STATE: - return self.default_observation - return {"health_status": file_state["visible_status"]} - - @property - def space(self) -> spaces.Space: - """Gymnasium space object describing the observation space shape. - - :return: Gymnasium space - :rtype: spaces.Space - """ - return spaces.Dict({"health_status": spaces.Discrete(6)}) - - @classmethod - def from_config(cls, config: Dict, game: "PrimaiteGame", parent_where: List[str] = None) -> "FileObservation": - """Create file observation from a config. - - :param config: Dictionary containing the configuration for this file observation. - :type config: Dict - :param game: _description_ - :type game: PrimaiteGame - :param parent_where: _description_, defaults to None - :type parent_where: _type_, optional - :return: _description_ - :rtype: _type_ - """ - return cls(where=parent_where + ["files", config["file_name"]]) - - -class ServiceObservation(AbstractObservation): - """Observation of a service in the network.""" - - default_observation: spaces.Space = {"operating_status": 0, "health_status": 0} - "Default observation is what should be returned when the service doesn't exist." - - def __init__(self, where: Optional[Tuple[str]] = None) -> None: - """Initialise service observation. - - :param where: Store information about where in the simulation state dictionary to find the relevant information. - Optional. If None, this corresponds that the file does not exist and the observation will be populated with - zeroes. - - A typical location for a service looks like this: - `['network','nodes',,'services', ]` - :type where: Optional[List[str]] - """ - super().__init__() - self.where: Optional[Tuple[str]] = where - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - :return: Observation - :rtype: Dict - """ - if self.where is None: - return self.default_observation - - service_state = access_from_nested_dict(state, self.where) - if service_state is NOT_PRESENT_IN_STATE: - return self.default_observation - return { - "operating_status": service_state["operating_state"], - "health_status": service_state["health_state_visible"], - } - - @property - def space(self) -> spaces.Space: - """Gymnasium space object describing the observation space shape.""" - return spaces.Dict({"operating_status": spaces.Discrete(7), "health_status": spaces.Discrete(6)}) - - @classmethod - def from_config( - cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]] = None - ) -> "ServiceObservation": - """Create service observation from a config. - - :param config: Dictionary containing the configuration for this service observation. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - :param parent_where: Where in the simulation state dictionary this service's parent node is located. Optional. - :type parent_where: Optional[List[str]], optional - :return: Constructed service observation - :rtype: ServiceObservation - """ - return cls(where=parent_where + ["services", config["service_name"]]) - - class LinkObservation(AbstractObservation): """Observation of a link in the network.""" @@ -238,111 +116,6 @@ class LinkObservation(AbstractObservation): return cls(where=["network", "links", game.ref_map_links[config["link_ref"]]]) -class FolderObservation(AbstractObservation): - """Folder observation, including files inside of the folder.""" - - def __init__( - self, where: Optional[Tuple[str]] = None, files: List[FileObservation] = [], num_files_per_folder: int = 2 - ) -> None: - """Initialise folder Observation, including files inside of the folder. - - :param where: Where in the simulation state dictionary to find the relevant information for this folder. - A typical location for a file looks like this: - ['network','nodes',,'file_system', 'folders',] - :type where: Optional[List[str]] - :param max_files: As size of the space must remain static, define max files that can be in this folder - , defaults to 5 - :type max_files: int, optional - :param file_positions: Defines the positioning within the observation space of particular files. This ensures - that even if new files are created, the existing files will always occupy the same space in the observation - space. The keys must be between 1 and max_files. Providing file_positions will reserve a spot in the - observation space for a file with that name, even if it's temporarily deleted, if it reappears with the same - name, it will take the position defined in this dict. Defaults to {} - :type file_positions: Dict[int, str], optional - """ - super().__init__() - - self.where: Optional[Tuple[str]] = where - - self.files: List[FileObservation] = files - while len(self.files) < num_files_per_folder: - self.files.append(FileObservation()) - while len(self.files) > num_files_per_folder: - truncated_file = self.files.pop() - msg = f"Too many files in folder observation. Truncating file {truncated_file}" - _LOGGER.warning(msg) - - self.default_observation = { - "health_status": 0, - "FILES": {i + 1: f.default_observation for i, f in enumerate(self.files)}, - } - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - :return: Observation - :rtype: Dict - """ - if self.where is None: - return self.default_observation - folder_state = access_from_nested_dict(state, self.where) - if folder_state is NOT_PRESENT_IN_STATE: - return self.default_observation - - health_status = folder_state["health_status"] - - obs = {} - - obs["health_status"] = health_status - obs["FILES"] = {i + 1: file.observe(state) for i, file in enumerate(self.files)} - - return obs - - @property - def space(self) -> spaces.Space: - """Gymnasium space object describing the observation space shape. - - :return: Gymnasium space - :rtype: spaces.Space - """ - return spaces.Dict( - { - "health_status": spaces.Discrete(6), - "FILES": spaces.Dict({i + 1: f.space for i, f in enumerate(self.files)}), - } - ) - - @classmethod - def from_config( - cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]], num_files_per_folder: int = 2 - ) -> "FolderObservation": - """Create folder observation from a config. Also creates child file observations. - - :param config: Dictionary containing the configuration for this folder observation. Includes the name of the - folder and the files inside of it. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - :param parent_where: Where in the simulation state dictionary to find the information about this folder's - parent node. A typical location for a node ``where`` can be: - ['network','nodes',,'file_system'] - :type parent_where: Optional[List[str]] - :param num_files_per_folder: How many spaces for files are in this folder observation (to preserve static - observation size) , defaults to 2 - :type num_files_per_folder: int, optional - :return: Constructed folder observation - :rtype: FolderObservation - """ - where = parent_where + ["folders", config["folder_name"]] - - file_configs = config["files"] - files = [FileObservation.from_config(config=f, game=game, parent_where=where) for f in file_configs] - - return cls(where=where, files=files, num_files_per_folder=num_files_per_folder) - - class NicObservation(AbstractObservation): """Observation of a Network Interface Card (NIC) in the network.""" @@ -444,191 +217,6 @@ class NicObservation(AbstractObservation): return cls(where=parent_where + ["NICs", config["nic_num"]]) -class NodeObservation(AbstractObservation): - """Observation of a node in the network. Includes services, folders and NICs.""" - - def __init__( - self, - where: Optional[Tuple[str]] = None, - services: List[ServiceObservation] = [], - folders: List[FolderObservation] = [], - network_interfaces: List[NicObservation] = [], - logon_status: bool = False, - num_services_per_node: int = 2, - num_folders_per_node: int = 2, - num_files_per_folder: int = 2, - num_nics_per_node: int = 2, - ) -> None: - """ - Configurable observation for a node in the simulation. - - :param where: Where in the simulation state dictionary for find relevant information for this observation. - A typical location for a node looks like this: - ['network','nodes',]. If empty list, a default null observation will be output, defaults to [] - :type where: List[str], optional - :param services: Mapping between position in observation space and service name, defaults to {} - :type services: Dict[int,str], optional - :param max_services: Max number of services that can be presented in observation space for this node - , defaults to 2 - :type max_services: int, optional - :param folders: Mapping between position in observation space and folder name, defaults to {} - :type folders: Dict[int,str], optional - :param max_folders: Max number of folders in this node's obs space, defaults to 2 - :type max_folders: int, optional - :param network_interfaces: Mapping between position in observation space and NIC idx, defaults to {} - :type network_interfaces: Dict[int,str], optional - :param max_nics: Max number of network interfaces in this node's obs space, defaults to 5 - :type max_nics: int, optional - """ - super().__init__() - self.where: Optional[Tuple[str]] = where - - self.services: List[ServiceObservation] = services - while len(self.services) < num_services_per_node: - # add empty service observation without `where` parameter so it always returns default (blank) observation - self.services.append(ServiceObservation()) - while len(self.services) > num_services_per_node: - truncated_service = self.services.pop() - msg = f"Too many services in Node observation space for node. Truncating service {truncated_service.where}" - _LOGGER.warning(msg) - # truncate service list - - self.folders: List[FolderObservation] = folders - # add empty folder observation without `where` parameter that will always return default (blank) observations - while len(self.folders) < num_folders_per_node: - self.folders.append(FolderObservation(num_files_per_folder=num_files_per_folder)) - while len(self.folders) > num_folders_per_node: - truncated_folder = self.folders.pop() - msg = f"Too many folders in Node observation for node. Truncating service {truncated_folder.where[-1]}" - _LOGGER.warning(msg) - - self.network_interfaces: List[NicObservation] = network_interfaces - while len(self.network_interfaces) < num_nics_per_node: - self.network_interfaces.append(NicObservation()) - while len(self.network_interfaces) > num_nics_per_node: - truncated_nic = self.network_interfaces.pop() - msg = f"Too many NICs in Node observation for node. Truncating service {truncated_nic.where[-1]}" - _LOGGER.warning(msg) - - self.logon_status: bool = logon_status - - self.default_observation: Dict = { - "SERVICES": {i + 1: s.default_observation for i, s in enumerate(self.services)}, - "FOLDERS": {i + 1: f.default_observation for i, f in enumerate(self.folders)}, - "NETWORK_INTERFACES": {i + 1: n.default_observation for i, n in enumerate(self.network_interfaces)}, - "operating_status": 0, - } - if self.logon_status: - self.default_observation["logon_status"] = 0 - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - :return: Observation - :rtype: Dict - """ - if self.where is None: - return self.default_observation - - node_state = access_from_nested_dict(state, self.where) - if node_state is NOT_PRESENT_IN_STATE: - return self.default_observation - - obs = {} - obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)} - obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)} - obs["operating_status"] = node_state["operating_state"] - obs["NETWORK_INTERFACES"] = { - i + 1: network_interface.observe(state) for i, network_interface in enumerate(self.network_interfaces) - } - - if self.logon_status: - obs["logon_status"] = 0 - - return obs - - @property - def space(self) -> spaces.Space: - """Gymnasium space object describing the observation space shape.""" - space_shape = { - "SERVICES": spaces.Dict({i + 1: service.space for i, service in enumerate(self.services)}), - "FOLDERS": spaces.Dict({i + 1: folder.space for i, folder in enumerate(self.folders)}), - "operating_status": spaces.Discrete(5), - "NETWORK_INTERFACES": spaces.Dict( - {i + 1: network_interface.space for i, network_interface in enumerate(self.network_interfaces)} - ), - } - if self.logon_status: - space_shape["logon_status"] = spaces.Discrete(3) - - return spaces.Dict(space_shape) - - @classmethod - def from_config( - cls, - config: Dict, - game: "PrimaiteGame", - parent_where: Optional[List[str]] = None, - num_services_per_node: int = 2, - num_folders_per_node: int = 2, - num_files_per_folder: int = 2, - num_nics_per_node: int = 2, - ) -> "NodeObservation": - """Create node observation from a config. Also creates child service, folder and NIC observations. - - :param config: Dictionary containing the configuration for this node observation. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - :param parent_where: Where in the simulation state dictionary to find the information about this node's parent - network. A typical location for it would be: ['network',] - :type parent_where: Optional[List[str]] - :param num_services_per_node: How many spaces for services are in this node observation (to preserve static - observation size) , defaults to 2 - :type num_services_per_node: int, optional - :param num_folders_per_node: How many spaces for folders are in this node observation (to preserve static - observation size) , defaults to 2 - :type num_folders_per_node: int, optional - :param num_files_per_folder: How many spaces for files are in the folder observations (to preserve static - observation size) , defaults to 2 - :type num_files_per_folder: int, optional - :return: Constructed node observation - :rtype: NodeObservation - """ - node_hostname = config["node_hostname"] - if parent_where is None: - where = ["network", "nodes", node_hostname] - else: - where = parent_where + ["nodes", node_hostname] - - svc_configs = config.get("services", {}) - services = [ServiceObservation.from_config(config=c, game=game, parent_where=where) for c in svc_configs] - folder_configs = config.get("folders", {}) - folders = [ - FolderObservation.from_config( - config=c, game=game, parent_where=where + ["file_system"], num_files_per_folder=num_files_per_folder - ) - for c in folder_configs - ] - # create some configs for the NIC observation in the format {"nic_num":1}, {"nic_num":2}, {"nic_num":3}, etc. - nic_configs = [{"nic_num": i for i in range(num_nics_per_node)}] - network_interfaces = [NicObservation.from_config(config=c, game=game, parent_where=where) for c in nic_configs] - logon_status = config.get("logon_status", False) - return cls( - where=where, - services=services, - folders=folders, - network_interfaces=network_interfaces, - logon_status=logon_status, - num_services_per_node=num_services_per_node, - num_folders_per_node=num_folders_per_node, - num_files_per_folder=num_files_per_folder, - num_nics_per_node=num_nics_per_node, - ) - - class AclObservation(AbstractObservation): """Observation of an Access Control List (ACL) in the network.""" diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py new file mode 100644 index 00000000..eae9dc1f --- /dev/null +++ b/src/primaite/game/agent/observations/software_observation.py @@ -0,0 +1,71 @@ +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING + +from gymnasium import spaces + +from primaite.game.agent.observations.observations import AbstractObservation +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE + +if TYPE_CHECKING: + from primaite.game.game import PrimaiteGame + + +class ServiceObservation(AbstractObservation): + """Observation of a service in the network.""" + + default_observation: spaces.Space = {"operating_status": 0, "health_status": 0} + "Default observation is what should be returned when the service doesn't exist." + + def __init__(self, where: Optional[Tuple[str]] = None) -> None: + """Initialise service observation. + + :param where: Store information about where in the simulation state dictionary to find the relevant information. + Optional. If None, this corresponds that the file does not exist and the observation will be populated with + zeroes. + + A typical location for a service looks like this: + `['network','nodes',,'services', ]` + :type where: Optional[List[str]] + """ + super().__init__() + self.where: Optional[Tuple[str]] = where + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + + service_state = access_from_nested_dict(state, self.where) + if service_state is NOT_PRESENT_IN_STATE: + return self.default_observation + return { + "operating_status": service_state["operating_state"], + "health_status": service_state["health_state_visible"], + } + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape.""" + return spaces.Dict({"operating_status": spaces.Discrete(7), "health_status": spaces.Discrete(6)}) + + @classmethod + def from_config( + cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]] = None + ) -> "ServiceObservation": + """Create service observation from a config. + + :param config: Dictionary containing the configuration for this service observation. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + :param parent_where: Where in the simulation state dictionary this service's parent node is located. Optional. + :type parent_where: Optional[List[str]], optional + :return: Constructed service observation + :rtype: ServiceObservation + """ + return cls(where=parent_where + ["services", config["service_name"]]) diff --git a/tests/integration_tests/game_layer/test_observations.py b/tests/integration_tests/game_layer/test_observations.py index b6aed30b..f52b52f7 100644 --- a/tests/integration_tests/game_layer/test_observations.py +++ b/tests/integration_tests/game_layer/test_observations.py @@ -1,6 +1,6 @@ from gymnasium import spaces -from primaite.game.agent.observations.observations import FileObservation +from primaite.game.agent.observations.file_system_observations import FileObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.sim_container import Simulation From 61aa24212847763737e2ef2759b05343ee0b5ef4 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 8 Mar 2024 14:48:31 +0000 Subject: [PATCH 089/128] #2350: tests + application --- .../observations/software_observation.py | 92 +++++++++++++++++++ src/primaite/simulator/file_system/folder.py | 2 +- .../game_layer/observations/__init__.py | 0 .../test_file_system_observations.py | 68 ++++++++++++++ .../observations/test_node_observations.py | 43 +++++++++ .../observations/test_observations.py | 35 +++++++ .../test_software_observations.py | 66 +++++++++++++ 7 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 tests/integration_tests/game_layer/observations/__init__.py create mode 100644 tests/integration_tests/game_layer/observations/test_file_system_observations.py create mode 100644 tests/integration_tests/game_layer/observations/test_node_observations.py create mode 100644 tests/integration_tests/game_layer/observations/test_observations.py create mode 100644 tests/integration_tests/game_layer/observations/test_software_observations.py diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index eae9dc1f..ff61714a 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -69,3 +69,95 @@ class ServiceObservation(AbstractObservation): :rtype: ServiceObservation """ return cls(where=parent_where + ["services", config["service_name"]]) + + +class ApplicationObservation(AbstractObservation): + """Observation of an application in the network.""" + + default_observation: spaces.Space = {"operating_status": 0, "health_status": 0, "num_executions": 0} + "Default observation is what should be returned when the application doesn't exist." + + def __init__(self, where: Optional[Tuple[str]] = None) -> None: + """Initialise application observation. + + :param where: Store information about where in the simulation state dictionary to find the relevant information. + Optional. If None, this corresponds that the file does not exist and the observation will be populated with + zeroes. + + A typical location for a service looks like this: + `['network','nodes',,'applications', ]` + :type where: Optional[List[str]] + """ + super().__init__() + self.where: Optional[Tuple[str]] = where + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + + app_state = access_from_nested_dict(state, self.where) + if app_state is NOT_PRESENT_IN_STATE: + return self.default_observation + return { + "operating_status": app_state["operating_state"], + "health_status": app_state["health_state_visible"], + "num_executions": self._categorise_num_executions(app_state["num_executions"]), + } + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape.""" + return spaces.Dict( + { + "operating_status": spaces.Discrete(7), + "health_status": spaces.Discrete(6), + "num_executions": spaces.Discrete(4), + } + ) + + @classmethod + def from_config( + cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]] = None + ) -> "ApplicationObservation": + """Create application observation from a config. + + :param config: Dictionary containing the configuration for this service observation. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + :param parent_where: Where in the simulation state dictionary this service's parent node is located. Optional. + :type parent_where: Optional[List[str]], optional + :return: Constructed service observation + :rtype: ApplicationObservation + """ + return cls(where=parent_where + ["services", config["application_name"]]) + + @classmethod + def _categorise_num_executions(cls, num_executions: int) -> int: + """ + Categorise the number of executions of an application. + + Helps classify the number of application executions into different categories. + + Current categories: + - 0: Application is never executed + - 1: Application is executed a low number of times (1-5) + - 2: Application is executed often (6-10) + - 3: Application is executed a high number of times (more than 10) + + :param: num_executions: Number of times the application is executed + """ + if num_executions > 10: + return 3 + elif num_executions > 5: + return 2 + elif num_executions > 0: + return 1 + return 0 diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 3ddc1e5f..529bfe11 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -130,7 +130,7 @@ class Folder(FileSystemItemABC): file = self.get_file_by_id(file_uuid=file_id) file.scan() if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: - self.visible_health_status = FileSystemItemHealthStatus.CORRUPT + self.health_status = FileSystemItemHealthStatus.CORRUPT self.visible_health_status = self.health_status def _reveal_to_red_timestep(self) -> None: diff --git a/tests/integration_tests/game_layer/observations/__init__.py b/tests/integration_tests/game_layer/observations/__init__.py new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..808007cc --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_file_system_observations.py @@ -0,0 +1,68 @@ +import pytest +from gymnasium import spaces + +from primaite.game.agent.observations.file_system_observations import FileObservation, FolderObservation +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.sim_container import Simulation + + +@pytest.fixture(scope="function") +def simulation(example_network) -> Simulation: + sim = Simulation() + + # set simulation network as example network + sim.network = example_network + + return sim + + +def test_file_observation(simulation): + """Test the file observation.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + # create a file on the pc + file = pc.file_system.create_file(file_name="dog.png") + + dog_file_obs = FileObservation( + where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"] + ) + + assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)}) + + observation_state = dog_file_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 1 # good initial + + file.corrupt() + observation_state = dog_file_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 1 # scan file so this changes + + file.scan() + file.apply_timestep(0) # apply time step + observation_state = dog_file_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 3 # corrupted + + +def test_folder_observation(simulation): + """Test the folder observation.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + # create a file and folder on the pc + folder = pc.file_system.create_folder("test_folder") + file = pc.file_system.create_file(file_name="dog.png", folder_name="test_folder") + + root_folder_obs = FolderObservation( + where=["network", "nodes", pc.hostname, "file_system", "folders", "test_folder"] + ) + + observation_state = root_folder_obs.observe(simulation.describe_state()) + assert observation_state.get("FILES") is not None + assert observation_state.get("health_status") == 1 + + 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 + + folder.scan() + for i in range(folder.scan_duration + 1): + folder.apply_timestep(i) # apply as many timesteps as needed for a scan + + observation_state = root_folder_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 3 # file is corrupt therefore folder is corrupted too diff --git a/tests/integration_tests/game_layer/observations/test_node_observations.py b/tests/integration_tests/game_layer/observations/test_node_observations.py new file mode 100644 index 00000000..835202c6 --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -0,0 +1,43 @@ +import copy +from uuid import uuid4 + +import pytest + +from primaite.game.agent.observations.node_observations import NodeObservation +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.sim_container import Simulation + + +@pytest.fixture(scope="function") +def simulation(example_network) -> Simulation: + sim = Simulation() + + # set simulation network as example network + sim.network = example_network + + return sim + + +def test_node_observation(simulation): + """Test a Node observation.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + + node_obs = NodeObservation(where=["network", "nodes", pc.hostname]) + + observation_state = node_obs.observe(simulation.describe_state()) + assert observation_state.get("operating_status") == 1 # computer is on + + assert observation_state.get("SERVICES") is not None + assert observation_state.get("FOLDERS") is not None + assert observation_state.get("NETWORK_INTERFACES") is not None + + # turn off computer + pc.power_off() + observation_state = node_obs.observe(simulation.describe_state()) + assert observation_state.get("operating_status") == 4 # shutting down + + for i in range(pc.shut_down_duration + 1): + pc.apply_timestep(i) + + observation_state = node_obs.observe(simulation.describe_state()) + assert observation_state.get("operating_status") == 2 diff --git a/tests/integration_tests/game_layer/observations/test_observations.py b/tests/integration_tests/game_layer/observations/test_observations.py new file mode 100644 index 00000000..eccda238 --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_observations.py @@ -0,0 +1,35 @@ +import pytest + +from primaite.game.agent.observations.observations import NicObservation +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.host_node import NIC +from primaite.simulator.sim_container import Simulation + + +@pytest.fixture(scope="function") +def simulation(example_network) -> Simulation: + sim = Simulation() + + # set simulation network as example network + sim.network = example_network + + return sim + + +def test_nic(simulation): + """Test the NIC observation.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + + nic: NIC = pc.network_interface[1] + + nic_obs = NicObservation(where=["network", "nodes", pc.hostname, "NICs", 1]) + + observation_state = nic_obs.observe(simulation.describe_state()) + assert observation_state.get("nic_status") == 1 # enabled + assert observation_state.get("nmne") is not None + assert observation_state["nmne"].get("inbound") == 0 + assert observation_state["nmne"].get("outbound") == 0 + + nic.disable() + observation_state = nic_obs.observe(simulation.describe_state()) + assert observation_state.get("nic_status") == 2 # disabled diff --git a/tests/integration_tests/game_layer/observations/test_software_observations.py b/tests/integration_tests/game_layer/observations/test_software_observations.py new file mode 100644 index 00000000..17fc386f --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_software_observations.py @@ -0,0 +1,66 @@ +import pytest + +from primaite.game.agent.observations.software_observation import ApplicationObservation, ServiceObservation +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.sim_container import Simulation +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.simulator.system.services.ntp.ntp_server import NTPServer + + +@pytest.fixture(scope="function") +def simulation(example_network) -> Simulation: + sim = Simulation() + + # set simulation network as example network + sim.network = example_network + + return sim + + +def test_service_observation(simulation): + """Test the service observation.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + # install software on the computer + pc.software_manager.install(NTPServer) + + ntp_server = pc.software_manager.software.get("NTPServer") + assert ntp_server + + service_obs = ServiceObservation(where=["network", "nodes", pc.hostname, "services", "NTPServer"]) + + observation_state = service_obs.observe(simulation.describe_state()) + + assert observation_state.get("health_status") == 0 + assert observation_state.get("operating_status") == 1 # running + + ntp_server.restart() + observation_state = service_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 0 + assert observation_state.get("operating_status") == 6 # resetting + + +def test_application_observation(simulation): + """Test the application observation.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + # install software on the computer + pc.software_manager.install(DatabaseClient) + + web_browser: WebBrowser = pc.software_manager.software.get("WebBrowser") + assert web_browser + + app_obs = ApplicationObservation(where=["network", "nodes", pc.hostname, "applications", "WebBrowser"]) + + web_browser.close() + observation_state = app_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 0 + assert observation_state.get("operating_status") == 2 # stopped + assert observation_state.get("num_executions") == 0 + + web_browser.run() + web_browser.scan() # scan to update health status + web_browser.get_webpage("test") + observation_state = app_obs.observe(simulation.describe_state()) + assert observation_state.get("health_status") == 1 + assert observation_state.get("operating_status") == 1 # running + assert observation_state.get("num_executions") == 1 From beb51834f9b3c00ad37102efbe525d4fd86d6536 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 8 Mar 2024 14:58:34 +0000 Subject: [PATCH 090/128] Make all requests return a RequestResponse --- src/primaite/interface/request.py | 37 +++++--- src/primaite/simulator/core.py | 4 +- .../simulator/file_system/file_system.py | 85 +++++++++++-------- .../file_system/file_system_item_abc.py | 45 ++++++---- src/primaite/simulator/file_system/folder.py | 25 +++++- .../simulator/network/hardware/base.py | 74 ++++++++++------ .../network/hardware/nodes/network/router.py | 32 ++++--- .../system/applications/database_client.py | 3 +- .../red_applications/data_manipulation_bot.py | 18 ++-- .../applications/red_applications/dos_bot.py | 17 ++-- .../system/applications/web_browser.py | 6 +- .../simulator/system/services/service.py | 46 ++++++---- src/primaite/simulator/system/software.py | 19 +++-- 13 files changed, 275 insertions(+), 136 deletions(-) diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py index 10ce6254..8e61c1cb 100644 --- a/src/primaite/interface/request.py +++ b/src/primaite/interface/request.py @@ -1,6 +1,9 @@ -from typing import Dict, Literal +from typing import Dict, ForwardRef, Literal -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, validate_call + +RequestResponse = ForwardRef("RequestResponse") +"""This makes it possible to type-hint RequestResponse.from_bool return type.""" class RequestResponse(BaseModel): @@ -9,21 +12,33 @@ class RequestResponse(BaseModel): model_config = ConfigDict(extra="forbid") """Cannot have extra fields in the response. Anything custom goes into the data field.""" - status: Literal["pending", "success", "failure"] = "pending" + status: Literal["pending", "success", "failure", "unreachable"] = "pending" """ What is the current status of the request: - pending - the request has not been received yet, or it has been received but it's still being processed. - - success - the request has successfully been received and processed. - - failure - the request could not reach it's intended target or it was rejected. - - Note that the failure status should only be used when the request cannot be processed, for instance when the - target SimComponent doesn't exist, or is in an OFF state that prevents it from accepting requests. If the - request is received by the target and the associated action is executed, but couldn't be completed due to - downstream factors, the request was still successfully received, it's just that the result wasn't what was - intended. + - success - the request has been received and executed successfully. + - failure - the request has been received and attempted, but execution failed. + - unreachable - the request could not reach it's intended target, either because it doesn't exist or the target + is off. """ data: Dict = {} """Catch-all place to provide any additional data that was generated as a response to the request.""" # TODO: currently, status and data have default values, because I don't want to interrupt existing functionality too # much. However, in the future we might consider making them mandatory. + + @classmethod + @validate_call + def from_bool(cls, status_bool: bool) -> RequestResponse: + """ + Construct a basic request response from a boolean. + + True maps to a success status. False maps to a failure status. + + :param status_bool: Whether to create a successful response + :type status_bool: bool + """ + if status_bool is True: + return cls(status="success", data={}) + elif status_bool is False: + return cls(status="failure", data={}) diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 9ea59305..64f33f6a 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -96,7 +96,7 @@ class RequestManager(BaseModel): # _LOGGER.error(msg) # raise RuntimeError(msg) _LOGGER.debug(msg) - return RequestResponse(status="failure", data={"reason": msg}) + return RequestResponse(status="unreachable", data={"reason": msg}) request_type = self.request_types[request_key] @@ -226,7 +226,7 @@ class SimComponent(BaseModel): """ if self._request_manager is None: return - self._request_manager(request, context) + return self._request_manager(request, context) def apply_timestep(self, timestep: int) -> None: """ diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 8fd4e5d7..3ff73a80 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -7,6 +7,7 @@ from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable from primaite import getLogger +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.file_system.file import File from primaite.simulator.file_system.file_type import FileType @@ -41,12 +42,16 @@ class FileSystem(SimComponent): self._delete_manager.add_request( name="file", request_type=RequestType( - func=lambda request, context: self.delete_file(folder_name=request[0], file_name=request[1]) + func=lambda request, context: RequestResponse.from_bool( + self.delete_file(folder_name=request[0], file_name=request[1]) + ) ), ) self._delete_manager.add_request( name="folder", - request_type=RequestType(func=lambda request, context: self.delete_folder(folder_name=request[0])), + request_type=RequestType( + func=lambda request, context: RequestResponse.from_bool(self.delete_folder(folder_name=request[0])) + ), ) rm.add_request( name="delete", @@ -57,12 +62,16 @@ class FileSystem(SimComponent): self._restore_manager.add_request( name="file", request_type=RequestType( - func=lambda request, context: self.restore_file(folder_name=request[0], file_name=request[1]) + func=lambda request, context: RequestResponse( + self.restore_file(folder_name=request[0], file_name=request[1]) + ) ), ) self._restore_manager.add_request( name="folder", - request_type=RequestType(func=lambda request, context: self.restore_folder(folder_name=request[0])), + request_type=RequestType( + func=lambda request, context: RequestResponse.from_bool(self.restore_folder(folder_name=request[0])) + ), ) rm.add_request( name="restore", @@ -138,7 +147,7 @@ class FileSystem(SimComponent): ) return folder - def delete_folder(self, folder_name: str): + def delete_folder(self, folder_name: str) -> bool: """ Deletes a folder, removes it from the folders list and removes any child folders and files. @@ -146,24 +155,26 @@ class FileSystem(SimComponent): """ if folder_name == "root": self.sys_log.warning("Cannot delete the root folder.") - return + return False folder = self.get_folder(folder_name) - if folder: - # set folder to deleted state - folder.delete() - - # remove from folder list - self.folders.pop(folder.uuid) - - # add to deleted list - folder.remove_all_files() - - self.deleted_folders[folder.uuid] = folder - self.sys_log.info(f"Deleted folder /{folder.name} and its contents") - else: + if not folder: _LOGGER.debug(f"Cannot delete folder as it does not exist: {folder_name}") + return False - def delete_folder_by_id(self, folder_uuid: str): + # set folder to deleted state + folder.delete() + + # remove from folder list + self.folders.pop(folder.uuid) + + # add to deleted list + folder.remove_all_files() + + self.deleted_folders[folder.uuid] = folder + self.sys_log.info(f"Deleted folder /{folder.name} and its contents") + return True + + def delete_folder_by_id(self, folder_uuid: str) -> None: """ Deletes a folder via its uuid. @@ -297,7 +308,7 @@ class FileSystem(SimComponent): return file - def delete_file(self, folder_name: str, file_name: str): + def delete_file(self, folder_name: str, file_name: str) -> bool: """ Delete a file by its name from a specific folder. @@ -309,8 +320,10 @@ class FileSystem(SimComponent): file = folder.get_file(file_name) if file: folder.remove_file(file) + return True + return False - def delete_file_by_id(self, folder_uuid: str, file_uuid: str): + def delete_file_by_id(self, folder_uuid: str, file_uuid: str) -> None: """ Deletes a file via its uuid. @@ -327,7 +340,7 @@ class FileSystem(SimComponent): else: self.sys_log.error(f"Unable to delete file that does not exist. (id: {file_uuid})") - def move_file(self, src_folder_name: str, src_file_name: str, dst_folder_name: str): + def move_file(self, src_folder_name: str, src_file_name: str, dst_folder_name: str) -> None: """ Move a file from one folder to another. @@ -404,7 +417,7 @@ class FileSystem(SimComponent): # Agent actions ############################################################### - def scan(self, instant_scan: bool = False): + def scan(self, instant_scan: bool = False) -> None: """ Scan all the folders (and child files) in the file system. @@ -413,7 +426,7 @@ class FileSystem(SimComponent): for folder_id in self.folders: self.folders[folder_id].scan(instant_scan=instant_scan) - def reveal_to_red(self, instant_scan: bool = False): + def reveal_to_red(self, instant_scan: bool = False) -> None: """ Reveals all the folders (and child files) in the file system to the red agent. @@ -422,7 +435,7 @@ class FileSystem(SimComponent): for folder_id in self.folders: self.folders[folder_id].reveal_to_red(instant_scan=instant_scan) - def restore_folder(self, folder_name: str): + def restore_folder(self, folder_name: str) -> bool: """ Restore a folder. @@ -435,13 +448,14 @@ class FileSystem(SimComponent): if folder is None: self.sys_log.error(f"Unable to restore folder {folder_name}. Folder is not in deleted folder list.") - return + return False self.deleted_folders.pop(folder.uuid, None) folder.restore() self.folders[folder.uuid] = folder + return True - def restore_file(self, folder_name: str, file_name: str): + def restore_file(self, folder_name: str, file_name: str) -> bool: """ Restore a file. @@ -454,12 +468,15 @@ class FileSystem(SimComponent): :type: file_name: str """ folder = self.get_folder(folder_name=folder_name) + if not folder: + _LOGGER.debug(f"Cannot restore file {file_name} in folder {folder_name} as the folder does not exist.") + return False - if folder: - file = folder.get_file(file_name=file_name, include_deleted=True) + file = folder.get_file(file_name=file_name, include_deleted=True) - if file is None: - self.sys_log.error(f"Unable to restore file {file_name}. File does not exist.") - return + if not file: + msg = f"Unable to restore file {file_name}. File was not found." + self.sys_log.error(msg) + return False - folder.restore_file(file_name=file_name) + return folder.restore_file(file_name=file_name) 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 fbe5f4b3..efac97c3 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -6,6 +6,7 @@ from enum import Enum from typing import Dict, Optional from primaite import getLogger +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.system.core.sys_log import SysLog @@ -102,12 +103,26 @@ class FileSystemItemABC(SimComponent): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request(name="scan", request_type=RequestType(func=lambda request, context: self.scan())) - rm.add_request(name="checkhash", request_type=RequestType(func=lambda request, context: self.check_hash())) - rm.add_request(name="repair", request_type=RequestType(func=lambda request, context: self.repair())) - rm.add_request(name="restore", request_type=RequestType(func=lambda request, context: self.restore())) + rm.add_request( + name="scan", request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan())) + ) + rm.add_request( + name="checkhash", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.check_hash())), + ) + rm.add_request( + name="repair", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.repair())), + ) + rm.add_request( + name="restore", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.restore())), + ) - rm.add_request(name="corrupt", request_type=RequestType(func=lambda request, context: self.corrupt())) + rm.add_request( + name="corrupt", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.corrupt())), + ) return rm @@ -124,9 +139,9 @@ class FileSystemItemABC(SimComponent): return convert_size(self.size) @abstractmethod - def scan(self) -> None: + def scan(self) -> bool: """Scan the folder/file - updates the visible_health_status.""" - pass + return False @abstractmethod def reveal_to_red(self) -> None: @@ -134,7 +149,7 @@ class FileSystemItemABC(SimComponent): pass @abstractmethod - def check_hash(self) -> None: + def check_hash(self) -> bool: """ Checks the has of the file to detect any changes. @@ -142,30 +157,30 @@ class FileSystemItemABC(SimComponent): Return False if corruption is detected, otherwise True """ - pass + return False @abstractmethod - def repair(self) -> None: + def repair(self) -> bool: """ Repair the FileSystemItem. True if successfully repaired. False otherwise. """ - pass + return False @abstractmethod - def corrupt(self) -> None: + def corrupt(self) -> bool: """ Corrupt the FileSystemItem. True if successfully corrupted. False otherwise. """ - pass + return False @abstractmethod - def restore(self) -> None: + def restore(self) -> bool: """Restore the file/folder to the state before it got ruined.""" - pass + return False @abstractmethod def delete(self) -> None: diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 771dc7a0..9ef1ae59 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -5,6 +5,7 @@ from typing import Dict, Optional from prettytable import MARKDOWN, PrettyTable from primaite import getLogger +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.file_system.file import File from primaite.simulator.file_system.file_system_item_abc import FileSystemItemABC, FileSystemItemHealthStatus @@ -53,7 +54,9 @@ class Folder(FileSystemItemABC): rm = super()._init_request_manager() rm.add_request( name="delete", - request_type=RequestType(func=lambda request, context: self.remove_file_by_id(file_uuid=request[0])), + request_type=RequestType( + func=lambda request, context: RequestResponse.from_bool(self.remove_file_by_name(file_name=request[0])) + ), ) self._file_request_manager = RequestManager() rm.add_request( @@ -249,6 +252,21 @@ class Folder(FileSystemItemABC): file = self.get_file_by_id(file_uuid=file_uuid) self.remove_file(file=file) + def remove_file_by_name(self, file_name: str) -> bool: + """ + Remove a file using its name. + + :param file_name: filename + :type file_name: str + :return: Whether it was successfully removed. + :rtype: bool + """ + for f in self.files.values(): + if f.name == file_name: + self.remove_file(f) + return True + return False + def remove_all_files(self): """Removes all the files in the folder.""" for file_id in self.files: @@ -258,7 +276,7 @@ class Folder(FileSystemItemABC): self.files = {} - def restore_file(self, file_name: str): + def restore_file(self, file_name: str) -> bool: """ Restores a file. @@ -268,13 +286,14 @@ class Folder(FileSystemItemABC): file = self.get_file(file_name=file_name, include_deleted=True) if not file: self.sys_log.error(f"Unable to restore file {file_name}. File does not exist.") - return + return False file.restore() self.files[file.uuid] = file if file.deleted: self.deleted_files.pop(file.uuid) + return True def quarantine(self): """Quarantines the File System Folder.""" diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 82fae164..3349bed4 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -12,6 +12,7 @@ from pydantic import BaseModel, Field from primaite import getLogger from primaite.exceptions import NetworkError +from primaite.interface.request import RequestResponse from primaite.simulator import SIM_OUTPUT from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.domain.account import Account @@ -115,8 +116,8 @@ class NetworkInterface(SimComponent, ABC): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request("enable", RequestType(func=lambda request, context: self.enable())) - rm.add_request("disable", RequestType(func=lambda request, context: self.disable())) + rm.add_request("enable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.enable()))) + rm.add_request("disable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.disable()))) return rm @@ -140,14 +141,16 @@ class NetworkInterface(SimComponent, ABC): return state @abstractmethod - def enable(self): + def enable(self) -> bool: """Enable the interface.""" pass + return False @abstractmethod - def disable(self): + def disable(self) -> bool: """Disable the interface.""" pass + return False def _capture_nmne(self, frame: Frame, inbound: bool = True) -> None: """ @@ -783,16 +786,28 @@ class Node(SimComponent): self._application_request_manager = RequestManager() rm.add_request("application", RequestType(func=self._application_request_manager)) - rm.add_request("scan", RequestType(func=lambda request, context: self.reveal_to_red())) + rm.add_request( + "scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.reveal_to_red())) + ) - rm.add_request("shutdown", RequestType(func=lambda request, context: self.power_off())) - rm.add_request("startup", RequestType(func=lambda request, context: self.power_on())) - rm.add_request("reset", RequestType(func=lambda request, context: self.reset())) # TODO implement node reset - rm.add_request("logon", RequestType(func=lambda request, context: ...)) # TODO implement logon request - rm.add_request("logoff", RequestType(func=lambda request, context: ...)) # TODO implement logoff request + rm.add_request( + "shutdown", RequestType(func=lambda request, context: RequestResponse.from_bool(self.power_off())) + ) + rm.add_request("startup", RequestType(func=lambda request, context: RequestResponse.from_bool(self.power_on()))) + rm.add_request( + "reset", RequestType(func=lambda request, context: RequestResponse.from_bool(self.reset())) + ) # TODO implement node reset + rm.add_request( + "logon", RequestType(func=lambda request, context: RequestResponse.from_bool(False)) + ) # TODO implement logon request + rm.add_request( + "logoff", RequestType(func=lambda request, context: RequestResponse.from_bool(False)) + ) # TODO implement logoff request self._os_request_manager = RequestManager() - self._os_request_manager.add_request("scan", RequestType(func=lambda request, context: self.scan())) + self._os_request_manager.add_request( + "scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan())) + ) rm.add_request("os", RequestType(func=self._os_request_manager)) return rm @@ -973,7 +988,7 @@ class Node(SimComponent): self.file_system.apply_timestep(timestep=timestep) - def scan(self) -> None: + def scan(self) -> bool: """ Scan the node and all the items within it. @@ -987,8 +1002,9 @@ class Node(SimComponent): to the red agent. """ self.node_scan_countdown = self.node_scan_duration + return True - def reveal_to_red(self) -> None: + def reveal_to_red(self) -> bool: """ Reveals the node and all the items within it to the red agent. @@ -1002,34 +1018,40 @@ class Node(SimComponent): `revealed_to_red` to `True`. """ self.red_scan_countdown = self.node_scan_duration + return True - def power_on(self): + def power_on(self) -> bool: """Power on the Node, enabling its NICs if it is in the OFF state.""" - if self.operating_state == NodeOperatingState.OFF: - self.operating_state = NodeOperatingState.BOOTING - self.start_up_countdown = self.start_up_duration - if self.start_up_duration <= 0: self.operating_state = NodeOperatingState.ON self._start_up_actions() self.sys_log.info("Power on") for network_interface in self.network_interfaces.values(): network_interface.enable() + return True + if self.operating_state == NodeOperatingState.OFF: + self.operating_state = NodeOperatingState.BOOTING + self.start_up_countdown = self.start_up_duration + return True - def power_off(self): + return False + + def power_off(self) -> bool: """Power off the Node, disabling its NICs if it is in the ON state.""" + if self.shut_down_duration <= 0: + self._shut_down_actions() + self.operating_state = NodeOperatingState.OFF + self.sys_log.info("Power off") + return True if self.operating_state == NodeOperatingState.ON: for network_interface in self.network_interfaces.values(): network_interface.disable() self.operating_state = NodeOperatingState.SHUTTING_DOWN self.shut_down_countdown = self.shut_down_duration + return True + return False - if self.shut_down_duration <= 0: - self._shut_down_actions() - self.operating_state = NodeOperatingState.OFF - self.sys_log.info("Power off") - - def reset(self): + def reset(self) -> bool: """ Resets the node. @@ -1040,6 +1062,8 @@ class Node(SimComponent): self.is_resetting = True self.sys_log.info("Resetting") self.power_off() + return True + return False def connect_nic(self, network_interface: NetworkInterface, port_name: Optional[str] = None): """ diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 7f7190fd..2fab4a3d 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable from pydantic import validate_call +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.network.hardware.base import IPWiredNetworkInterface from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState @@ -308,19 +309,24 @@ class AccessControlList(SimComponent): rm.add_request( "add_rule", RequestType( - func=lambda request, context: self.add_rule( - action=ACLAction[request[0]], - protocol=None if request[1] == "ALL" else IPProtocol[request[1]], - src_ip_address=None if request[2] == "ALL" else IPv4Address(request[2]), - src_port=None if request[3] == "ALL" else Port[request[3]], - dst_ip_address=None if request[4] == "ALL" else IPv4Address(request[4]), - dst_port=None if request[5] == "ALL" else Port[request[5]], - position=int(request[6]), + func=lambda request, context: RequestResponse.from_bool( + self.add_rule( + action=ACLAction[request[0]], + protocol=None if request[1] == "ALL" else IPProtocol[request[1]], + src_ip_address=None if request[2] == "ALL" else IPv4Address(request[2]), + src_port=None if request[3] == "ALL" else Port[request[3]], + dst_ip_address=None if request[4] == "ALL" else IPv4Address(request[4]), + dst_port=None if request[5] == "ALL" else Port[request[5]], + position=int(request[6]), + ) ) ), ) - rm.add_request("remove_rule", RequestType(func=lambda request, context: self.remove_rule(int(request[0])))) + rm.add_request( + "remove_rule", + RequestType(func=lambda request, context: RequestResponse.from_bool(self.remove_rule(int(request[0])))), + ) return rm def describe_state(self) -> Dict: @@ -366,7 +372,7 @@ class AccessControlList(SimComponent): src_port: Optional[Port] = None, dst_port: Optional[Port] = None, position: int = 0, - ) -> None: + ) -> bool: """ Adds a new ACL rule to control network traffic based on specified criteria. @@ -423,10 +429,12 @@ class AccessControlList(SimComponent): src_port=src_port, dst_port=dst_port, ) + return True else: raise ValueError(f"Cannot add ACL rule, position {position} is out of bounds.") + return False - def remove_rule(self, position: int) -> None: + def remove_rule(self, position: int) -> bool: """ Remove an ACL rule from a specific position. @@ -437,8 +445,10 @@ class AccessControlList(SimComponent): rule = self._acl[position] # noqa self._acl[position] = None del rule + return True else: raise ValueError(f"Cannot remove ACL rule, position {position} is out of bounds.") + return False def is_permitted(self, frame: Frame) -> Tuple[bool, ACLRule]: """Check if a packet with the given properties is permitted through the ACL.""" diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 7b259ff4..12148683 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional from uuid import uuid4 from primaite import getLogger +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -37,7 +38,7 @@ class DatabaseClient(Application): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request("execute", RequestType(func=lambda request, context: self.execute())) + rm.add_request("execute", RequestType(func=lambda request, context: RequestResponse.from_bool(self.execute()))) return rm def execute(self) -> bool: 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 ee98ea8e..f71b1465 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 @@ -4,6 +4,7 @@ from typing import Dict, Optional from primaite import getLogger from primaite.game.science import simulate_trial +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -76,7 +77,10 @@ class DataManipulationBot(Application): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request(name="execute", request_type=RequestType(func=lambda request, context: self.attack())) + rm.add_request( + name="execute", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.attack())), + ) return rm @@ -179,21 +183,21 @@ class DataManipulationBot(Application): """ super().run() - def attack(self): + def attack(self) -> bool: """Perform the attack steps after opening the application.""" if not self._can_perform_action(): _LOGGER.debug("Data manipulation application attempted to execute but it cannot perform actions right now.") self.run() - self._application_loop() + return self._application_loop() - def _application_loop(self): + def _application_loop(self) -> bool: """ The main application loop of the bot, handling the attack process. This is the core loop where the bot sequentially goes through the stages of the attack. """ if not self._can_perform_action(): - return + return False if self.server_ip_address and self.payload: self.sys_log.info(f"{self.name}: Running") self._logon() @@ -205,8 +209,12 @@ class DataManipulationBot(Application): DataManipulationAttackStage.FAILED, ): self.attack_stage = DataManipulationAttackStage.NOT_STARTED + + return True + else: self.sys_log.error(f"{self.name}: Failed to start as it requires both a target_ip_address and payload.") + return False def apply_timestep(self, timestep: int) -> None: """ diff --git a/src/primaite/simulator/system/applications/red_applications/dos_bot.py b/src/primaite/simulator/system/applications/red_applications/dos_bot.py index 202fd189..05f87f03 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -4,6 +4,7 @@ from typing import Optional from primaite import getLogger from primaite.game.science import simulate_trial +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient @@ -59,7 +60,10 @@ class DoSBot(DatabaseClient): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request(name="execute", request_type=RequestType(func=lambda request, context: self.run())) + rm.add_request( + name="execute", + request_type=RequestType(func=lambda request, context: RequestResponse.from_bool(self.run())), + ) return rm @@ -97,26 +101,26 @@ class DoSBot(DatabaseClient): f"{repeat=}, {port_scan_p_of_success=}, {dos_intensity=}, {max_sessions=}." ) - def run(self): + def run(self) -> bool: """Run the Denial of Service Bot.""" super().run() - self._application_loop() + return self._application_loop() - def _application_loop(self): + def _application_loop(self) -> bool: """ The main application loop for the Denial of Service bot. The loop goes through the stages of a DoS attack. """ if not self._can_perform_action(): - return + return False # DoS bot cannot do anything without a target if not self.target_ip_address or not self.target_port: self.sys_log.error( f"{self.name} is not properly configured. {self.target_ip_address=}, {self.target_port=}" ) - return + return True self.clear_connections() self._perform_port_scan(p_of_success=self.port_scan_p_of_success) @@ -126,6 +130,7 @@ class DoSBot(DatabaseClient): self.attack_stage = DoSAttackStage.NOT_STARTED else: self.attack_stage = DoSAttackStage.COMPLETED + return True def _perform_port_scan(self, p_of_success: Optional[float] = 0.1): """ diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 9fa86328..5dee1dd5 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -6,6 +6,7 @@ from urllib.parse import urlparse from pydantic import BaseModel, ConfigDict from primaite import getLogger +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.http import ( HttpRequestMethod, @@ -52,7 +53,10 @@ class WebBrowser(Application): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request( - name="execute", request_type=RequestType(func=lambda request, context: self.get_webpage()) # noqa + name="execute", + request_type=RequestType( + func=lambda request, context: RequestResponse.from_bool(self.get_webpage()) + ), # noqa ) return rm diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 4102657c..706f166b 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -3,6 +3,7 @@ from enum import Enum from typing import Any, Dict, Optional from primaite import getLogger +from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.system.software import IOSoftware, SoftwareHealthState @@ -80,14 +81,14 @@ class Service(IOSoftware): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request("scan", RequestType(func=lambda request, context: self.scan())) - rm.add_request("stop", RequestType(func=lambda request, context: self.stop())) - rm.add_request("start", RequestType(func=lambda request, context: self.start())) - rm.add_request("pause", RequestType(func=lambda request, context: self.pause())) - rm.add_request("resume", RequestType(func=lambda request, context: self.resume())) - rm.add_request("restart", RequestType(func=lambda request, context: self.restart())) - rm.add_request("disable", RequestType(func=lambda request, context: self.disable())) - rm.add_request("enable", RequestType(func=lambda request, context: self.enable())) + rm.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()))) + rm.add_request("stop", RequestType(func=lambda request, context: RequestResponse.from_bool(self.stop()))) + rm.add_request("start", RequestType(func=lambda request, context: RequestResponse.from_bool(self.start()))) + rm.add_request("pause", RequestType(func=lambda request, context: RequestResponse.from_bool(self.pause()))) + rm.add_request("resume", RequestType(func=lambda request, context: RequestResponse.from_bool(self.resume()))) + rm.add_request("restart", RequestType(func=lambda request, context: RequestResponse.from_bool(self.restart()))) + rm.add_request("disable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.disable()))) + rm.add_request("enable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.enable()))) return rm @abstractmethod @@ -106,17 +107,19 @@ class Service(IOSoftware): state["health_state_visible"] = self.health_state_visible.value return state - def stop(self) -> None: + def stop(self) -> bool: """Stop the service.""" if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]: self.sys_log.info(f"Stopping service {self.name}") self.operating_state = ServiceOperatingState.STOPPED + return True + return False - def start(self, **kwargs) -> None: + def start(self, **kwargs) -> bool: """Start the service.""" # cant start the service if the node it is on is off if not super()._can_perform_action(): - return + return False if self.operating_state == ServiceOperatingState.STOPPED: self.sys_log.info(f"Starting service {self.name}") @@ -124,36 +127,47 @@ class Service(IOSoftware): # set software health state to GOOD if initially set to UNUSED if self.health_state_actual == SoftwareHealthState.UNUSED: self.set_health_state(SoftwareHealthState.GOOD) + return True + return False - def pause(self) -> None: + def pause(self) -> bool: """Pause the service.""" if self.operating_state == ServiceOperatingState.RUNNING: self.sys_log.info(f"Pausing service {self.name}") self.operating_state = ServiceOperatingState.PAUSED + return True + return False - def resume(self) -> None: + def resume(self) -> bool: """Resume paused service.""" if self.operating_state == ServiceOperatingState.PAUSED: self.sys_log.info(f"Resuming service {self.name}") self.operating_state = ServiceOperatingState.RUNNING + return True + return False - def restart(self) -> None: + def restart(self) -> bool: """Restart running service.""" if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]: self.sys_log.info(f"Pausing service {self.name}") self.operating_state = ServiceOperatingState.RESTARTING self.restart_countdown = self.restart_duration + return True + return False - def disable(self) -> None: + def disable(self) -> bool: """Disable the service.""" self.sys_log.info(f"Disabling Application {self.name}") self.operating_state = ServiceOperatingState.DISABLED + return True - def enable(self) -> None: + def enable(self) -> bool: """Enable the disabled service.""" if self.operating_state == ServiceOperatingState.DISABLED: self.sys_log.info(f"Enabling Application {self.name}") self.operating_state = ServiceOperatingState.STOPPED + return True + return False def apply_timestep(self, timestep: int) -> None: """ diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 8864659c..2af53886 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -5,6 +5,7 @@ from enum import Enum from ipaddress import IPv4Address, IPv4Network from typing import Any, Dict, Optional, TYPE_CHECKING, Union +from primaite.interface.request import RequestResponse from primaite.simulator.core import _LOGGER, RequestManager, RequestType, SimComponent from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState @@ -105,16 +106,18 @@ class Software(SimComponent): rm.add_request( "compromise", RequestType( - func=lambda request, context: self.set_health_state(SoftwareHealthState.COMPROMISED), + func=lambda request, context: RequestResponse.from_bool( + self.set_health_state(SoftwareHealthState.COMPROMISED) + ), ), ) rm.add_request( "patch", RequestType( - func=lambda request, context: self.patch(), + func=lambda request, context: RequestResponse.from_bool(self.patch()), ), ) - rm.add_request("scan", RequestType(func=lambda request, context: self.scan())) + rm.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()))) return rm def _get_session_details(self, session_id: str) -> Session: @@ -148,7 +151,7 @@ class Software(SimComponent): ) return state - def set_health_state(self, health_state: SoftwareHealthState) -> None: + def set_health_state(self, health_state: SoftwareHealthState) -> bool: """ Assign a new health state to this software. @@ -160,6 +163,7 @@ class Software(SimComponent): :type health_state: SoftwareHealthState """ self.health_state_actual = health_state + return True def install(self) -> None: """ @@ -180,15 +184,18 @@ class Software(SimComponent): """ pass - def scan(self) -> None: + def scan(self) -> bool: """Update the observed health status to match the actual health status.""" self.health_state_visible = self.health_state_actual + return True - def patch(self) -> None: + def patch(self) -> bool: """Perform a patch on the software.""" if self.health_state_actual in (SoftwareHealthState.COMPROMISED, SoftwareHealthState.GOOD): self._patching_countdown = self.patching_duration self.set_health_state(SoftwareHealthState.PATCHING) + return True + return False def _update_patch_status(self) -> None: """Update the patch status of the software.""" From 0447a05084d2422e1931bf34b3b215e5de8d638e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 8 Mar 2024 15:57:43 +0000 Subject: [PATCH 091/128] Add call validation --- src/primaite/game/agent/actions.py | 4 +-- src/primaite/game/game.py | 8 +++-- src/primaite/session/io.py | 2 +- src/primaite/simulator/core.py | 7 +++-- src/primaite/simulator/file_system/file.py | 30 +++++++++++-------- src/primaite/simulator/file_system/folder.py | 30 +++++++++++-------- .../simulator/network/hardware/base.py | 20 ++++++++----- .../wireless/wireless_access_point.py | 6 ++-- .../wireless/wireless_nic.py | 6 ++-- 9 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 84bd3f39..4d28328e 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -492,9 +492,9 @@ class NetworkACLAddRuleAction(AbstractAction): "add_rule", permission_str, protocol, - src_ip, + str(src_ip), src_port, - dst_ip, + str(dst_ip), dst_port, position, ] diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 394a8154..c94cb3ad 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -165,9 +165,13 @@ class PrimaiteGame: for _, agent in self.agents.items(): obs = agent.observation_manager.current_observation action_choice, options = agent.get_action(obs, timestep=self.step_counter) - agent_actions[agent.agent_name] = (action_choice, options) request = agent.format_request(action_choice, options) - self.simulation.apply_request(request) + response = self.simulation.apply_request(request) + agent_actions[agent.agent_name] = { + "action": action_choice, + "parameters": options, + "response": response.model_dump(), + } return agent_actions def advance_timestep(self) -> None: diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 3e21ed16..ed2b4d62 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -93,7 +93,7 @@ class PrimaiteIO: { "episode": episode, "timestep": timestep, - "agent_actions": {k: {"action": v[0], "parameters": v[1]} for k, v in agent_actions.items()}, + "agent_actions": agent_actions, } ] ) diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 64f33f6a..02481661 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -43,7 +43,7 @@ class RequestType(BaseModel): the request can be performed or not. """ - func: Callable[[List[str], Dict], RequestResponse] + func: Callable[[List[Union[str, int, float]], Dict], RequestResponse] """ ``func`` is a function that accepts a request and a context dict. Typically this would be a lambda function that invokes a class method of your SimComponent. For example if the component is a node and the request type is for @@ -73,7 +73,7 @@ class RequestManager(BaseModel): """maps request name to an RequestType object.""" @validate_call - def __call__(self, request: List[str], context: Dict) -> RequestResponse: + def __call__(self, request: List[Union[str, int, float]], context: Dict) -> RequestResponse: """ Process an request request. @@ -206,7 +206,8 @@ class SimComponent(BaseModel): } return state - def apply_request(self, request: List[str], context: Dict = {}) -> None: + @validate_call + def apply_request(self, request: List[Union[str, int, float]], context: Dict = {}) -> RequestResponse: """ Apply a request to a simulation component. Request data is passed in as a 'namespaced' list of strings. diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index d9b02e8e..4dc222fb 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -100,15 +100,16 @@ class File(FileSystemItemABC): state["file_type"] = self.file_type.name return state - def scan(self) -> None: + def scan(self) -> bool: """Updates the visible statuses of the file.""" if self.deleted: self.sys_log.error(f"Unable to scan deleted file {self.folder_name}/{self.name}") - return + return False path = self.folder.name + "/" + self.name self.sys_log.info(f"Scanning file {self.sim_path if self.sim_path else path}") self.visible_health_status = self.health_status + return True def reveal_to_red(self) -> None: """Reveals the folder/file to the red agent.""" @@ -117,7 +118,7 @@ class File(FileSystemItemABC): return self.revealed_to_red = True - def check_hash(self) -> None: + def check_hash(self) -> bool: """ Check if the file has been changed. @@ -127,7 +128,7 @@ class File(FileSystemItemABC): """ if self.deleted: self.sys_log.error(f"Unable to check hash of deleted file {self.folder_name}/{self.name}") - return + return False current_hash = None # if file is real, read the file contents @@ -149,12 +150,13 @@ class File(FileSystemItemABC): # if the previous hash and current hash do not match, mark file as corrupted if self.previous_hash is not current_hash: self.corrupt() + return True - def repair(self) -> None: + def repair(self) -> bool: """Repair a corrupted File by setting the status to FileSystemItemStatus.GOOD.""" if self.deleted: self.sys_log.error(f"Unable to repair deleted file {self.folder_name}/{self.name}") - return + return False # set file status to good if corrupt if self.health_status == FileSystemItemHealthStatus.CORRUPT: @@ -162,12 +164,13 @@ class File(FileSystemItemABC): path = self.folder.name + "/" + self.name self.sys_log.info(f"Repaired file {self.sim_path if self.sim_path else path}") + return True - def corrupt(self) -> None: + def corrupt(self) -> bool: """Corrupt a File by setting the status to FileSystemItemStatus.CORRUPT.""" if self.deleted: self.sys_log.error(f"Unable to corrupt deleted file {self.folder_name}/{self.name}") - return + return False # set file status to good if corrupt if self.health_status == FileSystemItemHealthStatus.GOOD: @@ -175,24 +178,27 @@ class File(FileSystemItemABC): path = self.folder.name + "/" + self.name self.sys_log.info(f"Corrupted file {self.sim_path if self.sim_path else path}") + return True - def restore(self) -> None: + def restore(self) -> bool: """Determines if the file needs to be repaired or unmarked as deleted.""" if self.deleted: self.deleted = False - return + return True if self.health_status == FileSystemItemHealthStatus.CORRUPT: self.health_status = FileSystemItemHealthStatus.GOOD path = self.folder.name + "/" + self.name self.sys_log.info(f"Restored file {self.sim_path if self.sim_path else path}") + return True - def delete(self): + def delete(self) -> bool: """Marks the file as deleted.""" if self.deleted: self.sys_log.error(f"Unable to delete an already deleted file {self.folder_name}/{self.name}") - return + return False self.deleted = True self.sys_log.info(f"File deleted {self.folder_name}/{self.name}") + return True diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 9ef1ae59..fff08b23 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -307,7 +307,7 @@ class Folder(FileSystemItemABC): """Returns true if the folder is being quarantined.""" pass - def scan(self, instant_scan: bool = False) -> None: + def scan(self, instant_scan: bool = False) -> bool: """ Update Folder visible status. @@ -315,7 +315,7 @@ class Folder(FileSystemItemABC): """ if self.deleted: self.sys_log.error(f"Unable to scan deleted folder {self.name}") - return + return False if instant_scan: for file_id in self.files: @@ -323,7 +323,7 @@ class Folder(FileSystemItemABC): file.scan() if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: self.visible_health_status = FileSystemItemHealthStatus.CORRUPT - return + return True if self.scan_countdown <= 0: # scan one file per timestep @@ -332,6 +332,7 @@ class Folder(FileSystemItemABC): else: # scan already in progress self.sys_log.info(f"Scan is already in progress {self.name} (id: {self.uuid})") + return True def reveal_to_red(self, instant_scan: bool = False): """ @@ -358,7 +359,7 @@ class Folder(FileSystemItemABC): # scan already in progress self.sys_log.info(f"Red Agent Scan is already in progress {self.name} (id: {self.uuid})") - def check_hash(self) -> None: + def check_hash(self) -> bool: """ Runs a :func:`check_hash` on all files in the folder. @@ -371,7 +372,7 @@ class Folder(FileSystemItemABC): """ if self.deleted: self.sys_log.error(f"Unable to check hash of deleted folder {self.name}") - return + return False # iterate through the files and run a check hash no_corrupted_files = True @@ -387,12 +388,13 @@ class Folder(FileSystemItemABC): self.corrupt() self.sys_log.info(f"Checking hash of folder {self.name} (id: {self.uuid})") + return True - def repair(self) -> None: + def repair(self) -> bool: """Repair a corrupted Folder by setting the folder and containing files status to FileSystemItemStatus.GOOD.""" if self.deleted: self.sys_log.error(f"Unable to repair deleted folder {self.name}") - return + return False # iterate through the files in the folder for file_id in self.files: @@ -406,8 +408,9 @@ class Folder(FileSystemItemABC): self.health_status = FileSystemItemHealthStatus.GOOD self.sys_log.info(f"Repaired folder {self.name} (id: {self.uuid})") + return True - def restore(self) -> None: + def restore(self) -> bool: """ If a Folder is corrupted, run a repair on the folder and its child files. @@ -423,12 +426,13 @@ class Folder(FileSystemItemABC): else: # scan already in progress self.sys_log.info(f"Folder restoration already in progress {self.name} (id: {self.uuid})") + return True - def corrupt(self) -> None: + def corrupt(self) -> bool: """Corrupt a File by setting the folder and containing files status to FileSystemItemStatus.CORRUPT.""" if self.deleted: self.sys_log.error(f"Unable to corrupt deleted folder {self.name}") - return + return False # iterate through the files in the folder for file_id in self.files: @@ -439,11 +443,13 @@ class Folder(FileSystemItemABC): self.health_status = FileSystemItemHealthStatus.CORRUPT self.sys_log.info(f"Corrupted folder {self.name} (id: {self.uuid})") + return True - def delete(self): + def delete(self) -> bool: """Marks the file as deleted. Prevents agent actions from occuring.""" if self.deleted: self.sys_log.error(f"Unable to delete an already deleted folder {self.name}") - return + return False self.deleted = True + return True diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 3349bed4..d5945653 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -287,24 +287,24 @@ class WiredNetworkInterface(NetworkInterface, ABC): _connected_link: Optional[Link] = None "The network link to which the network interface is connected." - def enable(self): + def enable(self) -> bool: """Attempt to enable the network interface.""" if self.enabled: - return + return True if not self._connected_node: _LOGGER.error(f"Interface {self} cannot be enabled as it is not connected to a Node") - return + return False if self._connected_node.operating_state != NodeOperatingState.ON: self._connected_node.sys_log.info( f"Interface {self} cannot be enabled as the connected Node is not powered on" ) - return + return False if not self._connected_link: self._connected_node.sys_log.info(f"Interface {self} cannot be enabled as there is no Link connected.") - return + return False self.enabled = True self._connected_node.sys_log.info(f"Network Interface {self} enabled") @@ -313,11 +313,12 @@ class WiredNetworkInterface(NetworkInterface, ABC): ) if self._connected_link: self._connected_link.endpoint_up() + return True - def disable(self): + def disable(self) -> bool: """Disable the network interface.""" if not self.enabled: - return + return True self.enabled = False if self._connected_node: self._connected_node.sys_log.info(f"Network Interface {self} disabled") @@ -325,6 +326,7 @@ class WiredNetworkInterface(NetworkInterface, ABC): _LOGGER.debug(f"Interface {self} disabled") if self._connected_link: self._connected_link.endpoint_down() + return True def connect_link(self, link: Link): """ @@ -499,7 +501,7 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): return state - def enable(self): + def enable(self) -> bool: """ Enables this wired network interface and attempts to send a "hello" message to the default gateway. @@ -515,8 +517,10 @@ class IPWiredNetworkInterface(WiredNetworkInterface, Layer3Interface, ABC): try: pass self._connected_node.default_gateway_hello() + return True except AttributeError: pass + return False @abstractmethod def receive_frame(self, frame: Frame) -> bool: 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 721814f8..4b73b6a8 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 @@ -51,13 +51,15 @@ class WirelessAccessPoint(IPWirelessNetworkInterface): return state - def enable(self): + def enable(self) -> bool: """Enable the interface.""" pass + return True - def disable(self): + def disable(self) -> bool: """Disable the interface.""" pass + return True def send_frame(self, frame: Frame) -> bool: """ 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 7b8f6f54..2e0a1823 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 @@ -48,13 +48,15 @@ class WirelessNIC(IPWirelessNetworkInterface): return state - def enable(self): + def enable(self) -> bool: """Enable the interface.""" pass + return True - def disable(self): + def disable(self) -> bool: """Disable the interface.""" pass + return True def send_frame(self, frame: Frame) -> bool: """ From 289b5c548ac9e9a19567d0911291ed064ef1c007 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 8 Mar 2024 17:14:41 +0000 Subject: [PATCH 092/128] Make a type alias for request & fix typo --- src/primaite/simulator/core.py | 15 +++++++-------- src/primaite/simulator/file_system/file_system.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 02481661..aeb4e865 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -11,6 +11,8 @@ from primaite.interface.request import RequestResponse _LOGGER = getLogger(__name__) +RequestFormat = List[Union[str, int, float]] + class RequestPermissionValidator(BaseModel): """ @@ -22,7 +24,7 @@ class RequestPermissionValidator(BaseModel): """ @abstractmethod - def __call__(self, request: List[str], context: Dict) -> bool: + def __call__(self, request: RequestFormat, context: Dict) -> bool: """Use the request and context parameters to decide whether the request should be permitted.""" pass @@ -30,7 +32,7 @@ class RequestPermissionValidator(BaseModel): class AllowAllValidator(RequestPermissionValidator): """Always allows the request.""" - def __call__(self, request: List[str], context: Dict) -> bool: + def __call__(self, request: RequestFormat, context: Dict) -> bool: """Always allow the request.""" return True @@ -43,7 +45,7 @@ class RequestType(BaseModel): the request can be performed or not. """ - func: Callable[[List[Union[str, int, float]], Dict], RequestResponse] + func: Callable[[RequestFormat, Dict], RequestResponse] """ ``func`` is a function that accepts a request and a context dict. Typically this would be a lambda function that invokes a class method of your SimComponent. For example if the component is a node and the request type is for @@ -72,8 +74,7 @@ class RequestManager(BaseModel): request_types: Dict[str, RequestType] = {} """maps request name to an RequestType object.""" - @validate_call - def __call__(self, request: List[Union[str, int, float]], context: Dict) -> RequestResponse: + def __call__(self, request: RequestFormat, context: Dict) -> RequestResponse: """ Process an request request. @@ -93,8 +94,6 @@ class RequestManager(BaseModel): f"Request {request} could not be processed because {request_key} is not a valid request name", "within this RequestManager", ) - # _LOGGER.error(msg) - # raise RuntimeError(msg) _LOGGER.debug(msg) return RequestResponse(status="unreachable", data={"reason": msg}) @@ -207,7 +206,7 @@ class SimComponent(BaseModel): return state @validate_call - def apply_request(self, request: List[Union[str, int, float]], context: Dict = {}) -> RequestResponse: + def apply_request(self, request: RequestFormat, context: Dict = {}) -> RequestResponse: """ Apply a request to a simulation component. Request data is passed in as a 'namespaced' list of strings. diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 3ff73a80..9e2a3b0e 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -62,7 +62,7 @@ class FileSystem(SimComponent): self._restore_manager.add_request( name="file", request_type=RequestType( - func=lambda request, context: RequestResponse( + func=lambda request, context: RequestResponse.from_bool( self.restore_file(folder_name=request[0], file_name=request[1]) ) ), From cc721056d89563d64aae94ae9c936480a7c6388a Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 8 Mar 2024 19:32:07 +0000 Subject: [PATCH 093/128] #2350: configurable NMNE category thresholds --- .../_package_data/data_manipulation.yaml | 5 + .../agent/observations/nic_observations.py | 175 ++++++++++++++++++ .../agent/observations/node_observations.py | 3 +- .../game/agent/observations/observations.py | 102 ---------- src/primaite/game/game.py | 7 +- ...software_installation_and_configuration.py | 11 +- .../test_game_options_config.py | 25 +++ .../observations/test_observations.py | 42 ++++- .../network/test_capture_nmne.py | 2 +- 9 files changed, 261 insertions(+), 111 deletions(-) create mode 100644 src/primaite/game/agent/observations/nic_observations.py create mode 100644 tests/integration_tests/configuration_file_parsing/test_game_options_config.py diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index dffb40ea..47204878 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -30,6 +30,11 @@ game: - ICMP - TCP - UDP + thresholds: + nmne: + high: 10 + medium: 5 + low: 0 agents: - ref: client_2_green_user diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py new file mode 100644 index 00000000..39298ffe --- /dev/null +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -0,0 +1,175 @@ +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING + +from gymnasium import spaces + +from primaite.game.agent.observations.observations import AbstractObservation +from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE +from primaite.simulator.network.nmne import CAPTURE_NMNE + +if TYPE_CHECKING: + from primaite.game.game import PrimaiteGame + + +class NicObservation(AbstractObservation): + """Observation of a Network Interface Card (NIC) in the network.""" + + low_nmne_threshold: int = 0 + """The minimum number of malicious network events to be considered low.""" + med_nmne_threshold: int = 5 + """The minimum number of malicious network events to be considered medium.""" + high_nmne_threshold: int = 10 + """The minimum number of malicious network events to be considered high.""" + + @property + def default_observation(self) -> Dict: + """The default NIC observation dict.""" + data = {"nic_status": 0} + if CAPTURE_NMNE: + data.update({"nmne": {"inbound": 0, "outbound": 0}}) + + return data + + def __init__( + self, + where: Optional[Tuple[str]] = None, + low_nmne_threshold: Optional[int] = 0, + med_nmne_threshold: Optional[int] = 5, + high_nmne_threshold: Optional[int] = 10, + ) -> None: + """Initialise NIC observation. + + :param where: Where in the simulation state dictionary to find the relevant information for this NIC. A typical + example may look like this: + ['network','nodes',,'NICs',] + If None, this denotes that the NIC does not exist and the observation will be populated with zeroes. + :type where: Optional[Tuple[str]], optional + """ + super().__init__() + self.where: Optional[Tuple[str]] = where + + if low_nmne_threshold or med_nmne_threshold or high_nmne_threshold: + self._validate_nmne_categories( + low_nmne_threshold=low_nmne_threshold, + med_nmne_threshold=med_nmne_threshold, + high_nmne_threshold=high_nmne_threshold, + ) + + def _validate_nmne_categories( + self, low_nmne_threshold: int = 0, med_nmne_threshold: int = 5, high_nmne_threshold: int = 10 + ): + """ + Validates the nmne threshold config. + + If the configuration is valid, the thresholds will be set, otherwise, an exception is raised. + + :param: low_nmne_threshold: The minimum number of malicious network events to be considered low + :param: med_nmne_threshold: The minimum number of malicious network events to be considered medium + :param: high_nmne_threshold: The minimum number of malicious network events to be considered high + """ + if high_nmne_threshold <= med_nmne_threshold: + raise Exception( + f"nmne_categories: high nmne count ({high_nmne_threshold}) must be greater " + f"than medium nmne count ({med_nmne_threshold})" + ) + + if med_nmne_threshold <= low_nmne_threshold: + raise Exception( + f"nmne_categories: medium nmne count ({med_nmne_threshold}) must be greater " + f"than low nmne count ({low_nmne_threshold})" + ) + + self.high_nmne_threshold = high_nmne_threshold + self.med_nmne_threshold = med_nmne_threshold + self.low_nmne_threshold = low_nmne_threshold + + def _categorise_mne_count(self, nmne_count: int) -> int: + """ + Categorise the number of Malicious Network Events (NMNEs) into discrete bins. + + This helps in classifying the severity or volume of MNEs into manageable levels for the agent. + + Bins are defined as follows: + - 0: No MNEs detected (0 events). + - 1: Low number of MNEs (default 1-5 events). + - 2: Moderate number of MNEs (default 6-10 events). + - 3: High number of MNEs (default more than 10 events). + + :param nmne_count: Number of MNEs detected. + :return: Bin number corresponding to the number of MNEs. Returns 0, 1, 2, or 3 based on the detected MNE count. + """ + if nmne_count > self.high_nmne_threshold: + return 3 + elif nmne_count > self.med_nmne_threshold: + return 2 + elif nmne_count > self.low_nmne_threshold: + return 1 + return 0 + + def observe(self, state: Dict) -> Dict: + """Generate observation based on the current state of the simulation. + + :param state: Simulation state dictionary + :type state: Dict + :return: Observation + :rtype: Dict + """ + if self.where is None: + return self.default_observation + nic_state = access_from_nested_dict(state, self.where) + + if nic_state is NOT_PRESENT_IN_STATE: + return self.default_observation + else: + obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2} + if CAPTURE_NMNE: + obs_dict.update({"nmne": {}}) + direction_dict = nic_state["nmne"].get("direction", {}) + inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) + inbound_count = inbound_keywords.get("*", 0) + outbound_keywords = direction_dict.get("outbound", {}).get("keywords", {}) + outbound_count = outbound_keywords.get("*", 0) + obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count) + obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count) + return obs_dict + + @property + def space(self) -> spaces.Space: + """Gymnasium space object describing the observation space shape.""" + return spaces.Dict( + { + "nic_status": spaces.Discrete(3), + "nmne": spaces.Dict({"inbound": spaces.Discrete(6), "outbound": spaces.Discrete(6)}), + } + ) + + @classmethod + def from_config(cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]]) -> "NicObservation": + """Create NIC observation from a config. + + :param config: Dictionary containing the configuration for this NIC observation. + :type config: Dict + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame + :param parent_where: Where in the simulation state dictionary to find the information about this NIC's parent + node. A typical location for a node ``where`` can be: ['network','nodes',] + :type parent_where: Optional[List[str]] + :return: Constructed NIC observation + :rtype: NicObservation + """ + low_nmne_threshold = None + med_nmne_threshold = None + high_nmne_threshold = None + + if game and game.options and game.options.thresholds and game.options.thresholds.get("nmne"): + threshold = game.options.thresholds["nmne"] + + low_nmne_threshold = int(threshold.get("low")) if threshold.get("low") is not None else None + med_nmne_threshold = int(threshold.get("medium")) if threshold.get("medium") is not None else None + high_nmne_threshold = int(threshold.get("high")) if threshold.get("high") is not None else None + + return cls( + where=parent_where + ["NICs", config["nic_num"]], + low_nmne_threshold=low_nmne_threshold, + med_nmne_threshold=med_nmne_threshold, + high_nmne_threshold=high_nmne_threshold, + ) diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index 93c6765b..f211a6b5 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -4,7 +4,8 @@ from gymnasium import spaces from primaite import getLogger from primaite.game.agent.observations.file_system_observations import FolderObservation -from primaite.game.agent.observations.observations import AbstractObservation, NicObservation +from primaite.game.agent.observations.nic_observations import NicObservation +from primaite.game.agent.observations.observations import AbstractObservation from primaite.game.agent.observations.software_observation import ServiceObservation from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE diff --git a/src/primaite/game/agent/observations/observations.py b/src/primaite/game/agent/observations/observations.py index 10e69ea5..6236b00d 100644 --- a/src/primaite/game/agent/observations/observations.py +++ b/src/primaite/game/agent/observations/observations.py @@ -7,7 +7,6 @@ from gymnasium import spaces from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE -from primaite.simulator.network.nmne import CAPTURE_NMNE _LOGGER = getLogger(__name__) @@ -116,107 +115,6 @@ class LinkObservation(AbstractObservation): return cls(where=["network", "links", game.ref_map_links[config["link_ref"]]]) -class NicObservation(AbstractObservation): - """Observation of a Network Interface Card (NIC) in the network.""" - - @property - def default_observation(self) -> Dict: - """The default NIC observation dict.""" - data = {"nic_status": 0} - if CAPTURE_NMNE: - data.update({"nmne": {"inbound": 0, "outbound": 0}}) - - return data - - def __init__(self, where: Optional[Tuple[str]] = None) -> None: - """Initialise NIC observation. - - :param where: Where in the simulation state dictionary to find the relevant information for this NIC. A typical - example may look like this: - ['network','nodes',,'NICs',] - If None, this denotes that the NIC does not exist and the observation will be populated with zeroes. - :type where: Optional[Tuple[str]], optional - """ - super().__init__() - self.where: Optional[Tuple[str]] = where - - def _categorise_mne_count(self, nmne_count: int) -> int: - """ - Categorise the number of Malicious Network Events (NMNEs) into discrete bins. - - This helps in classifying the severity or volume of MNEs into manageable levels for the agent. - - Bins are defined as follows: - - 0: No MNEs detected (0 events). - - 1: Low number of MNEs (1-5 events). - - 2: Moderate number of MNEs (6-10 events). - - 3: High number of MNEs (more than 10 events). - - :param nmne_count: Number of MNEs detected. - :return: Bin number corresponding to the number of MNEs. Returns 0, 1, 2, or 3 based on the detected MNE count. - """ - if nmne_count > 10: - return 3 - elif nmne_count > 5: - return 2 - elif nmne_count > 0: - return 1 - return 0 - - def observe(self, state: Dict) -> Dict: - """Generate observation based on the current state of the simulation. - - :param state: Simulation state dictionary - :type state: Dict - :return: Observation - :rtype: Dict - """ - if self.where is None: - return self.default_observation - nic_state = access_from_nested_dict(state, self.where) - - if nic_state is NOT_PRESENT_IN_STATE: - return self.default_observation - else: - obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2} - if CAPTURE_NMNE: - obs_dict.update({"nmne": {}}) - direction_dict = nic_state["nmne"].get("direction", {}) - inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) - inbound_count = inbound_keywords.get("*", 0) - outbound_keywords = direction_dict.get("outbound", {}).get("keywords", {}) - outbound_count = outbound_keywords.get("*", 0) - obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count) - obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count) - return obs_dict - - @property - def space(self) -> spaces.Space: - """Gymnasium space object describing the observation space shape.""" - return spaces.Dict( - { - "nic_status": spaces.Discrete(3), - "nmne": spaces.Dict({"inbound": spaces.Discrete(6), "outbound": spaces.Discrete(6)}), - } - ) - - @classmethod - def from_config(cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]]) -> "NicObservation": - """Create NIC observation from a config. - - :param config: Dictionary containing the configuration for this NIC observation. - :type config: Dict - :param game: Reference to the PrimaiteGame object that spawned this observation. - :type game: PrimaiteGame - :param parent_where: Where in the simulation state dictionary to find the information about this NIC's parent - node. A typical location for a node ``where`` can be: ['network','nodes',] - :type parent_where: Optional[List[str]] - :return: Constructed NIC observation - :rtype: NicObservation - """ - return cls(where=parent_where + ["NICs", config["nic_num"]]) - - class AclObservation(AbstractObservation): """Observation of an Access Control List (ACL) in the network.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 33f9186b..3edb8651 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,6 +1,6 @@ """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from pydantic import BaseModel, ConfigDict @@ -67,8 +67,13 @@ class PrimaiteGameOptions(BaseModel): model_config = ConfigDict(extra="forbid") max_episode_length: int = 256 + """Maximum number of episodes for the PrimAITE game.""" ports: List[str] + """A whitelist of available ports in the simulation.""" protocols: List[str] + """A whitelist of available protocols in the simulation.""" + thresholds: Optional[Dict] = {} + """A dict containing the thresholds used for determining what is acceptable during observations.""" class PrimaiteGame: 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 f993af5f..a5fcb372 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 @@ -5,8 +5,9 @@ from typing import Union import yaml from primaite.config.load import data_manipulation_config_path -from primaite.game.agent.interface import ProxyAgent, RandomAgent +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 APPLICATION_TYPES_MAPPING, PrimaiteGame, SERVICE_TYPES_MAPPING from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -43,15 +44,15 @@ def test_example_config(): # green agent 1 assert "client_2_green_user" in game.agents - assert isinstance(game.agents["client_2_green_user"], RandomAgent) + assert isinstance(game.agents["client_2_green_user"], ProbabilisticAgent) # green agent 2 assert "client_1_green_user" in game.agents - assert isinstance(game.agents["client_1_green_user"], RandomAgent) + assert isinstance(game.agents["client_1_green_user"], ProbabilisticAgent) # red agent - assert "client_1_data_manipulation_red_bot" in game.agents - assert isinstance(game.agents["client_1_data_manipulation_red_bot"], DataManipulationAgent) + assert "data_manipulation_attacker" in game.agents + assert isinstance(game.agents["data_manipulation_attacker"], DataManipulationAgent) # blue agent assert "defender" in game.agents 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 new file mode 100644 index 00000000..adbbf2b5 --- /dev/null +++ b/tests/integration_tests/configuration_file_parsing/test_game_options_config.py @@ -0,0 +1,25 @@ +from pathlib import Path +from typing import Union + +import yaml + +from primaite.config.load import data_manipulation_config_path +from primaite.game.game import PrimaiteGame +from tests import TEST_ASSETS_ROOT + +BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" + + +def load_config(config_path: Union[str, Path]) -> PrimaiteGame: + """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" + with open(config_path, "r") as f: + cfg = yaml.safe_load(f) + + return PrimaiteGame.from_config(cfg) + + +def test_thresholds(): + """Test that the game options can be parsed correctly.""" + game = load_config(data_manipulation_config_path()) + + assert game.options.thresholds is not None diff --git a/tests/integration_tests/game_layer/observations/test_observations.py b/tests/integration_tests/game_layer/observations/test_observations.py index eccda238..97df7882 100644 --- a/tests/integration_tests/game_layer/observations/test_observations.py +++ b/tests/integration_tests/game_layer/observations/test_observations.py @@ -1,6 +1,6 @@ import pytest -from primaite.game.agent.observations.observations import NicObservation +from primaite.game.agent.observations.nic_observations import NicObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC from primaite.simulator.sim_container import Simulation @@ -33,3 +33,43 @@ def test_nic(simulation): nic.disable() observation_state = nic_obs.observe(simulation.describe_state()) assert observation_state.get("nic_status") == 2 # disabled + + +def test_nic_categories(simulation): + """Test the NIC observation nmne count categories.""" + pc: Computer = simulation.network.get_node_by_hostname("client_1") + + nic_obs = NicObservation(where=["network", "nodes", pc.hostname, "NICs", 1]) + + assert nic_obs.high_nmne_threshold == 10 # default + assert nic_obs.med_nmne_threshold == 5 # default + assert nic_obs.low_nmne_threshold == 0 # default + + nic_obs = NicObservation( + where=["network", "nodes", pc.hostname, "NICs", 1], + low_nmne_threshold=3, + med_nmne_threshold=6, + high_nmne_threshold=9, + ) + + assert nic_obs.high_nmne_threshold == 9 + assert nic_obs.med_nmne_threshold == 6 + assert nic_obs.low_nmne_threshold == 3 + + with pytest.raises(Exception): + # should throw an error + NicObservation( + where=["network", "nodes", pc.hostname, "NICs", 1], + low_nmne_threshold=9, + med_nmne_threshold=6, + high_nmne_threshold=9, + ) + + with pytest.raises(Exception): + # should throw an error + NicObservation( + where=["network", "nodes", pc.hostname, "NICs", 1], + low_nmne_threshold=3, + med_nmne_threshold=9, + high_nmne_threshold=9, + ) diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index 4bbde32f..32d4ee8f 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -1,4 +1,4 @@ -from primaite.game.agent.observations.observations import NicObservation +from primaite.game.agent.observations.nic_observations import NicObservation from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.nmne import set_nmne_config from primaite.simulator.sim_container import Simulation From 31ae4672acc3c19ac0ed6991004a23b1fb32a99e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sat, 9 Mar 2024 20:47:57 +0000 Subject: [PATCH 094/128] Make nodes only accept requests when they're on --- src/primaite/interface/request.py | 6 +- .../simulator/network/hardware/base.py | 52 ++++++++--- .../test_simulation/__init__.py | 0 .../test_simulation/test_request_response.py | 92 +++++++++++++++++++ .../_primaite/_interface/__init__.py | 0 .../_primaite/_interface/test_request.py | 32 +++++++ 6 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 tests/integration_tests/test_simulation/__init__.py create mode 100644 tests/integration_tests/test_simulation/test_request_response.py create mode 100644 tests/unit_tests/_primaite/_interface/__init__.py create mode 100644 tests/unit_tests/_primaite/_interface/test_request.py diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py index 8e61c1cb..8e922ef9 100644 --- a/src/primaite/interface/request.py +++ b/src/primaite/interface/request.py @@ -1,6 +1,6 @@ from typing import Dict, ForwardRef, Literal -from pydantic import BaseModel, ConfigDict, validate_call +from pydantic import BaseModel, ConfigDict, StrictBool, validate_call RequestResponse = ForwardRef("RequestResponse") """This makes it possible to type-hint RequestResponse.from_bool return type.""" @@ -9,7 +9,7 @@ RequestResponse = ForwardRef("RequestResponse") class RequestResponse(BaseModel): """Schema for generic request responses.""" - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="forbid", strict=True) """Cannot have extra fields in the response. Anything custom goes into the data field.""" status: Literal["pending", "success", "failure", "unreachable"] = "pending" @@ -29,7 +29,7 @@ class RequestResponse(BaseModel): @classmethod @validate_call - def from_bool(cls, status_bool: bool) -> RequestResponse: + def from_bool(cls, status_bool: StrictBool) -> RequestResponse: """ Construct a basic request response from a boolean. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index d5945653..f3cf29bb 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -14,7 +14,7 @@ from primaite import getLogger from primaite.exceptions import NetworkError from primaite.interface.request import RequestResponse from primaite.simulator import SIM_OUTPUT -from primaite.simulator.core import RequestManager, RequestType, SimComponent +from primaite.simulator.core import RequestFormat, RequestManager, RequestPermissionValidator, RequestType, SimComponent from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState @@ -772,47 +772,69 @@ class Node(SimComponent): self.sys_log.current_episode = episode self.sys_log.setup_logger() + class _NodeIsOnValidator(RequestPermissionValidator): + """ + When requests come in, this validator will only let them through if the node is on. + + This is useful because no actions should be being resolved if the node is off. + """ + + node: Node + """Save a reference to the node instance.""" + + def __call__(self, request: RequestFormat, context: Dict) -> bool: + """Return whether the node is on or off.""" + return self.node.operating_state == NodeOperatingState.ON + def _init_request_manager(self) -> RequestManager: - # TODO: I see that this code is really confusing and hard to read right now... I think some of these things will - # need a better name and better documentation. + _node_is_on = Node._NodeIsOnValidator(node=self) + rm = super()._init_request_manager() # since there are potentially many services, create an request manager that can map service name self._service_request_manager = RequestManager() - rm.add_request("service", RequestType(func=self._service_request_manager)) + rm.add_request("service", RequestType(func=self._service_request_manager, validator=_node_is_on)) self._nic_request_manager = RequestManager() - rm.add_request("network_interface", RequestType(func=self._nic_request_manager)) + rm.add_request("network_interface", RequestType(func=self._nic_request_manager, validator=_node_is_on)) - rm.add_request("file_system", RequestType(func=self.file_system._request_manager)) + rm.add_request("file_system", RequestType(func=self.file_system._request_manager, validator=_node_is_on)) # currently we don't have any applications nor processes, so these will be empty self._process_request_manager = RequestManager() - rm.add_request("process", RequestType(func=self._process_request_manager)) + rm.add_request("process", RequestType(func=self._process_request_manager, validator=_node_is_on)) self._application_request_manager = RequestManager() - rm.add_request("application", RequestType(func=self._application_request_manager)) + rm.add_request("application", RequestType(func=self._application_request_manager, validator=_node_is_on)) rm.add_request( - "scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.reveal_to_red())) + "scan", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.reveal_to_red()), validator=_node_is_on + ), ) rm.add_request( - "shutdown", RequestType(func=lambda request, context: RequestResponse.from_bool(self.power_off())) + "shutdown", + RequestType( + func=lambda request, context: RequestResponse.from_bool(self.power_off()), validator=_node_is_on + ), ) rm.add_request("startup", RequestType(func=lambda request, context: RequestResponse.from_bool(self.power_on()))) rm.add_request( - "reset", RequestType(func=lambda request, context: RequestResponse.from_bool(self.reset())) + "reset", + RequestType(func=lambda request, context: RequestResponse.from_bool(self.reset()), validator=_node_is_on), ) # TODO implement node reset rm.add_request( - "logon", RequestType(func=lambda request, context: RequestResponse.from_bool(False)) + "logon", RequestType(func=lambda request, context: RequestResponse.from_bool(False), validator=_node_is_on) ) # TODO implement logon request rm.add_request( - "logoff", RequestType(func=lambda request, context: RequestResponse.from_bool(False)) + "logoff", RequestType(func=lambda request, context: RequestResponse.from_bool(False), validator=_node_is_on) ) # TODO implement logoff request self._os_request_manager = RequestManager() self._os_request_manager.add_request( - "scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan())) + "scan", + RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()), validator=_node_is_on), ) - rm.add_request("os", RequestType(func=self._os_request_manager)) + rm.add_request("os", RequestType(func=self._os_request_manager, validator=_node_is_on)) return rm diff --git a/tests/integration_tests/test_simulation/__init__.py b/tests/integration_tests/test_simulation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py new file mode 100644 index 00000000..09680740 --- /dev/null +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -0,0 +1,92 @@ +# 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 +# 2. test that sending a request to a software on a turned-off component results in a failure +# 3. test every implemented action under several different situation, some of which should lead to a success and some to a failure. + +import pytest + +from primaite.interface.request import RequestResponse +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.host.host_node import HostNode +from tests.conftest import TestApplication, TestService + + +def test_successful_application_requests(example_network): + net = example_network + + client_1 = net.get_node_by_hostname("client_1") + client_1.software_manager.install(TestApplication) + client_1.software_manager.software.get("TestApplication").run() + + resp_1 = net.apply_request(["node", "client_1", "application", "TestApplication", "scan"]) + assert resp_1 == RequestResponse(status="success", data={}) + resp_2 = net.apply_request(["node", "client_1", "application", "TestApplication", "patch"]) + assert resp_2 == RequestResponse(status="success", data={}) + resp_3 = net.apply_request(["node", "client_1", "application", "TestApplication", "compromise"]) + assert resp_3 == RequestResponse(status="success", data={}) + + +def test_successful_service_requests(example_network): + net = example_network + server_1 = net.get_node_by_hostname("server_1") + server_1.software_manager.install(TestService) + + # Careful: the order here is important, for example we cannot run "stop" unless we run "start" first + for verb in [ + "disable", + "enable", + "start", + "stop", + "start", + "restart", + "pause", + "resume", + "compromise", + "scan", + "patch", + ]: + resp_1 = net.apply_request(["node", "server_1", "service", "TestService", verb]) + assert resp_1 == RequestResponse(status="success", data={}) + server_1.apply_timestep(timestep=1) + server_1.apply_timestep(timestep=1) + server_1.apply_timestep(timestep=1) + server_1.apply_timestep(timestep=1) + server_1.apply_timestep(timestep=1) + server_1.apply_timestep(timestep=1) + server_1.apply_timestep(timestep=1) + # lazily apply timestep 7 times to make absolutely sure any time-based things like restart have a chance to finish + + +def test_non_existent_requests(example_network): + net = example_network + resp_1 = net.apply_request(["fake"]) + assert resp_1.status == "unreachable" + resp_2 = net.apply_request(["network", "node", "client_39", "application", "WebBrowser", "execute"]) + assert resp_2.status == "unreachable" + + +@pytest.mark.parametrize( + "node_request", + [ + ["node", "client_1", "file_system", "folder", "root", "scan"], + ["node", "client_1", "os", "scan"], + ["node", "client_1", "service", "DNSClient", "stop"], + ["node", "client_1", "application", "WebBrowser", "scan"], + ["node", "client_1", "network_interface", 1, "disable"], + ], +) +def test_request_fails_if_node_off(example_network, node_request): + """Test that requests succeed when the node is on, and fail if the node is off.""" + net = example_network + client_1: HostNode = net.get_node_by_hostname("client_1") + client_1.shut_down_duration = 0 + + assert client_1.operating_state == NodeOperatingState.ON + resp_1 = net.apply_request(node_request) + assert resp_1.status == "success" + + client_1.power_off() + assert client_1.operating_state == NodeOperatingState.OFF + resp_2 = net.apply_request(node_request) + assert resp_2.status == "failure" diff --git a/tests/unit_tests/_primaite/_interface/__init__.py b/tests/unit_tests/_primaite/_interface/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/_primaite/_interface/test_request.py b/tests/unit_tests/_primaite/_interface/test_request.py new file mode 100644 index 00000000..5c65b572 --- /dev/null +++ b/tests/unit_tests/_primaite/_interface/test_request.py @@ -0,0 +1,32 @@ +import pytest +from pydantic import ValidationError + +from primaite.interface.request import RequestResponse + + +def test_creating_response_object(): + """Test that we can create a response object with given parameters.""" + r1 = RequestResponse(status="success", data={"test_data": 1, "other_data": 2}) + r2 = RequestResponse(status="unreachable") + r3 = RequestResponse(data={"test_data": "is_good"}) + r4 = RequestResponse() + assert isinstance(r1, RequestResponse) + assert isinstance(r2, RequestResponse) + assert isinstance(r3, RequestResponse) + assert isinstance(r4, RequestResponse) + + +def test_creating_response_from_boolean(): + """Test that we can build a response with a single boolean.""" + r1 = RequestResponse.from_bool(True) + assert r1.status == "success" + + r2 = RequestResponse.from_bool(False) + assert r2.status == "failure" + + with pytest.raises(ValidationError): + r3 = RequestResponse.from_bool(1) + with pytest.raises(ValidationError): + r4 = RequestResponse.from_bool("good") + with pytest.raises(ValidationError): + r5 = RequestResponse.from_bool({"data": True}) From 359777f4f8ebcb06aa06a4c454e698b60f3beb11 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sat, 9 Mar 2024 23:06:53 +0000 Subject: [PATCH 095/128] Add tests for request success/fail --- src/primaite/simulator/network/networks.py | 12 ++-- .../test_simulation/test_request_response.py | 68 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index fa9d86ef..c1eef224 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -146,9 +146,12 @@ def arcd_uc2_network() -> Network: ) client_1.power_on() network.connect(endpoint_b=client_1.network_interface[1], endpoint_a=switch_2.network_interface[1]) - db_client_1 = client_1.software_manager.install(DatabaseClient) - db_client_1 = client_1.software_manager.software.get("DatabaseClient") + client_1.software_manager.install(DatabaseClient) + db_client_1: DatabaseClient = client_1.software_manager.software.get("DatabaseClient") + db_client_1.configure(server_ip_address=IPv4Address("192.168.1.14")) db_client_1.run() + web_browser_1 = client_1.software_manager.software.get("WebBrowser") + web_browser_1.target_url = "http://arcd.com/users/" client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") db_manipulation_bot.configure( @@ -170,9 +173,10 @@ def arcd_uc2_network() -> Network: client_2.power_on() client_2.software_manager.install(DatabaseClient) db_client_2 = client_2.software_manager.software.get("DatabaseClient") + db_client_2.configure(server_ip_address=IPv4Address("192.168.1.14")) db_client_2.run() - web_browser = client_2.software_manager.software.get("WebBrowser") - web_browser.target_url = "http://arcd.com/users/" + web_browser_2 = client_2.software_manager.software.get("WebBrowser") + web_browser_2.target_url = "http://arcd.com/users/" network.connect(endpoint_b=client_2.network_interface[1], endpoint_a=switch_2.network_interface[2]) # Domain Controller diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index 09680740..aee5c816 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -9,6 +9,8 @@ import pytest from primaite.interface.request import RequestResponse from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.host_node import HostNode +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.transmission.transport_layer import Port from tests.conftest import TestApplication, TestService @@ -90,3 +92,69 @@ def test_request_fails_if_node_off(example_network, node_request): assert client_1.operating_state == NodeOperatingState.OFF resp_2 = net.apply_request(node_request) assert resp_2.status == "failure" + + +class TestDataManipulationGreenRequests: + def test_node_off(self, uc2_network): + """Test that green requests succeed when the node is on and fail if the node is off.""" + net = uc2_network + + client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) + client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) + client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) + assert client_1_browser_execute.status == "success" + assert client_1_db_client_execute.status == "success" + assert client_2_browser_execute.status == "success" + assert client_2_db_client_execute.status == "success" + + client_1 = net.get_node_by_hostname("client_1") + client_2 = net.get_node_by_hostname("client_2") + + client_1.shut_down_duration = 0 + client_1.power_off() + client_2.shut_down_duration = 0 + client_2.power_off() + + client_1_browser_execute_off = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) + client_1_db_client_execute_off = net.apply_request( + ["node", "client_1", "application", "DatabaseClient", "execute"] + ) + client_2_browser_execute_off = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + client_2_db_client_execute_off = net.apply_request( + ["node", "client_2", "application", "DatabaseClient", "execute"] + ) + assert client_1_browser_execute_off.status == "failure" + assert client_1_db_client_execute_off.status == "failure" + assert client_2_browser_execute_off.status == "failure" + assert client_2_db_client_execute_off.status == "failure" + + def test_acl_block(self, uc2_network): + """Test that green requests succeed when not blocked by ACLs but fail when blocked.""" + net = uc2_network + + router: Router = net.get_node_by_hostname("router_1") + client_1: HostNode = net.get_node_by_hostname("client_1") + client_2: HostNode = net.get_node_by_hostname("client_2") + + client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) + client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + assert client_1_browser_execute.status == "success" + assert client_2_browser_execute.status == "success" + + router.acl.add_rule(ACLAction.DENY, src_port=Port.HTTP, dst_port=Port.HTTP, position=3) + client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) + client_2_browser_execute = net.apply_request(["node", "client_2", "application", "WebBrowser", "execute"]) + assert client_1_browser_execute.status == "failure" + assert client_2_browser_execute.status == "failure" + + client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) + client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) + assert client_1_db_client_execute.status == "success" + assert client_2_db_client_execute.status == "success" + + router.acl.add_rule(ACLAction.DENY, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER) + client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) + client_2_db_client_execute = net.apply_request(["node", "client_2", "application", "DatabaseClient", "execute"]) + assert client_1_db_client_execute.status == "failure" + assert client_2_db_client_execute.status == "failure" From f4684b0349d306a9c1cbe756fba3749c6beb5fd7 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sat, 9 Mar 2024 23:32:00 +0000 Subject: [PATCH 096/128] Fix how nmnes are getting put into obs space. --- src/primaite/game/agent/observations.py | 13 +++- .../Data-Manipulation-E2E-Demonstration.ipynb | 67 +++++++++++++------ .../simulator/network/hardware/base.py | 3 +- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index 82e11fe0..83d1c4be 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -367,6 +367,13 @@ class NicObservation(AbstractObservation): """ super().__init__() self.where: Optional[Tuple[str]] = where + if CAPTURE_NMNE: + self.nmne_inbound_last_step: int = 0 + """NMNEs persist for the whole episode, but we want to count per step. Keeping track of last step count lets + us find the difference.""" + self.nmne_outbound_last_step: int = 0 + """NMNEs persist for the whole episode, but we want to count per step. Keeping track of last step count lets + us find the difference.""" def _categorise_mne_count(self, nmne_count: int) -> int: """ @@ -414,8 +421,10 @@ class NicObservation(AbstractObservation): inbound_count = inbound_keywords.get("*", 0) outbound_keywords = direction_dict.get("outbound", {}).get("keywords", {}) outbound_count = outbound_keywords.get("*", 0) - obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count) - obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count) + obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count - self.nmne_inbound_last_step) + obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count - self.nmne_outbound_last_step) + self.nmne_inbound_last_step = inbound_count + self.nmne_outbound_last_step = outbound_count return obs_dict @property diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index e35e6126..1d7cb157 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -426,13 +426,13 @@ "def friendly_output_red_action(info):\n", " # parse the info dict form step output and write out what the red agent is doing\n", " red_info = info['agent_actions']['data_manipulation_attacker']\n", - " red_action = red_info[0]\n", + " red_action = red_info['action']\n", " if red_action == 'DONOTHING':\n", " red_str = 'DO NOTHING'\n", " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", - " client = \"client 1\" if red_info[1]['node_id'] == 0 else \"client 2\"\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" + " return red_str\n" ] }, { @@ -492,7 +492,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Also, the NMNE outbound of either client 1 (node 6) or client 2 (node 7) has increased from 0 to 1. This tells us which client is being used by the red agent." + "Also, the NMNE outbound of either client 1 (node 6) or client 2 (node 7) increased from 0 to 1, but only right after the red attack, so we probably cannot see it now." ] }, { @@ -510,9 +510,9 @@ "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", - "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'][0]}\" )\n", - "print(f\"Green action: {info['agent_actions']['client_1_green_user'][0]}\" )\n", - "print(f\"Green action: {info['agent_actions']['client_2_green_user'][0]}\" )\n", + "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker']['action']}\" )\n", + "print(f\"Green action: {info['agent_actions']['client_1_green_user']['action']}\" )\n", + "print(f\"Green action: {info['agent_actions']['client_2_green_user']['action']}\" )\n", "print(f\"Blue reward:{reward}\" )" ] }, @@ -535,7 +535,7 @@ "source": [ "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", - "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'][0]}\" )\n", + "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker']['action']}\" )\n", "print(f\"Green action: {info['agent_actions']['client_2_green_user']}\" )\n", "print(f\"Green action: {info['agent_actions']['client_1_green_user']}\" )\n", "print(f\"Blue reward:{reward:.2f}\" )" @@ -557,17 +557,17 @@ "outputs": [], "source": [ "env.step(13) # Patch the database\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )\n", "\n", "env.step(50) # Block client 1\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )\n", "\n", "env.step(51) # Block client 2\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )\n", "\n", "for step in range(30):\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )" ] }, { @@ -606,20 +606,35 @@ "metadata": {}, "outputs": [], "source": [ - "if obs['NODES'][6]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", - " # client 1 has NMNEs, let's unblock client 2\n", - " env.step(58) # remove ACL rule 6\n", - "elif obs['NODES'][7]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", - " env.step(57) # remove ACL rule 5\n", - "else:\n", - " print(\"something went wrong, neither client has NMNEs\")" + "env.step(58) # Remove the ACL rule that blocks client 1\n", + "env.step(57) # Remove the ACL rule that blocks client 2\n", + "\n", + "tries = 0\n", + "while True:\n", + " tries += 1\n", + " obs, reward, terminated, truncated, info = env.step(0)\n", + "\n", + " if obs['NODES'][6]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", + " # client 1 has NMNEs, let's block it\n", + " obs, reward, terminated, truncated, info = env.step(50) # block client 1\n", + " break\n", + " elif obs['NODES'][7]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", + " # client 2 has NMNEs, so let's block it\n", + " obs, reward, terminated, truncated, info = env.step(51) # block client 2\n", + " break\n", + " if tries>100:\n", + " print(\"Error: NMNE never increased\")\n", + " break\n", + "\n", + "env.step(13) # Patch the database\n", + "..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, the reward will eventually increase to 1.0, even after red agent attempts subsequent attacks." + "Now, the reward will eventually increase to 0.9, even after red agent attempts subsequent attacks." ] }, { @@ -628,9 +643,10 @@ "metadata": {}, "outputs": [], "source": [ - "for step in range(30):\n", + "\n", + "for step in range(40):\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'][0]}, Blue reward:{reward:.2f}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )" ] }, { @@ -648,6 +664,13 @@ "source": [ "env.reset()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index f3cf29bb..69c6b1da 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -260,10 +260,9 @@ class NetworkInterface(SimComponent, ABC): """ Apply a timestep evolution to this component. - This just clears the nmne count back to 0.tests/integration_tests/network/test_capture_nmne.py + This just clears the nmne count back to 0. """ super().apply_timestep(timestep=timestep) - self.nmne.clear() class WiredNetworkInterface(NetworkInterface, ABC): From e5c5a85003d462e6e61beeb6929443530375504d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 10 Mar 2024 13:05:57 +0000 Subject: [PATCH 097/128] Add docs on request response --- docs/Makefile | 2 +- docs/source/request_system.rst | 41 ++++++++++++++++++++++++++++------ docs/source/state_system.rst | 4 ++-- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index dd71ec33..82719283 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,7 +6,7 @@ SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build -AUTOSUMMARY="source\_autosummary" +AUTOSUMMARY="source/_autosummary" # Remove command is different depending on OS ifdef OS diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index e4c5584e..fb9d3978 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -3,7 +3,7 @@ © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK Request System -============== +************** ``SimComponent`` objects in the simulation are decoupled from the agent training logic. However, they still need a managed means of accepting requests to perform actions. For this, they use ``RequestManager`` and ``RequestType``. @@ -12,26 +12,37 @@ Just like other aspects of SimComponent, the request types are not managed centr - API When requesting an action within the simulation, these two arguments must be provided: - 1. ``request`` - selects which action you want to take on this ``SimComponent``. This is formatted as a list of strings such as `['network', 'node', '', 'service', '', 'restart']`. + 1. ``request`` - selects which action you want to take on this ``SimComponent``. This is formatted as a list of strings such as ``['network', 'node', '', 'service', '', 'restart']``. 2. ``context`` - optional extra information that can be used to decide how to process the request. This is formatted as a dictionary. For example, if the request requires authentication, the context can include information about the user that initiated the request to decide if their permissions are sufficient. + When a request is resolved, it returns a success status, and optional additional data about the request. + + ``status`` can be one of: + + * ``success``: the request was executed + * ``failure``: the request could not be executed + * ``unreachable``: the target for the request was not found + * ``pending``: the request was initiated, but has not finished during this step + + ``data`` can be a dictionary with any arbitrary JSON-like data to describe the outcome of the request. + - ``request`` detail The request is a list of strings which help specify who should handle the request. The strings in the request list help RequestManagers traverse the 'ownership tree' of SimComponent. The example given above would be handled in the following way: - 1. ``Simulation`` receives `['network', 'node', '', 'service', '', 'restart']`. + 1. ``Simulation`` receives ``['network', 'node', 'computer_1', 'service', 'DNSService', 'restart']``. The first element of the request is ``network``, therefore it passes the request down to its network. - 2. ``Network`` receives `['node', '', 'service', '', 'restart']`. + 2. ``Network`` receives ``['node', 'computer_1', 'service', 'DNSService', 'restart']``. The first element of the request is ``node``, therefore the network looks at the node name and passes the request down to the node with that name. - 3. ``Node`` receives `['service', '', 'restart']`. + 3. ``computer_1`` receives ``['service', 'DNSService', 'restart']``. The first element of the request is ``service``, therefore the node looks at the service name and passes the rest of the request to the service with that name. - 4. ``Service`` receives ``['restart']``. + 4. ``DNSService`` receives ``['restart']``. Since ``restart`` is a defined request type in the service's own RequestManager, the service performs a restart. - ``context`` detail The context is not used by any of the currently implemented components or requests. Technical Detail ----------------- +================ This system was achieved by implementing two classes, :py:class:`primaite.simulator.core.RequestType`, and :py:class:`primaite.simulator.core.RequestManager`. @@ -93,3 +104,19 @@ An example of how this works is in the :py:class:`primaite.simulator.network.har self._service_request_manager.add_request(service.name, RequestType(func=service._request_manager)) This process is repeated until the request word corresponds to a callable function rather than another ``RequestManager`` . + +Request Validation +------------------ + +There are times when a request should be rejected. For instance, if an agent attempts to run an application on a node that is currently off. For this purpose, requests are filtered by an object called a validator. :py:class:`primaite.simulator.core.RequestPermissionValidator` is a basic class whose ``__call__()`` method returns ``True`` if the request should be permitted or ``False`` if it cannot be permitted. For example, the Node class has a validator called :py:class:`primaite.simulator.network.hardware.base.Node._NodeIsOnValidator<_NodeIsOnValidator>` which allows requests only when the operating status of the node is ``ON``. + +Requests that are specified without a validator automatically get assigned an ``AllowAllValidator`` which allows requests no matter what. + +Request Response +---------------- + +The :py:class:`primaite.interface.request.RequestResponse` is a data transfer object that carries response data between the simulator and the game layer. The ``status`` field reports on the success or failure, and the ``data`` field is for any additional data. The most common way that this class is initiated is by its ``from_bool`` method. This way, given a True or False, a successful or failed request response is generated, respectively (with an empty data field). + +For instance, the ``execute`` action on a :py:class:`primaite.simulator.system.applications.web_browser.WebBrowser` calls the ``get_webpage()`` method of the ``WebBrowser``. ``get_webpage()`` returns a True if the webpage was successfully retrieved, and False if unsuccessful for any reason, such as being blocked by an ACL, or if the database server is unresponsive. The boolean returned from ``get_webpage()`` is used to create the request response. + +Just as the requests themselves were passed from owner to component, the request response is bubbled back up from component to owner until it arrives at the game layer. diff --git a/docs/source/state_system.rst b/docs/source/state_system.rst index 0bbbdd34..5fc12c23 100644 --- a/docs/source/state_system.rst +++ b/docs/source/state_system.rst @@ -5,9 +5,9 @@ Simulation State ================ -``SimComponent`` objects in the simulation have a method called ``describe_state`` which return a dictionary of the state of the component. This is used to report pertinent data that could impact an agent's actions or rewards. For instance, the name and health status of a node is reported, which can be used by a reward function to punish corrupted or compromised nodes and reward healthy nodes. Each ``SimComponent`` object reports not only its own attributes in the state but also those of its child components. I.e. a computer node will report the state of its ``FileSystem`` and the ``FileSystem`` will report the state of its files and folders. This happens by recursively calling the childrens' own ``describe_state`` methods. +``SimComponent`` objects in the simulation have a method called ``describe_state`` which return a dictionary of the state of the component. This is used to report pertinent data that could impact an agent's actions or rewards. For instance, the name and health status of a node is reported, which can be used by a reward function to punish corrupted or compromised nodes and reward healthy nodes. Each ``SimComponent`` object reports not only its own attributes in the state but also those of its child components. I.e. a computer node will report the state of its ``FileSystem`` and the ``FileSystem`` will report the state of its files and folders. This happens by recursively calling the children's own ``describe_state`` methods. -The game layer calls ``describe_state`` on the trunk ``SimComponent`` (the top-level parent) and then passes the state to the agents once per simulation step. For this reason, all ``SimComponent`` objetcs must have a ``describe_state`` method, and they must all be linked to the trunk ``SimComponent``. +The game layer calls ``describe_state`` on the trunk ``SimComponent`` (the top-level parent) and then passes the state to the agents once per simulation step. For this reason, all ``SimComponent`` objects must have a ``describe_state`` method, and they must all be linked to the trunk ``SimComponent``. This code snippet demonstrates how the state information is defined within the ``SimComponent`` class: From d2afcaa939d66449e75d9cd80131f70e8fe22953 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 10 Mar 2024 13:06:45 +0000 Subject: [PATCH 098/128] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf7b5c3..ae40a36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Made requests fail to reach their target if the node is off +- Added responses to requests - Made environment reset completely recreate the game object. - Changed the red agent in the data manipulation scenario to randomly choose client 1 or client 2 to start its attack. - Changed the data manipulation scenario to include a second green agent on client 1. From a228a099175aeb40acaad33af87d13b19a6c34ef Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sun, 10 Mar 2024 15:13:37 +0000 Subject: [PATCH 099/128] #2350: documentation --- docs/source/configuration/game.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/configuration/game.rst b/docs/source/configuration/game.rst index e43ea224..828571a7 100644 --- a/docs/source/configuration/game.rst +++ b/docs/source/configuration/game.rst @@ -23,6 +23,11 @@ This section defines high-level settings that apply across the game, currently i - ICMP - TCP - UDP + thresholds: + nmne: + high: 10 + medium: 5 + low: 0 ``max_episode_length`` ---------------------- @@ -44,3 +49,8 @@ See :ref:`List of Ports ` for a list of ports. A list of protocols that the Reinforcement Learning agent(s) are able to see in the observation space. See :ref:`List of IPProtocols ` for a list of protocols. + +``thresholds`` +-------------- + +These are used to determine the thresholds of high, medium and low categories for counted observation occurrences. From 66ab5ec980a806a384d0aa7c2fd9280aa9d35da4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 11 Mar 2024 09:18:31 +0000 Subject: [PATCH 100/128] Fix last tests --- .../component_creation/test_action_integration.py | 2 ++ tests/integration_tests/network/test_capture_nmne.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index f41a57af..e7c9fcc6 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -12,6 +12,8 @@ def test_passing_actions_down(monkeypatch) -> None: sim = Simulation() pc1 = Computer(hostname="PC-1", ip_address="10.10.1.1", subnet_mask="255.255.255.0") + pc1.start_up_duration = 0 + pc1.power_on() pc2 = Computer(hostname="PC-2", ip_address="10.10.1.2", subnet_mask="255.255.255.0") srv = Server(hostname="WEBSERVER", ip_address="10.10.1.100", subnet_mask="255.255.255.0") s1 = Switch(hostname="switch1") diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index 698bfc72..43bb176b 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -125,8 +125,8 @@ def test_describe_state_nmne(uc2_network): web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() uc2_network.apply_timestep(timestep=0) - assert web_server_nic_state["nmne"] == {} - assert db_server_nic_state["nmne"] == {} + assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} + assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} # Perform another "DELETE" query db_client.query("DELETE") @@ -135,8 +135,8 @@ def test_describe_state_nmne(uc2_network): web_server_nic_state = web_server_nic.describe_state() db_server_nic_state = db_server_nic.describe_state() uc2_network.apply_timestep(timestep=0) - assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 1}}}} - assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} + assert web_server_nic_state["nmne"] == {"direction": {"outbound": {"keywords": {"*": 2}}}} + assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 2}}}} def test_capture_nmne_observations(uc2_network): From 1faefbccac5b2e93041e044fbc2f0d2c1c5c6125 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 11 Mar 2024 10:20:47 +0000 Subject: [PATCH 101/128] Add docstring for init request manager --- src/primaite/simulator/domain/controller.py | 5 +++++ src/primaite/simulator/file_system/file_system.py | 5 +++++ .../simulator/file_system/file_system_item_abc.py | 5 +++++ src/primaite/simulator/file_system/folder.py | 5 +++++ src/primaite/simulator/network/container.py | 5 +++++ src/primaite/simulator/network/hardware/base.py | 10 ++++++++++ .../simulator/network/hardware/nodes/network/router.py | 10 ++++++++++ src/primaite/simulator/sim_container.py | 5 +++++ .../simulator/system/applications/database_client.py | 5 +++++ .../red_applications/data_manipulation_bot.py | 5 +++++ .../system/applications/red_applications/dos_bot.py | 5 +++++ .../simulator/system/applications/web_browser.py | 5 +++++ src/primaite/simulator/system/services/service.py | 5 +++++ src/primaite/simulator/system/software.py | 5 +++++ 14 files changed, 80 insertions(+) diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index 0936b5f8..432a1d9a 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -80,6 +80,11 @@ class DomainController(SimComponent): super().__init__(**kwargs) def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() # Action 'account' matches requests like: # ['account', '', *account_action] diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 9e2a3b0e..ade03412 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -36,6 +36,11 @@ class FileSystem(SimComponent): self.create_folder("root") def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() self._delete_manager = RequestManager() 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 efac97c3..32f5f6be 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -101,6 +101,11 @@ class FileSystemItemABC(SimComponent): return state def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request( diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index fff08b23..c3ddff8a 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -51,6 +51,11 @@ class Folder(FileSystemItemABC): self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})") def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request( name="delete", diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 6c2f38c5..0e970c3d 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -61,6 +61,11 @@ class Network(SimComponent): software.run() def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() self._node_request_manager = RequestManager() rm.add_request( diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 69c6b1da..a91a709c 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -114,6 +114,11 @@ class NetworkInterface(SimComponent, ABC): self.enable() def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request("enable", RequestType(func=lambda request, context: RequestResponse.from_bool(self.enable()))) @@ -786,6 +791,11 @@ class Node(SimComponent): return self.node.operating_state == NodeOperatingState.ON def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ _node_is_on = Node._NodeIsOnValidator(node=self) rm = super()._init_request_manager() diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 2fab4a3d..d2b47c1a 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -294,6 +294,11 @@ class AccessControlList(SimComponent): self._acl = [None] * (self.max_acl_rules - 1) def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ # TODO: Add src and dst wildcard masks as positional args in this request. rm = super()._init_request_manager() @@ -1092,6 +1097,11 @@ class Router(NetworkNode): super().setup_for_episode(episode=episode) def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request("acl", RequestType(func=self.acl._request_manager)) return rm diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 2f603f3a..997cc0be 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -27,6 +27,11 @@ class Simulation(SimComponent): self.network.setup_for_episode(episode=episode) def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() # pass through network requests to the network objects rm.add_request("network", RequestType(func=self.network._request_manager)) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 12148683..de7103f7 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -37,6 +37,11 @@ class DatabaseClient(Application): super().__init__(**kwargs) def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request("execute", RequestType(func=lambda request, context: RequestResponse.from_bool(self.execute()))) return rm 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 f71b1465..23e69e4d 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 @@ -75,6 +75,11 @@ class DataManipulationBot(Application): return db_client def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request( 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 05f87f03..27a4da05 100644 --- a/src/primaite/simulator/system/applications/red_applications/dos_bot.py +++ b/src/primaite/simulator/system/applications/red_applications/dos_bot.py @@ -58,6 +58,11 @@ class DoSBot(DatabaseClient): self.max_sessions = 1000 # override normal max sessions def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request( diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 5dee1dd5..a26570ed 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -51,6 +51,11 @@ class WebBrowser(Application): self.run() def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request( name="execute", diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 706f166b..e15377a9 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -80,6 +80,11 @@ class Service(IOSoftware): return super().receive(payload=payload, session_id=session_id, **kwargs) def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan()))) rm.add_request("stop", RequestType(func=lambda request, context: RequestResponse.from_bool(self.stop()))) diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 2af53886..d55f141f 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -102,6 +102,11 @@ class Software(SimComponent): "Current number of ticks left to patch the software." def _init_request_manager(self) -> RequestManager: + """ + Initialise the request manager. + + More information in user guide and docstring for SimComponent._init_request_manager. + """ rm = super()._init_request_manager() rm.add_request( "compromise", From cd6d6325db51ab7857efaf8af4fba03f06f79aa9 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 11 Mar 2024 17:47:33 +0000 Subject: [PATCH 102/128] #2350: add tests to check spaces + acl obs test + nmne space changes --- .../_package_data/data_manipulation.yaml | 2 - .../agent/observations/nic_observations.py | 29 ++++++-- .../observations/software_observation.py | 2 +- .../observations/test_acl_observations.py | 66 +++++++++++++++++ .../test_file_system_observations.py | 4 +- .../observations/test_link_observations.py | 73 +++++++++++++++++++ ...servations.py => test_nic_observations.py} | 22 ++++++ .../observations/test_node_observations.py | 3 + .../test_software_observations.py | 4 + 9 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 tests/integration_tests/game_layer/observations/test_acl_observations.py create mode 100644 tests/integration_tests/game_layer/observations/test_link_observations.py rename tests/integration_tests/game_layer/observations/{test_observations.py => test_nic_observations.py} (76%) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 47204878..a3a7e44a 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -22,8 +22,6 @@ io_settings: game: max_episode_length: 256 ports: - - ARP - - DNS - HTTP - POSTGRES_SERVER protocols: diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 39298ffe..735b41d4 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -20,6 +20,8 @@ class NicObservation(AbstractObservation): high_nmne_threshold: int = 10 """The minimum number of malicious network events to be considered high.""" + global CAPTURE_NMNE + @property def default_observation(self) -> Dict: """The default NIC observation dict.""" @@ -47,6 +49,15 @@ class NicObservation(AbstractObservation): super().__init__() self.where: Optional[Tuple[str]] = where + global CAPTURE_NMNE + if CAPTURE_NMNE: + self.nmne_inbound_last_step: int = 0 + """NMNEs persist for the whole episode, but we want to count per step. Keeping track of last step count lets + us find the difference.""" + self.nmne_outbound_last_step: int = 0 + """NMNEs persist for the whole episode, but we want to count per step. Keeping track of last step count lets + us find the difference.""" + if low_nmne_threshold or med_nmne_threshold or high_nmne_threshold: self._validate_nmne_categories( low_nmne_threshold=low_nmne_threshold, @@ -128,19 +139,21 @@ class NicObservation(AbstractObservation): inbound_count = inbound_keywords.get("*", 0) outbound_keywords = direction_dict.get("outbound", {}).get("keywords", {}) outbound_count = outbound_keywords.get("*", 0) - obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count) - obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count) + obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count - self.nmne_inbound_last_step) + obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count - self.nmne_outbound_last_step) + self.nmne_inbound_last_step = inbound_count + self.nmne_outbound_last_step = outbound_count return obs_dict @property def space(self) -> spaces.Space: """Gymnasium space object describing the observation space shape.""" - return spaces.Dict( - { - "nic_status": spaces.Discrete(3), - "nmne": spaces.Dict({"inbound": spaces.Discrete(6), "outbound": spaces.Discrete(6)}), - } - ) + space = spaces.Dict({"nic_status": spaces.Discrete(3)}) + + if CAPTURE_NMNE: + space["nmne"] = spaces.Dict({"inbound": spaces.Discrete(4), "outbound": spaces.Discrete(4)}) + + return space @classmethod def from_config(cls, config: Dict, game: "PrimaiteGame", parent_where: Optional[List[str]]) -> "NicObservation": diff --git a/src/primaite/game/agent/observations/software_observation.py b/src/primaite/game/agent/observations/software_observation.py index ff61714a..6caf791c 100644 --- a/src/primaite/game/agent/observations/software_observation.py +++ b/src/primaite/game/agent/observations/software_observation.py @@ -51,7 +51,7 @@ class ServiceObservation(AbstractObservation): @property def space(self) -> spaces.Space: """Gymnasium space object describing the observation space shape.""" - return spaces.Dict({"operating_status": spaces.Discrete(7), "health_status": spaces.Discrete(6)}) + return spaces.Dict({"operating_status": spaces.Discrete(7), "health_status": spaces.Discrete(5)}) @classmethod def from_config( diff --git a/tests/integration_tests/game_layer/observations/test_acl_observations.py b/tests/integration_tests/game_layer/observations/test_acl_observations.py new file mode 100644 index 00000000..93867edd --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_acl_observations.py @@ -0,0 +1,66 @@ +import pytest + +from primaite.game.agent.observations.observations import AclObservation +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.sim_container import Simulation +from primaite.simulator.system.services.ntp.ntp_client import NTPClient +from primaite.simulator.system.services.ntp.ntp_server import NTPServer + + +@pytest.fixture(scope="function") +def simulation(example_network) -> Simulation: + sim = Simulation() + + # set simulation network as example network + sim.network = example_network + + return sim + + +def test_acl_observations(simulation): + """Test the ACL rule observations.""" + router: Router = simulation.network.get_node_by_hostname("router_1") + client_1: Computer = simulation.network.get_node_by_hostname("client_1") + server: Computer = simulation.network.get_node_by_hostname("server_1") + + # quick set up of ntp + client_1.software_manager.install(NTPClient) + ntp_client: NTPClient = client_1.software_manager.software.get("NTPClient") + ntp_client.configure(server.network_interface.get(1).ip_address) + server.software_manager.install(NTPServer) + + # add router acl rule + router.acl.add_rule(action=ACLAction.PERMIT, dst_port=Port.NTP, src_port=Port.NTP, position=1) + + acl_obs = AclObservation( + where=["network", "nodes", router.hostname, "acl", "acl"], + node_ip_to_id={}, + ports=["NTP", "HTTP", "POSTGRES_SERVER"], + protocols=["TCP", "UDP", "ICMP"], + ) + + observation_space = acl_obs.observe(simulation.describe_state()) + assert observation_space.get(1) is not None + rule_obs = observation_space.get(1) # this is the ACL Rule added to allow NTP + assert rule_obs.get("position") == 0 # rule was put at position 1 (0 because counting from 1 instead of 1) + assert rule_obs.get("permission") == 1 # permit = 1 deny = 2 + assert rule_obs.get("source_node_id") == 1 # applies to all source nodes + assert rule_obs.get("dest_node_id") == 1 # applies to all destination nodes + assert rule_obs.get("source_port") == 2 # NTP port is mapped to value 2 (1 = ALL, so 1+1 = 2 quik mafs) + assert rule_obs.get("dest_port") == 2 # NTP port is mapped to value 2 + assert rule_obs.get("protocol") == 1 # 1 = No Protocol + + router.acl.remove_rule(1) + + observation_space = acl_obs.observe(simulation.describe_state()) + assert observation_space.get(1) is not None + rule_obs = observation_space.get(1) # this is the ACL Rule added to allow NTP + assert rule_obs.get("position") == 0 + assert rule_obs.get("permission") == 0 + assert rule_obs.get("source_node_id") == 0 + assert rule_obs.get("dest_node_id") == 0 + assert rule_obs.get("source_port") == 0 + assert rule_obs.get("dest_port") == 0 + assert rule_obs.get("protocol") == 0 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 808007cc..35bb95fd 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 @@ -26,7 +26,7 @@ def test_file_observation(simulation): where=["network", "nodes", pc.hostname, "file_system", "folders", "root", "files", "dog.png"] ) - assert dog_file_obs.space == spaces.Dict({"health_status": spaces.Discrete(6)}) + 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 @@ -52,6 +52,8 @@ def test_folder_observation(simulation): where=["network", "nodes", pc.hostname, "file_system", "folders", "test_folder"] ) + assert root_folder_obs.space["health_status"] == spaces.Discrete(6) + observation_state = root_folder_obs.observe(simulation.describe_state()) assert observation_state.get("FILES") is not None assert observation_state.get("health_status") == 1 diff --git a/tests/integration_tests/game_layer/observations/test_link_observations.py b/tests/integration_tests/game_layer/observations/test_link_observations.py new file mode 100644 index 00000000..bfe4d5cc --- /dev/null +++ b/tests/integration_tests/game_layer/observations/test_link_observations.py @@ -0,0 +1,73 @@ +import pytest +from gymnasium import spaces + +from primaite.game.agent.observations.observations import LinkObservation +from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.base import Link, Node +from primaite.simulator.network.hardware.nodes.host.computer import Computer +from primaite.simulator.network.hardware.nodes.host.server import Server +from primaite.simulator.sim_container import Simulation + + +@pytest.fixture(scope="function") +def simulation() -> Simulation: + sim = Simulation() + + network = Network() + + # Create Computer + computer = Computer( + hostname="computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + computer.power_on() + + # Create Server + server = Server( + hostname="server", + ip_address="192.168.1.3", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + start_up_duration=0, + ) + server.power_on() + + # Connect Computer and Server + network.connect(computer.network_interface[1], server.network_interface[1]) + + # Should be linked + assert next(iter(network.links.values())).is_up + + assert computer.ping(server.network_interface.get(1).ip_address) + + # set simulation network as example network + sim.network = network + + return sim + + +def test_link_observation(simulation): + """Test the link observation.""" + # get a link + link: Link = next(iter(simulation.network.links.values())) + + computer: Computer = simulation.network.get_node_by_hostname("computer") + server: Server = simulation.network.get_node_by_hostname("server") + + simulation.apply_timestep(0) # some pings when network was made - reset with apply timestep + + link_obs = LinkObservation(where=["network", "links", link.uuid]) + + assert link_obs.space["PROTOCOLS"]["ALL"] == spaces.Discrete(11) # test that the spaces are 0-10 including 0 and 10 + + observation_state = link_obs.observe(simulation.describe_state()) + assert observation_state.get("PROTOCOLS") is not None + assert observation_state["PROTOCOLS"]["ALL"] == 0 + + computer.ping(server.network_interface.get(1).ip_address) + + observation_state = link_obs.observe(simulation.describe_state()) + assert observation_state["PROTOCOLS"]["ALL"] == 1 diff --git a/tests/integration_tests/game_layer/observations/test_observations.py b/tests/integration_tests/game_layer/observations/test_nic_observations.py similarity index 76% rename from tests/integration_tests/game_layer/observations/test_observations.py rename to tests/integration_tests/game_layer/observations/test_nic_observations.py index 97df7882..c210b751 100644 --- a/tests/integration_tests/game_layer/observations/test_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -1,9 +1,27 @@ +from pathlib import Path +from typing import Union + import pytest +import yaml +from gymnasium import spaces from primaite.game.agent.observations.nic_observations import NicObservation +from primaite.game.game import PrimaiteGame from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC +from primaite.simulator.network.nmne import CAPTURE_NMNE from primaite.simulator.sim_container import Simulation +from tests import TEST_ASSETS_ROOT + +BASIC_CONFIG = TEST_ASSETS_ROOT / "configs/basic_switched_network.yaml" + + +def load_config(config_path: Union[str, Path]) -> PrimaiteGame: + """Returns a PrimaiteGame object which loads the contents of a given yaml path.""" + with open(config_path, "r") as f: + cfg = yaml.safe_load(f) + + return PrimaiteGame.from_config(cfg) @pytest.fixture(scope="function") @@ -24,6 +42,10 @@ def test_nic(simulation): nic_obs = NicObservation(where=["network", "nodes", pc.hostname, "NICs", 1]) + assert nic_obs.space["nic_status"] == spaces.Discrete(3) + assert nic_obs.space["nmne"]["inbound"] == spaces.Discrete(4) + assert nic_obs.space["nmne"]["outbound"] == spaces.Discrete(4) + observation_state = nic_obs.observe(simulation.describe_state()) assert observation_state.get("nic_status") == 1 # enabled assert observation_state.get("nmne") is not None 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 835202c6..b1563fbd 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -2,6 +2,7 @@ import copy from uuid import uuid4 import pytest +from gymnasium import spaces from primaite.game.agent.observations.node_observations import NodeObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -24,6 +25,8 @@ def test_node_observation(simulation): node_obs = NodeObservation(where=["network", "nodes", pc.hostname]) + assert node_obs.space["operating_status"] == spaces.Discrete(5) + observation_state = node_obs.observe(simulation.describe_state()) assert observation_state.get("operating_status") == 1 # computer is on 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 17fc386f..4ae0701e 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,5 @@ import pytest +from gymnasium import spaces from primaite.game.agent.observations.software_observation import ApplicationObservation, ServiceObservation from primaite.simulator.network.hardware.nodes.host.computer import Computer @@ -29,6 +30,9 @@ def test_service_observation(simulation): service_obs = ServiceObservation(where=["network", "nodes", pc.hostname, "services", "NTPServer"]) + assert service_obs.space["operating_status"] == spaces.Discrete(7) + assert service_obs.space["health_status"] == spaces.Discrete(5) + observation_state = service_obs.observe(simulation.describe_state()) assert observation_state.get("health_status") == 0 From 759965587982931bd039df6a4084ff7aa364cbdd Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 11 Mar 2024 20:10:08 +0000 Subject: [PATCH 103/128] Add agent action history --- .../game/agent/data_manipulation_bot.py | 5 +- src/primaite/game/agent/interface.py | 37 ++++++++++++-- src/primaite/game/agent/rewards.py | 37 ++++++++++---- src/primaite/game/game.py | 51 +++++++++---------- src/primaite/interface/request.py | 4 +- .../Data-Manipulation-E2E-Demonstration.ipynb | 10 ++-- src/primaite/session/environment.py | 25 ++++----- src/primaite/session/io.py | 41 +++------------ src/primaite/simulator/core.py | 4 +- 9 files changed, 110 insertions(+), 104 deletions(-) diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/data_manipulation_bot.py index 16453433..d3ec19cb 100644 --- a/src/primaite/game/agent/data_manipulation_bot.py +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -14,7 +14,7 @@ class DataManipulationAgent(AbstractScriptedAgent): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.reset_agent_for_episode() + self.setup_agent() def _set_next_execution_timestep(self, timestep: int) -> None: """Set the next execution timestep with a configured random variance. @@ -43,9 +43,8 @@ class DataManipulationAgent(AbstractScriptedAgent): return "NODE_APPLICATION_EXECUTE", {"node_id": self.starting_node_idx, "application_id": 0} - def reset_agent_for_episode(self) -> None: + def setup_agent(self) -> None: """Set the next execution timestep when the episode resets.""" - super().reset_agent_for_episode() self._select_start_node() self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 88848479..0531b25f 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,6 +1,6 @@ """Interface for agents.""" from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Tuple, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING from gymnasium.core import ActType, ObsType from pydantic import BaseModel, model_validator @@ -8,11 +8,31 @@ from pydantic import BaseModel, model_validator from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction +from primaite.interface.request import RequestFormat, RequestResponse if TYPE_CHECKING: pass +class AgentActionHistoryItem(BaseModel): + """One entry of an agent's action log - what the agent did and how the simulator responded in 1 step.""" + + timestep: int + """Timestep of this action.""" + + action: str + """CAOS Action name.""" + + parameters: Dict[str, Any] + """CAOS parameters for the given action.""" + + request: RequestFormat + """The request that was sent to the simulation based on the CAOS action chosen.""" + + response: RequestResponse + """The response sent back by the simulator for this action.""" + + class AgentStartSettings(BaseModel): """Configuration values for when an agent starts performing actions.""" @@ -90,6 +110,7 @@ class AbstractAgent(ABC): self.observation_manager: Optional[ObservationManager] = observation_space self.reward_function: Optional[RewardFunction] = reward_function self.agent_settings = agent_settings or AgentSettings() + self.action_history: List[AgentActionHistoryItem] = [] def update_observation(self, state: Dict) -> ObsType: """ @@ -109,7 +130,7 @@ class AbstractAgent(ABC): :return: Reward from the state. :rtype: float """ - return self.reward_function.update(state) + return self.reward_function.update(state=state, last_action_response=self.action_history[-1]) @abstractmethod def get_action(self, obs: ObsType, timestep: int = 0) -> Tuple[str, Dict]: @@ -138,9 +159,15 @@ class AbstractAgent(ABC): request = self.action_manager.form_request(action_identifier=action, action_options=options) return request - def reset_agent_for_episode(self) -> None: - """Agent reset logic should go here.""" - pass + 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.action_history.append( + AgentActionHistoryItem( + timestep=timestep, action=action, parameters=parameters, request=request, response=response + ) + ) class AbstractScriptedAgent(AbstractAgent): diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 8c8e36ad..6ab5aa42 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -26,11 +26,14 @@ the structure: ``` """ from abc import abstractmethod -from typing import Dict, List, Tuple, Type +from typing import Dict, List, Tuple, Type, TYPE_CHECKING 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 AgentActionHistoryItem + _LOGGER = getLogger(__name__) @@ -38,7 +41,9 @@ class AbstractReward: """Base class for reward function components.""" @abstractmethod - def calculate(self, state: Dict) -> float: + def calculate( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """Calculate the reward for the current state.""" return 0.0 @@ -58,7 +63,9 @@ class AbstractReward: class DummyReward(AbstractReward): """Dummy reward function component which always returns 0.""" - def calculate(self, state: Dict) -> float: + def calculate( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """Calculate the reward for the current state.""" return 0.0 @@ -98,7 +105,9 @@ class DatabaseFileIntegrity(AbstractReward): file_name, ] - def calculate(self, state: Dict) -> float: + def calculate( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """Calculate the reward for the current state. :param state: The current state of the simulation. @@ -153,7 +162,9 @@ class WebServer404Penalty(AbstractReward): """ self.location_in_state = ["network", "nodes", node_hostname, "services", service_name] - def calculate(self, state: Dict) -> float: + def calculate( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """Calculate the reward for the current state. :param state: The current state of the simulation. @@ -206,7 +217,9 @@ class WebpageUnavailablePenalty(AbstractReward): self._node = node_hostname self.location_in_state = ["network", "nodes", node_hostname, "applications", "WebBrowser"] - def calculate(self, state: Dict) -> float: + def calculate( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """ Calculate the reward based on current simulation state. @@ -255,13 +268,17 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): self._node = node_hostname self.location_in_state = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] - def calculate(self, state: Dict) -> float: + def calculate( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """ Calculate the reward based on current simulation state. :param state: The current state of the simulation. :type state: Dict """ + if last_action_response.request == ["network", "node", "client_2", "application", "DatabaseClient", "execute"]: + pass # TODO db_state = access_from_nested_dict(state, self.location_in_state) if db_state is NOT_PRESENT_IN_STATE or "last_connection_successful" not in db_state: _LOGGER.debug(f"Can't calculate reward for {self.__class__.__name__}") @@ -313,7 +330,9 @@ class RewardFunction: """ self.reward_components.append((component, weight)) - def update(self, state: Dict) -> float: + def update( + self, state: Dict, last_action_response: "AgentActionHistoryItem" + ) -> float: # todo maybe make last_action_response optional? """Calculate the overall reward for the current state. :param state: The current state of the simulation. @@ -323,7 +342,7 @@ class RewardFunction: for comp_and_weight in self.reward_components: comp = comp_and_weight[0] weight = comp_and_weight[1] - total += weight * comp.calculate(state=state) + total += weight * comp.calculate(state=state, last_action_response=last_action_response) self.current_reward = total return self.current_reward diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index c94cb3ad..1cc8cfed 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,6 +1,6 @@ """PrimAITE game - Encapsulates the simulation and agents.""" from ipaddress import IPv4Address -from typing import Dict, List, Tuple +from typing import Dict, List from pydantic import BaseModel, ConfigDict @@ -130,49 +130,44 @@ class PrimaiteGame: """ _LOGGER.debug(f"Stepping. Step counter: {self.step_counter}") - # Get the current state of the simulation - sim_state = self.get_sim_state() - - # Update agents' observations and rewards based on the current state - self.update_agents(sim_state) - # Apply all actions to simulation as requests - self.apply_agent_actions() + action_data = self.apply_agent_actions() # Advance timestep self.advance_timestep() + # Get the current state of the simulation + sim_state = self.get_sim_state() + + # Update agents' observations and rewards based on the current state, and the response from the last action + self.update_agents(state=sim_state, action_data=action_data) + def get_sim_state(self) -> Dict: """Get the current state of the simulation.""" return self.simulation.describe_state() def update_agents(self, state: Dict) -> None: """Update agents' observations and rewards based on the current state.""" - for _, agent in self.agents.items(): - agent.update_observation(state) - agent.update_reward(state) + for agent_name, agent in self.agents.items(): + if self.step_counter > 0: # can't get reward before first action + agent.update_reward(state=state) + agent.update_observation(state=state) agent.reward_function.total_reward += agent.reward_function.current_reward - def apply_agent_actions(self) -> Dict[str, Tuple[str, Dict]]: - """ - Apply all actions to simulation as requests. - - :return: A recap of each agent's actions, in CAOS format. - :rtype: Dict[str, Tuple[str, Dict]] - - """ - agent_actions = {} + def apply_agent_actions(self) -> None: + """Apply all actions to simulation as requests.""" for _, agent in self.agents.items(): obs = agent.observation_manager.current_observation - action_choice, options = agent.get_action(obs, timestep=self.step_counter) - request = agent.format_request(action_choice, options) + action_choice, parameters = agent.get_action(obs, timestep=self.step_counter) + request = agent.format_request(action_choice, parameters) response = self.simulation.apply_request(request) - agent_actions[agent.agent_name] = { - "action": action_choice, - "parameters": options, - "response": response.model_dump(), - } - return agent_actions + agent.process_action_response( + timestep=self.step_counter, + action=action_choice, + parameters=parameters, + request=request, + response=response, + ) def advance_timestep(self) -> None: """Advance timestep.""" diff --git a/src/primaite/interface/request.py b/src/primaite/interface/request.py index 8e922ef9..bc076599 100644 --- a/src/primaite/interface/request.py +++ b/src/primaite/interface/request.py @@ -1,7 +1,9 @@ -from typing import Dict, ForwardRef, Literal +from typing import Dict, ForwardRef, List, Literal, Union from pydantic import BaseModel, ConfigDict, StrictBool, validate_call +RequestFormat = List[Union[str, int, float]] + RequestResponse = ForwardRef("RequestResponse") """This makes it possible to type-hint RequestResponse.from_bool return type.""" diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 1d7cb157..b2522c2b 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -373,7 +373,7 @@ "# Imports\n", "from primaite.config.load import data_manipulation_config_path\n", "from primaite.session.environment import PrimaiteGymEnv\n", - "from primaite.game.game import PrimaiteGame\n", + "from primaite.game.agent.interface import AgentActionHistoryItem\n", "import yaml\n", "from pprint import pprint\n" ] @@ -425,14 +425,14 @@ "source": [ "def friendly_output_red_action(info):\n", " # parse the info dict form step output and write out what the red agent is doing\n", - " red_info = info['agent_actions']['data_manipulation_attacker']\n", - " red_action = red_info['action']\n", + " red_info : AgentActionHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", + " red_action = red_info.action\n", " if red_action == 'DONOTHING':\n", " red_str = 'DO NOTHING'\n", " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", - " client = \"client 1\" if red_info['parameters']['node_id'] == 0 else \"client 2\"\n", + " client = \"client 1\" if red_info.parameters['node_id'] == 0 else \"client 2\"\n", " red_str = f\"ATTACK from {client}\"\n", - " return red_str\n" + " return red_str" ] }, { diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 87638e7d..64534b04 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -49,23 +49,20 @@ class PrimaiteGymEnv(gymnasium.Env): # make ProxyAgent store the action chosen my the RL policy self.agent.store_action(action) # apply_agent_actions accesses the action we just stored - agent_actions = self.game.apply_agent_actions() + self.game.apply_agent_actions() self.game.advance_timestep() state = self.game.get_sim_state() - self.game.update_agents(state) - next_obs = self._get_obs() + next_obs = self._get_obs() # this doesn't update observation, just gets the current observation reward = self.agent.reward_function.current_reward terminated = False truncated = self.game.calculate_truncated() - info = {"agent_actions": agent_actions} # tell us what all the agents did for convenience. + info = { + "agent_actions": {name: agent.action_history[-1] for name, agent in self.game.agents.items()} + } # tell us what all the agents did for convenience. if self.game.save_step_metadata: self._write_step_metadata_json(action, state, reward) - if self.io.settings.save_agent_actions: - self.io.store_agent_actions( - agent_actions=agent_actions, episode=self.episode_counter, timestep=self.game.step_counter - ) return next_obs, reward, terminated, truncated, info def _write_step_metadata_json(self, action: int, state: Dict, reward: int): @@ -91,13 +88,13 @@ class PrimaiteGymEnv(gymnasium.Env): f"avg. reward: {self.agent.reward_function.total_reward}" ) if self.io.settings.save_agent_actions: - self.io.write_agent_actions(episode=self.episode_counter) - self.io.clear_agent_actions() + all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} + self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=copy.deepcopy(self.game_config)) self.game.setup_for_episode(episode=self.episode_counter) self.episode_counter += 1 state = self.game.get_sim_state() - self.game.update_agents(state) + self.game.update_agents(state=state) next_obs = self._get_obs() info = {} return next_obs, info @@ -217,7 +214,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): # 1. Perform actions for agent_name, action in actions.items(): self.agents[agent_name].store_action(action) - agent_actions = self.game.apply_agent_actions() + self.game.apply_agent_actions() # 2. Advance timestep self.game.advance_timestep() @@ -236,10 +233,6 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): truncateds["__all__"] = self.game.calculate_truncated() if self.game.save_step_metadata: self._write_step_metadata_json(actions, state, rewards) - if self.io.settings.save_agent_actions: - self.io.store_agent_actions( - agent_actions=agent_actions, episode=self.episode_counter, timestep=self.game.step_counter - ) return next_obs, rewards, terminateds, truncateds, infos def _write_step_metadata_json(self, actions: Dict, state: Dict, rewards: Dict): diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index ed2b4d62..87289c43 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -48,8 +48,6 @@ class PrimaiteIO: SIM_OUTPUT.save_pcap_logs = self.settings.save_pcap_logs SIM_OUTPUT.save_sys_logs = self.settings.save_sys_logs - self.agent_action_log: List[Dict] = [] - def generate_session_path(self, timestamp: Optional[datetime] = None) -> Path: """Create a folder for the session and return the path to it.""" if timestamp is None: @@ -72,48 +70,23 @@ class PrimaiteIO: """Return the path where agent actions will be saved.""" return self.session_path / "agent_actions" / f"episode_{episode}.json" - def store_agent_actions(self, agent_actions: Dict, episode: int, timestep: int) -> None: - """Cache agent actions for a particular step. - - :param agent_actions: Dictionary describing actions for any agents that acted in this timestep. The expected - format contains agent identifiers as keys. The keys should map to a tuple of [CAOS action, parameters] - CAOS action is a string representing one the CAOS actions. - parameters is a dict of parameter names and values for that particular CAOS action. - For example: - { - 'green1' : ('NODE_APPLICATION_EXECUTE', {'node_id':1, 'application_id':0}), - 'defender': ('DO_NOTHING', {}) - } - :type agent_actions: Dict - :param timestep: Simulation timestep when these actions occurred. - :type timestep: int - """ - self.agent_action_log.append( - [ - { - "episode": episode, - "timestep": timestep, - "agent_actions": agent_actions, - } - ] - ) - - def write_agent_actions(self, episode: int) -> None: + def write_agent_actions(self, agent_actions: Dict[str, List], episode: int) -> None: """Take the contents of the agent action log and write it to a file. :param episode: Episode number :type episode: int """ + data = {} + longest_history = max([len(hist) for hist in agent_actions]) + for i in range(longest_history): + data[i] = {"timestep": i, "episode": episode, **{name: acts[i] for name, acts in agent_actions.items()}} + path = self.generate_agent_actions_save_path(episode=episode) path.parent.mkdir(exist_ok=True, parents=True) path.touch() _LOGGER.info(f"Saving agent action log to {path}") with open(path, "w") as file: - json.dump(self.agent_action_log, fp=file, indent=1) - - def clear_agent_actions(self) -> None: - """Reset the agent action log back to an empty dictionary.""" - self.agent_action_log = [] + json.dump(data, fp=file, indent=1) @classmethod def from_config(cls, config: Dict) -> "PrimaiteIO": diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index aeb4e865..6da8a2f8 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -7,12 +7,10 @@ from uuid import uuid4 from pydantic import BaseModel, ConfigDict, Field, validate_call from primaite import getLogger -from primaite.interface.request import RequestResponse +from primaite.interface.request import RequestFormat, RequestResponse _LOGGER = getLogger(__name__) -RequestFormat = List[Union[str, int, float]] - class RequestPermissionValidator(BaseModel): """ From c3f1cfb33d3516fcca84b5ffe944b46d080d1cf3 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 11 Mar 2024 22:53:39 +0000 Subject: [PATCH 104/128] Add shared reward --- .../_package_data/data_manipulation.yaml | 39 ++++--- src/primaite/game/agent/rewards.py | 100 +++++++++++++++--- src/primaite/game/game.py | 55 +++++++++- src/primaite/game/science.py | 79 ++++++++++++++ src/primaite/session/io.py | 7 +- 5 files changed, 242 insertions(+), 38 deletions(-) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index dffb40ea..f4789e50 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -73,7 +73,14 @@ agents: reward_function: reward_components: - - type: DUMMY + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 - ref: client_1_green_user team: GREEN @@ -116,7 +123,14 @@ agents: reward_function: reward_components: - - type: DUMMY + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_1 @@ -696,22 +710,15 @@ agents: node_hostname: database_server folder_name: database file_name: database.db - - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.25 + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: client_1 - - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.25 + agent_name: client_1_green_user + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.05 - options: - node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.05 - options: - node_hostname: client_2 + agent_name: client_2_green_user + agent_settings: diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 6ab5aa42..86a61535 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -26,7 +26,9 @@ the structure: ``` """ from abc import abstractmethod -from typing import Dict, List, Tuple, Type, TYPE_CHECKING +from typing import Callable, Dict, List, Optional, Tuple, Type, TYPE_CHECKING + +from typing_extensions import Never from primaite import getLogger from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_STATE @@ -214,18 +216,29 @@ class WebpageUnavailablePenalty(AbstractReward): :param node_hostname: Hostname of the node which has the web browser. :type node_hostname: str """ - self._node = node_hostname - self.location_in_state = ["network", "nodes", node_hostname, "applications", "WebBrowser"] + self._node: str = node_hostname + self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] + self._last_request_failed: bool = False def calculate( self, state: Dict, last_action_response: "AgentActionHistoryItem" ) -> float: # todo maybe make last_action_response optional? """ - Calculate the reward based on current simulation state. + Calculate the reward based on current simulation state, and the recent agent action. - :param state: The current state of the simulation. - :type state: Dict + When the green agent requests to execute the browser application, and that request fails, this reward + component will keep track of that information. In that case, it doesn't matter whether the last webpage + had a 200 status code, because there has been an unsuccessful request since. """ + if last_action_response.request == ["network", "node", self._node, "application", "DatabaseClient", "execute"]: + self._last_request_failed = last_action_response.response.status != "success" + + # if agent couldn't even get as far as sending the request (because for example the node was off), then + # apply a penalty + if self._last_request_failed: + return -1.0 + + # If the last request did actually go through, then check if the webpage also loaded web_browser_state = access_from_nested_dict(state, self.location_in_state) if web_browser_state is NOT_PRESENT_IN_STATE or "history" not in web_browser_state: _LOGGER.info( @@ -265,20 +278,28 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): :param node_hostname: Hostname of the node where the database client sits. :type node_hostname: str """ - self._node = node_hostname - self.location_in_state = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] + self._node: str = node_hostname + self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "DatabaseClient"] + self._last_request_failed: bool = False - def calculate( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """ - Calculate the reward based on current simulation state. + Calculate the reward based on current simulation state, and the recent agent action. - :param state: The current state of the simulation. - :type state: Dict + When the green agent requests to execute the database client application, and that request fails, this reward + component will keep track of that information. In that case, it doesn't matter whether the last successful + request returned was able to connect to the database server, because there has been an unsuccessful request + since. """ - if last_action_response.request == ["network", "node", "client_2", "application", "DatabaseClient", "execute"]: - pass # TODO + if last_action_response.request == ["network", "node", self._node, "application", "DatabaseClient", "execute"]: + self._last_request_failed = last_action_response.response.status != "success" + + # if agent couldn't even get as far as sending the request (because for example the node was off), then + # apply a penalty + if self._last_request_failed: + return -1.0 + + # If the last request was actually sent, then check if the connection was established. db_state = access_from_nested_dict(state, self.location_in_state) if db_state is NOT_PRESENT_IN_STATE or "last_connection_successful" not in db_state: _LOGGER.debug(f"Can't calculate reward for {self.__class__.__name__}") @@ -301,6 +322,52 @@ class GreenAdminDatabaseUnreachablePenalty(AbstractReward): return cls(node_hostname=node_hostname) +class SharedReward(AbstractReward): + """Adds another agent's reward to the overall reward.""" + + def __init__(self, agent_name: Optional[str] = None) -> None: + """ + Initialise the shared reward. + + The agent_ref is a placeholder value. It starts off as none, but it must be set before this reward can work + correctly. + + :param agent_name: The name whose reward is an input + :type agent_ref: Optional[str] + """ + self.agent_name = agent_name + """Agent whose reward to track.""" + + def default_callback() -> Never: + """ + Default callback to prevent calling this reward until it's properly initialised. + + SharedReward should not be used until the game layer replaces self.callback with a reference to the + function that retrieves the desired agent's reward. Therefore, we define this default callback that raises + an error. + """ + raise RuntimeError("Attempted to calculate SharedReward but it was not initialised properly.") + + self.callback: Callable[[], float] = default_callback + """Method that retrieves an agent's current reward given the agent's name.""" + + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: + """Simply access the other agent's reward and return it.""" + print(self.callback(), self.agent_name) + return self.callback() + + @classmethod + def from_config(cls, config: Dict) -> "SharedReward": + """ + Build the SharedReward object from config. + + :param config: Configuration dictionary + :type config: Dict + """ + agent_name = config.get("agent_name") + return cls(agent_name=agent_name) + + class RewardFunction: """Manages the reward function for the agent.""" @@ -310,6 +377,7 @@ class RewardFunction: "WEB_SERVER_404_PENALTY": WebServer404Penalty, "WEBPAGE_UNAVAILABLE_PENALTY": WebpageUnavailablePenalty, "GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY": GreenAdminDatabaseUnreachablePenalty, + "SHARED_REWARD": SharedReward, } """List of reward class identifiers.""" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 1cc8cfed..e766bcd3 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -9,8 +9,9 @@ from primaite.game.agent.actions import ActionManager from primaite.game.agent.data_manipulation_bot import DataManipulationAgent from primaite.game.agent.interface import AbstractAgent, AgentSettings, ProxyAgent from primaite.game.agent.observations import ObservationManager -from primaite.game.agent.rewards import RewardFunction +from primaite.game.agent.rewards import RewardFunction, SharedReward from primaite.game.agent.scripted_agents import ProbabilisticAgent +from primaite.game.science import graph_has_cycle, topological_sort from primaite.simulator.network.hardware.base import NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.host_node import NIC @@ -110,6 +111,9 @@ class PrimaiteGame: self.save_step_metadata: bool = False """Whether to save the RL agents' action, environment state, and other data at every single step.""" + self._reward_calculation_order: List[str] = [name for name in self.agents] + """Agent order for reward evaluation, as some rewards can be dependent on other agents' rewards.""" + def step(self): """ Perform one step of the simulation/agent loop. @@ -148,10 +152,11 @@ class PrimaiteGame: def update_agents(self, state: Dict) -> None: """Update agents' observations and rewards based on the current state.""" - for agent_name, agent in self.agents.items(): + for agent_name in self._reward_calculation_order: + agent = self.agents[agent_name] if self.step_counter > 0: # can't get reward before first action agent.update_reward(state=state) - agent.update_observation(state=state) + agent.update_observation(state=state) # order of this doesn't matter so just use reward order agent.reward_function.total_reward += agent.reward_function.current_reward def apply_agent_actions(self) -> None: @@ -443,7 +448,51 @@ class PrimaiteGame: 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. + game.setup_reward_sharing() + # Set the NMNE capture config set_nmne_config(network_config.get("nmne_config", {})) return game + + def setup_reward_sharing(self): + """Do necessary setup to enable reward sharing between agents. + + This method ensures that there are no cycles in the reward sharing. A cycle would be for example if agent_1 + depends on agent_2 and agent_2 depends on agent_1. It would cause an infinite loop. + + Also, SharedReward requires us to pass it a callback method that will provide the reward of the agent who is + sharing their reward. This callback is provided by this setup method. + + Finally, this method sorts the agents in order in which rewards will be evaluated to make sure that any rewards + that rely on the value of another reward are evaluated later. + + :raises RuntimeError: If the reward sharing is specified with a cyclic dependency. + """ + # construct dependency graph in the reward sharing between agents. + graph = {} + for name, agent in self.agents.items(): + graph[name] = set() + for comp, weight in agent.reward_function.reward_components: + if isinstance(comp, SharedReward): + comp: SharedReward + graph[name].add(comp.agent_name) + + # while constructing the graph, we might as well set up the reward sharing itself. + comp.callback = lambda: self.agents[comp.agent_name].reward_function.current_reward + # TODO: make sure this lambda is working like I think it does -> it goes to the agent and fetches + # the most recent value of current_reward, NOT just simply caching the reward value at the time this + # callback method is defined. + + # make sure the graph is acyclic. Otherwise we will enter an infinite loop of reward sharing. + if graph_has_cycle(graph): + raise RuntimeError( + ( + "Detected cycle in agent reward sharing. Check the agent reward function ", + "configuration: reward sharing can only go one way.", + ) + ) + + # sort the agents so the rewards that depend on other rewards are always evaluated later + self._reward_calculation_order = topological_sort(graph) diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py index 19a86237..801ef269 100644 --- a/src/primaite/game/science.py +++ b/src/primaite/game/science.py @@ -1,4 +1,5 @@ from random import random +from typing import Any, Iterable, Mapping def simulate_trial(p_of_success: float) -> bool: @@ -14,3 +15,81 @@ def simulate_trial(p_of_success: float) -> bool: :returns: True if the trial is successful (with probability 'p_of_success'); otherwise, False. """ return random() < p_of_success + + +def graph_has_cycle(graph: Mapping[Any, Iterable[Any]]) -> bool: + """Detect cycles in a directed graph. + + Provide the graph as a dictionary that describes which nodes are linked. For example: + {0: {1,2}, 1:{2,3}, 3:{0}} here there's a cycle 0 -> 1 -> 3 -> 0 + {'a': ('b','c'), c:('b')} here there is no cycle + + :param graph: a mapping from node to a set of nodes to which it is connected. + :type graph: Mapping[Any, Iterable[Any]] + :return: Whether the graph has any cycles + :rtype: bool + """ + visited = set() + currently_visiting = set() + + def depth_first_search(node: Any) -> bool: + """Perform depth-first search (DFS) traversal to detect cycles starting from a given node.""" + if node in currently_visiting: + return True # Cycle detected + if node in visited: + return False # Already visited, no need to explore further + + visited.add(node) + currently_visiting.add(node) + + for neighbour in graph.get(node, []): + if depth_first_search(neighbour): + return True # Cycle detected + + currently_visiting.remove(node) + return False + + # Start DFS traversal from each node + for node in graph: + if depth_first_search(node): + return True # Cycle detected + + return False # No cycles found + + +def topological_sort(graph: Mapping[Any, Iterable[Any]]) -> Iterable[Any]: + """ + Perform topological sorting on a directed graph. + + This guarantees that if there's a directed edge from node A to node B, then A appears before B. + + :param graph: A dictionary representing the directed graph, where keys are node identifiers + and values are lists of outgoing edges from each node. + :type graph: dict[int, list[Any]] + + :return: A topologically sorted list of node identifiers. + :rtype: list[Any] + """ + visited: set[Any] = set() + stack: list[Any] = [] + + def dfs(node: Any) -> None: + """ + Depth-first search traversal to visit nodes and their neighbors. + + :param node: The current node to visit. + :type node: Any + """ + if node in visited: + return + visited.add(node) + for neighbour in graph.get(node, []): + dfs(neighbour) + stack.append(node) + + # Perform DFS traversal from each node + for node in graph: + dfs(node) + + # Reverse the stack and return it. + return stack[::-1] diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index 87289c43..ef77c63d 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -77,16 +77,17 @@ class PrimaiteIO: :type episode: int """ data = {} - longest_history = max([len(hist) for hist in agent_actions]) + longest_history = max([len(hist) for hist in agent_actions.values()]) for i in range(longest_history): - data[i] = {"timestep": i, "episode": episode, **{name: acts[i] for name, acts in agent_actions.items()}} + data[i] = {"timestep": i, "episode": episode} + data[i].update({name: acts[i] for name, acts in agent_actions.items() if len(acts) > i}) path = self.generate_agent_actions_save_path(episode=episode) path.parent.mkdir(exist_ok=True, parents=True) path.touch() _LOGGER.info(f"Saving agent action log to {path}") with open(path, "w") as file: - json.dump(data, fp=file, indent=1) + json.dump(data, fp=file, indent=1, default=lambda x: x.model_dump()) @classmethod def from_config(cls, config: Dict) -> "PrimaiteIO": From 03ee976a2d66d40e35a20940c41f1aae88c5a9e2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 12 Mar 2024 11:00:55 +0000 Subject: [PATCH 105/128] remove extra print statement --- src/primaite/game/agent/rewards.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 86a61535..3d61c0b4 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -353,7 +353,6 @@ class SharedReward(AbstractReward): def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Simply access the other agent's reward and return it.""" - print(self.callback(), self.agent_name) return self.callback() @classmethod From 24fdb8dc17f2a7608e7876a58afa054993e30851 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 12 Mar 2024 11:40:26 +0000 Subject: [PATCH 106/128] Fix minor reward sharing bugs --- src/primaite/game/agent/rewards.py | 8 ++--- src/primaite/game/game.py | 5 +-- src/primaite/game/science.py | 3 +- .../Data-Manipulation-E2E-Demonstration.ipynb | 32 +++++++++++-------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 3d61c0b4..a2ffd875 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -230,7 +230,7 @@ class WebpageUnavailablePenalty(AbstractReward): component will keep track of that information. In that case, it doesn't matter whether the last webpage had a 200 status code, because there has been an unsuccessful request since. """ - if last_action_response.request == ["network", "node", self._node, "application", "DatabaseClient", "execute"]: + if last_action_response.request == ["network", "node", self._node, "application", "WebBrowser", "execute"]: self._last_request_failed = last_action_response.response.status != "success" # if agent couldn't even get as far as sending the request (because for example the node was off), then @@ -338,7 +338,7 @@ class SharedReward(AbstractReward): self.agent_name = agent_name """Agent whose reward to track.""" - def default_callback() -> Never: + def default_callback(agent_name: str) -> Never: """ Default callback to prevent calling this reward until it's properly initialised. @@ -348,12 +348,12 @@ class SharedReward(AbstractReward): """ raise RuntimeError("Attempted to calculate SharedReward but it was not initialised properly.") - self.callback: Callable[[], float] = default_callback + self.callback: Callable[[str], float] = default_callback """Method that retrieves an agent's current reward given the agent's name.""" def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Simply access the other agent's reward and return it.""" - return self.callback() + return self.callback(self.agent_name) @classmethod def from_config(cls, config: Dict) -> "SharedReward": diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index e766bcd3..ac23610c 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -480,10 +480,7 @@ class PrimaiteGame: graph[name].add(comp.agent_name) # while constructing the graph, we might as well set up the reward sharing itself. - comp.callback = lambda: self.agents[comp.agent_name].reward_function.current_reward - # TODO: make sure this lambda is working like I think it does -> it goes to the agent and fetches - # the most recent value of current_reward, NOT just simply caching the reward value at the time this - # callback method is defined. + comp.callback = lambda agent_name: self.agents[agent_name].reward_function.current_reward # make sure the graph is acyclic. Otherwise we will enter an infinite loop of reward sharing. if graph_has_cycle(graph): diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py index 801ef269..908b326f 100644 --- a/src/primaite/game/science.py +++ b/src/primaite/game/science.py @@ -91,5 +91,4 @@ def topological_sort(graph: Mapping[Any, Iterable[Any]]) -> Iterable[Any]: for node in graph: dfs(node) - # Reverse the stack and return it. - return stack[::-1] + return stack diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index b2522c2b..946202b6 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -450,7 +450,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now the reward is -1, let's have a look at blue agent's observation." + "Now the reward is -0.8, let's have a look at blue agent's observation." ] }, { @@ -510,9 +510,9 @@ "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", - "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker']['action']}\" )\n", - "print(f\"Green action: {info['agent_actions']['client_1_green_user']['action']}\" )\n", - "print(f\"Green action: {info['agent_actions']['client_2_green_user']['action']}\" )\n", + "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'].action}\" )\n", + "print(f\"Green action: {info['agent_actions']['client_1_green_user'].action}\" )\n", + "print(f\"Green action: {info['agent_actions']['client_2_green_user'].action}\" )\n", "print(f\"Blue reward:{reward}\" )" ] }, @@ -533,9 +533,9 @@ "metadata": {}, "outputs": [], "source": [ - "obs, reward, terminated, truncated, info = env.step(0) # patch the database\n", + "obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", "print(f\"step: {env.game.step_counter}\")\n", - "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker']['action']}\" )\n", + "print(f\"Red action: {info['agent_actions']['data_manipulation_attacker'].action}\" )\n", "print(f\"Green action: {info['agent_actions']['client_2_green_user']}\" )\n", "print(f\"Green action: {info['agent_actions']['client_1_green_user']}\" )\n", "print(f\"Blue reward:{reward:.2f}\" )" @@ -557,17 +557,19 @@ "outputs": [], "source": [ "env.step(13) # Patch the database\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", "\n", "env.step(50) # Block client 1\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", "\n", "env.step(51) # Block client 2\n", - "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )\n", + "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", "\n", - "for step in range(30):\n", + "while abs(reward - 0.8) > 1e-5:\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", + " if env.game.step_counter > 10000:\n", + " break # make sure there's no infinite loop if something went wrong" ] }, { @@ -617,17 +619,19 @@ " if obs['NODES'][6]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", " # client 1 has NMNEs, let's block it\n", " obs, reward, terminated, truncated, info = env.step(50) # block client 1\n", + " print(\"blocking client 1\")\n", " break\n", " elif obs['NODES'][7]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", " # client 2 has NMNEs, so let's block it\n", " obs, reward, terminated, truncated, info = env.step(51) # block client 2\n", + " print(\"blocking client 2\")\n", " break\n", " if tries>100:\n", " print(\"Error: NMNE never increased\")\n", " break\n", "\n", "env.step(13) # Patch the database\n", - "..." + "print()\n" ] }, { @@ -646,14 +650,14 @@ "\n", "for step in range(40):\n", " obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", - " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker']['action']}, Blue reward:{reward:.2f}\" )" + " print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Reset the environment, you can rerun the other cells to verify that the attack works the same every episode." + "Reset the environment, you can rerun the other cells to verify that the attack works the same every episode. (except the red agent will move between `client_1` and `client_2`.)" ] }, { From 045f46740702918a711003f2b9859988def28dbd Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Tue, 12 Mar 2024 11:51:17 +0000 Subject: [PATCH 107/128] Update marl config --- .../_package_data/data_manipulation_marl.yaml | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index f7288cb0..be53d2c5 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -75,7 +75,14 @@ agents: reward_function: reward_components: - - type: DUMMY + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 - ref: client_1_green_user team: GREEN @@ -118,7 +125,14 @@ agents: reward_function: reward_components: - - type: DUMMY + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_1 @@ -700,22 +714,14 @@ agents: node_hostname: database_server folder_name: database file_name: database.db - - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.25 + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: client_1 - - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.25 + agent_name: client_1_green_user + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.05 - options: - node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.05 - options: - node_hostname: client_2 + agent_name: client_2_green_user agent_settings: @@ -1259,22 +1265,14 @@ agents: node_hostname: database_server folder_name: database file_name: database.db - - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.25 + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: client_1 - - type: WEBPAGE_UNAVAILABLE_PENALTY - weight: 0.25 + agent_name: client_1_green_user + - type: SHARED_REWARD + weight: 1.0 options: - node_hostname: client_2 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.05 - options: - node_hostname: client_1 - - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY - weight: 0.05 - options: - node_hostname: client_2 + agent_name: client_2_green_user agent_settings: From f2c6f10c21f445cf5d85db808ad4092ffa923993 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Tue, 12 Mar 2024 12:20:02 +0000 Subject: [PATCH 108/128] #2350: apply PR suggestions --- .../game/agent/observations/nic_observations.py | 10 +++++----- .../game/agent/observations/node_observations.py | 6 +++--- .../simulator/system/applications/database_client.py | 3 +-- .../red_applications/data_manipulation_bot.py | 3 ++- .../game_layer/observations/test_nic_observations.py | 10 +++++----- .../game_layer/observations/test_node_observations.py | 2 +- tests/integration_tests/network/test_capture_nmne.py | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/primaite/game/agent/observations/nic_observations.py b/src/primaite/game/agent/observations/nic_observations.py index 735b41d4..de83e03a 100644 --- a/src/primaite/game/agent/observations/nic_observations.py +++ b/src/primaite/game/agent/observations/nic_observations.py @@ -27,7 +27,7 @@ class NicObservation(AbstractObservation): """The default NIC observation dict.""" data = {"nic_status": 0} if CAPTURE_NMNE: - data.update({"nmne": {"inbound": 0, "outbound": 0}}) + data.update({"NMNE": {"inbound": 0, "outbound": 0}}) return data @@ -133,14 +133,14 @@ class NicObservation(AbstractObservation): else: obs_dict = {"nic_status": 1 if nic_state["enabled"] else 2} if CAPTURE_NMNE: - obs_dict.update({"nmne": {}}) + obs_dict.update({"NMNE": {}}) direction_dict = nic_state["nmne"].get("direction", {}) inbound_keywords = direction_dict.get("inbound", {}).get("keywords", {}) inbound_count = inbound_keywords.get("*", 0) outbound_keywords = direction_dict.get("outbound", {}).get("keywords", {}) outbound_count = outbound_keywords.get("*", 0) - obs_dict["nmne"]["inbound"] = self._categorise_mne_count(inbound_count - self.nmne_inbound_last_step) - obs_dict["nmne"]["outbound"] = self._categorise_mne_count(outbound_count - self.nmne_outbound_last_step) + obs_dict["NMNE"]["inbound"] = self._categorise_mne_count(inbound_count - self.nmne_inbound_last_step) + obs_dict["NMNE"]["outbound"] = self._categorise_mne_count(outbound_count - self.nmne_outbound_last_step) self.nmne_inbound_last_step = inbound_count self.nmne_outbound_last_step = outbound_count return obs_dict @@ -151,7 +151,7 @@ class NicObservation(AbstractObservation): space = spaces.Dict({"nic_status": spaces.Discrete(3)}) if CAPTURE_NMNE: - space["nmne"] = spaces.Dict({"inbound": spaces.Discrete(4), "outbound": spaces.Discrete(4)}) + space["NMNE"] = spaces.Dict({"inbound": spaces.Discrete(4), "outbound": spaces.Discrete(4)}) return space diff --git a/src/primaite/game/agent/observations/node_observations.py b/src/primaite/game/agent/observations/node_observations.py index f211a6b5..94f0974b 100644 --- a/src/primaite/game/agent/observations/node_observations.py +++ b/src/primaite/game/agent/observations/node_observations.py @@ -86,7 +86,7 @@ class NodeObservation(AbstractObservation): self.default_observation: Dict = { "SERVICES": {i + 1: s.default_observation for i, s in enumerate(self.services)}, "FOLDERS": {i + 1: f.default_observation for i, f in enumerate(self.folders)}, - "NETWORK_INTERFACES": {i + 1: n.default_observation for i, n in enumerate(self.network_interfaces)}, + "NICS": {i + 1: n.default_observation for i, n in enumerate(self.network_interfaces)}, "operating_status": 0, } if self.logon_status: @@ -111,7 +111,7 @@ class NodeObservation(AbstractObservation): obs["SERVICES"] = {i + 1: service.observe(state) for i, service in enumerate(self.services)} obs["FOLDERS"] = {i + 1: folder.observe(state) for i, folder in enumerate(self.folders)} obs["operating_status"] = node_state["operating_state"] - obs["NETWORK_INTERFACES"] = { + obs["NICS"] = { i + 1: network_interface.observe(state) for i, network_interface in enumerate(self.network_interfaces) } @@ -127,7 +127,7 @@ class NodeObservation(AbstractObservation): "SERVICES": spaces.Dict({i + 1: service.space for i, service in enumerate(self.services)}), "FOLDERS": spaces.Dict({i + 1: folder.space for i, folder in enumerate(self.folders)}), "operating_status": spaces.Discrete(5), - "NETWORK_INTERFACES": spaces.Dict( + "NICS": spaces.Dict( {i + 1: network_interface.space for i, network_interface in enumerate(self.network_interfaces)} ), } diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index bc51b3a2..d3afef59 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -48,6 +48,7 @@ class DatabaseClient(Application): def execute(self) -> bool: """Execution definition for db client: perform a select query.""" + self.num_executions += 1 # trying to connect counts as an execution if self.connections: can_connect = self.check_connection(connection_id=list(self.connections.keys())[-1]) else: @@ -82,8 +83,6 @@ class DatabaseClient(Application): if not self._can_perform_action(): return False - self.num_executions += 1 # trying to connect counts as an execution - if not connection_id: connection_id = str(uuid4()) 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 2a6c2b11..ee276971 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 @@ -193,6 +193,8 @@ class DataManipulationBot(Application): if not self._can_perform_action(): _LOGGER.debug("Data manipulation application attempted to execute but it cannot perform actions right now.") self.run() + + self.num_executions += 1 return self._application_loop() def _application_loop(self) -> bool: @@ -202,7 +204,6 @@ class DataManipulationBot(Application): This is the core loop where the bot sequentially goes through the stages of the attack. """ if not self._can_perform_action(): - self.num_executions += 1 return False if self.server_ip_address and self.payload: self.sys_log.info(f"{self.name}: Running") 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 c210b751..332bc1f7 100644 --- a/tests/integration_tests/game_layer/observations/test_nic_observations.py +++ b/tests/integration_tests/game_layer/observations/test_nic_observations.py @@ -43,14 +43,14 @@ def test_nic(simulation): nic_obs = NicObservation(where=["network", "nodes", pc.hostname, "NICs", 1]) assert nic_obs.space["nic_status"] == spaces.Discrete(3) - assert nic_obs.space["nmne"]["inbound"] == spaces.Discrete(4) - assert nic_obs.space["nmne"]["outbound"] == spaces.Discrete(4) + assert nic_obs.space["NMNE"]["inbound"] == spaces.Discrete(4) + assert nic_obs.space["NMNE"]["outbound"] == spaces.Discrete(4) observation_state = nic_obs.observe(simulation.describe_state()) assert observation_state.get("nic_status") == 1 # enabled - assert observation_state.get("nmne") is not None - assert observation_state["nmne"].get("inbound") == 0 - assert observation_state["nmne"].get("outbound") == 0 + assert observation_state.get("NMNE") is not None + assert observation_state["NMNE"].get("inbound") == 0 + assert observation_state["NMNE"].get("outbound") == 0 nic.disable() observation_state = nic_obs.observe(simulation.describe_state()) 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 b1563fbd..dce05b6a 100644 --- a/tests/integration_tests/game_layer/observations/test_node_observations.py +++ b/tests/integration_tests/game_layer/observations/test_node_observations.py @@ -32,7 +32,7 @@ def test_node_observation(simulation): assert observation_state.get("SERVICES") is not None assert observation_state.get("FOLDERS") is not None - assert observation_state.get("NETWORK_INTERFACES") is not None + assert observation_state.get("NICS") is not None # turn off computer pc.power_off() diff --git a/tests/integration_tests/network/test_capture_nmne.py b/tests/integration_tests/network/test_capture_nmne.py index 85fcf102..9efc70f7 100644 --- a/tests/integration_tests/network/test_capture_nmne.py +++ b/tests/integration_tests/network/test_capture_nmne.py @@ -179,8 +179,8 @@ def test_capture_nmne_observations(uc2_network): # Observe the current state of NMNEs from the NICs of both the database and web servers state = sim.describe_state() - db_nic_obs = db_server_nic_obs.observe(state)["nmne"] - web_nic_obs = web_server_nic_obs.observe(state)["nmne"] + db_nic_obs = db_server_nic_obs.observe(state)["NMNE"] + web_nic_obs = web_server_nic_obs.observe(state)["NMNE"] # Define expected NMNE values based on the iteration count if i > 10: From 6dedb910990df2b289f31162b43e678d12cf0d12 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 13 Mar 2024 09:17:29 +0000 Subject: [PATCH 109/128] Remove redundant TODOs --- src/primaite/game/agent/interface.py | 2 -- src/primaite/game/agent/rewards.py | 24 ++++++------------------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 0531b25f..91fa03d4 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -141,8 +141,6 @@ class AbstractAgent(ABC): :param obs: Observation of the environment. :type obs: ObsType - :param reward: Reward from the previous action, defaults to None TODO: should this parameter even be accepted? - :type reward: float, optional :param timestep: The current timestep in the simulation, used for non-RL agents. Optional :type timestep: int :return: Action to be taken in the environment. diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index a2ffd875..d8cb1328 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -43,9 +43,7 @@ class AbstractReward: """Base class for reward function components.""" @abstractmethod - def calculate( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Calculate the reward for the current state.""" return 0.0 @@ -65,9 +63,7 @@ class AbstractReward: class DummyReward(AbstractReward): """Dummy reward function component which always returns 0.""" - def calculate( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Calculate the reward for the current state.""" return 0.0 @@ -107,9 +103,7 @@ class DatabaseFileIntegrity(AbstractReward): file_name, ] - def calculate( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Calculate the reward for the current state. :param state: The current state of the simulation. @@ -164,9 +158,7 @@ class WebServer404Penalty(AbstractReward): """ self.location_in_state = ["network", "nodes", node_hostname, "services", service_name] - def calculate( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Calculate the reward for the current state. :param state: The current state of the simulation. @@ -220,9 +212,7 @@ class WebpageUnavailablePenalty(AbstractReward): self.location_in_state: List[str] = ["network", "nodes", node_hostname, "applications", "WebBrowser"] self._last_request_failed: bool = False - def calculate( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def calculate(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """ Calculate the reward based on current simulation state, and the recent agent action. @@ -397,9 +387,7 @@ class RewardFunction: """ self.reward_components.append((component, weight)) - def update( - self, state: Dict, last_action_response: "AgentActionHistoryItem" - ) -> float: # todo maybe make last_action_response optional? + def update(self, state: Dict, last_action_response: "AgentActionHistoryItem") -> float: """Calculate the overall reward for the current state. :param state: The current state of the simulation. From 10ee9b300fe8e6596c25fbad6ebd689ac1bc4aaf Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 13 Mar 2024 12:08:20 +0000 Subject: [PATCH 110/128] Update docs on rewards --- docs/source/game_layer.rst | 72 ++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 39ab7bde..ba400ac2 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -6,19 +6,12 @@ The Primaite codebase consists of two main modules: * ``simulator``: The simulation logic including the network topology, the network state, and behaviour of various hardware and software classes. * ``game``: The agent-training infrastructure which helps reinforcement learning agents interface with the simulation. This includes the observation, action, and rewards, for RL agents, but also scripted deterministic agents. The game layer orchestrates all the interactions between modules. - The simulator and game layer communicate using the PrimAITE State API and the PrimAITE Request API. - -.. - TODO: write up these APIs and link them here. - - -Game layer ----------- +The simulator and game layer communicate using the PrimAITE State API and the PrimAITE Request API. The game layer is responsible for managing agents and getting them to interface with the simulator correctly. It consists of several components: PrimAITE Session -^^^^^^^^^^^^^^^^ +================ .. admonition:: Deprecated :class: deprecated @@ -28,7 +21,7 @@ PrimAITE Session ``PrimaiteSession`` is the main entry point into Primaite and it allows the simultaneous coordination of a simulation and agents that interact with it. ``PrimaiteSession`` keeps track of multiple agents of different types. Agents -^^^^^^ +====== All agents inherit from the :py:class:`primaite.game.agent.interface.AbstractAgent` class, which mandates that they have an ObservationManager, ActionManager, and RewardManager. The agent behaviour depends on the type of agent, but there are two main types: @@ -39,16 +32,67 @@ All agents inherit from the :py:class:`primaite.game.agent.interface.AbstractAge TODO: add seed to stochastic scripted agents Observations -^^^^^^^^^^^^^^^^^^ +============ An agent's observations are managed by the ``ObservationManager`` class. It generates observations based on the current simulation state dictionary. It also provides the observation space during initial setup. The data is formatted so it's compatible with ``Gymnasium.spaces``. Observation spaces are composed of one or more components which are defined by the ``AbstractObservation`` base class. Actions -^^^^^^^ +======= An agent's actions are managed by the ``ActionManager``. It converts actions selected by agents (which are typically integers chosen from a ``gymnasium.spaces.Discrete`` space) into simulation-friendly requests. It also provides the action space during initial setup. Action spaces are composed of one or more components which are defined by the ``AbstractAction`` base class. Rewards -^^^^^^^ +======= -An agent's reward function is managed by the ``RewardManager``. It calculates rewards based on the simulation state (in a way similar to observations). Rewards can be defined as a weighted sum of small reward components. For example, an agents reward can be based on the uptime of a database service plus the loss rate of packets between clients and a web server. The reward components are defined by the AbstractReward base class. +An agent's reward function is managed by the ``RewardManager``. It calculates rewards based on the simulation state (in a way similar to observations). Rewards can be defined as a weighted sum of small reward components. For example, an agents reward can be based on the uptime of a database service plus the loss rate of packets between clients and a web server. + +Reward Components +----------------- + +Currently implemented are reward components tailored to the data manipulation scenario. View the full API and description of how they work here: :py:module:`primaite.game.agent.reward`. + +Reward Sharing +-------------- + +An agent's reward can be based on rewards of other agents. This is particularly useful for modelling a situation where the blue agent's job is to protect the ability of green agents to perform their pattern-of-life. This can be configured in the YAML file this way: + +```yaml +green_agent_1: # this agent sometimes tries to access the webpage, and sometimes the database + # actions, observations, and agent settings go here + reward_function: + reward_components: + + # When the webpage loads, the reward goes up by 0.25 when it fails to load, it goes down to -0.25 + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + + # When the database is reachable, the reward goes up by 0.05, when it is unreachable it goes down to -0.05 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 + +blue_agent: + # actions, observations, and agent settings go here + reward_function: + reward_components: + + # When the database file is in a good state, blue's reward is 0.4, when it's in a corrupted state the reward is -0.4 + - type: DATABASE_FILE_INTEGRITY + weight: 0.40 + options: + node_hostname: database_server + folder_name: database + file_name: database.db + + # The green's reward is added onto the blue's reward. + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_2_green_user + +``` + +When defining agent reward sharing, users must be careful to avoid circular references, as that would lead to an infinite calculation loop. PrimAITE will prevent circular dependencies and provide a helpful error message if they are detected in the yaml. From f438acf745c54137b08b41175b05d91e517d7db4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 13 Mar 2024 14:01:17 +0000 Subject: [PATCH 111/128] Add shared reward test --- tests/assets/configs/shared_rewards.yaml | 956 ++++++++++++++++++ .../game_layer/test_rewards.py | 27 + 2 files changed, 983 insertions(+) create mode 100644 tests/assets/configs/shared_rewards.yaml diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml new file mode 100644 index 00000000..91ff20e7 --- /dev/null +++ b/tests/assets/configs/shared_rewards.yaml @@ -0,0 +1,956 @@ +training_config: + rl_framework: SB3 + rl_algorithm: PPO + seed: 333 + n_learn_episodes: 1 + n_eval_episodes: 5 + max_steps_per_episode: 128 + deterministic_eval: false + n_agents: 1 + agent_references: + - defender + +io_settings: + save_checkpoints: true + checkpoint_interval: 5 + save_agent_actions: true + save_step_metadata: false + save_pcap_logs: false + save_sys_logs: true + + +game: + max_episode_length: 256 + ports: + - HTTP + - POSTGRES_SERVER + protocols: + - ICMP + - TCP + - UDP + thresholds: + nmne: + high: 10 + medium: 5 + low: 0 + +agents: + - ref: client_2_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_2 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_2 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_2 + + - ref: client_1_green_user + team: GREEN + type: ProbabilisticAgent + agent_settings: + action_probabilities: + 0: 0.3 + 1: 0.6 + 2: 0.1 + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: WebBrowser + - application_name: DatabaseClient + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_applications_per_node: 2 + action_map: + 0: + action: DONOTHING + options: {} + 1: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 0 + 2: + action: NODE_APPLICATION_EXECUTE + options: + node_id: 0 + application_id: 1 + + reward_function: + reward_components: + - type: WEBPAGE_UNAVAILABLE_PENALTY + weight: 0.25 + options: + node_hostname: client_1 + - type: GREEN_ADMIN_DATABASE_UNREACHABLE_PENALTY + weight: 0.05 + options: + node_hostname: client_1 + + + + + + - ref: data_manipulation_attacker + team: RED + type: RedDatabaseCorruptingAgent + + observation_space: + type: UC2RedObservation + options: + nodes: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_name: client_1 + applications: + - application_name: DataManipulationBot + - node_name: client_2 + applications: + - application_name: DataManipulationBot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + + - ref: defender + team: BLUE + type: ProxyAgent + + observation_space: + type: UC2BlueObservation + options: + num_services_per_node: 1 + num_folders_per_node: 1 + num_files_per_folder: 1 + num_nics_per_node: 2 + nodes: + - node_hostname: domain_controller + services: + - service_name: DNSServer + - node_hostname: web_server + services: + - service_name: WebServer + - node_hostname: database_server + folders: + - folder_name: database + files: + - file_name: database.db + - node_hostname: backup_server + - node_hostname: security_suite + - node_hostname: client_1 + - node_hostname: client_2 + links: + - link_ref: router_1___switch_1 + - link_ref: router_1___switch_2 + - link_ref: switch_1___domain_controller + - link_ref: switch_1___web_server + - link_ref: switch_1___database_server + - link_ref: switch_1___backup_server + - link_ref: switch_1___security_suite + - link_ref: switch_2___client_1 + - link_ref: switch_2___client_2 + - link_ref: switch_2___security_suite + acl: + options: + max_acl_rules: 10 + router_hostname: router_1 + ip_address_order: + - node_hostname: domain_controller + nic_num: 1 + - node_hostname: web_server + nic_num: 1 + - node_hostname: database_server + nic_num: 1 + - node_hostname: backup_server + nic_num: 1 + - node_hostname: security_suite + nic_num: 1 + - node_hostname: client_1 + nic_num: 1 + - node_hostname: client_2 + nic_num: 1 + - node_hostname: security_suite + nic_num: 2 + ics: null + + 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_PATCH + - 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: NETWORK_ACL_ADDRULE + options: + target_router_hostname: router_1 + - type: NETWORK_ACL_REMOVERULE + options: + target_router_hostname: router_1 + - type: NETWORK_NIC_ENABLE + - type: NETWORK_NIC_DISABLE + + action_map: + 0: + action: DONOTHING + options: {} + # scan webapp service + 1: + action: NODE_SERVICE_SCAN + options: + node_id: 1 + service_id: 0 + # stop webapp service + 2: + action: NODE_SERVICE_STOP + options: + node_id: 1 + service_id: 0 + # start webapp service + 3: + action: "NODE_SERVICE_START" + options: + node_id: 1 + service_id: 0 + 4: + action: "NODE_SERVICE_PAUSE" + options: + node_id: 1 + service_id: 0 + 5: + action: "NODE_SERVICE_RESUME" + options: + node_id: 1 + service_id: 0 + 6: + action: "NODE_SERVICE_RESTART" + options: + node_id: 1 + service_id: 0 + 7: + action: "NODE_SERVICE_DISABLE" + options: + node_id: 1 + service_id: 0 + 8: + action: "NODE_SERVICE_ENABLE" + options: + node_id: 1 + service_id: 0 + 9: # check database.db file + action: "NODE_FILE_SCAN" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 10: + action: "NODE_FILE_CHECKHASH" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 11: + action: "NODE_FILE_DELETE" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 12: + action: "NODE_FILE_REPAIR" + options: + node_id: 2 + folder_id: 0 + file_id: 0 + 13: + action: "NODE_SERVICE_PATCH" + options: + node_id: 2 + service_id: 0 + 14: + action: "NODE_FOLDER_SCAN" + options: + node_id: 2 + folder_id: 0 + 15: + action: "NODE_FOLDER_CHECKHASH" + options: + node_id: 2 + folder_id: 0 + 16: + action: "NODE_FOLDER_REPAIR" + options: + node_id: 2 + folder_id: 0 + 17: + action: "NODE_FOLDER_RESTORE" + options: + node_id: 2 + folder_id: 0 + 18: + action: "NODE_OS_SCAN" + options: + node_id: 0 + 19: + action: "NODE_SHUTDOWN" + options: + node_id: 0 + 20: + action: NODE_STARTUP + options: + node_id: 0 + 21: + action: NODE_RESET + options: + node_id: 0 + 22: + action: "NODE_OS_SCAN" + options: + node_id: 1 + 23: + action: "NODE_SHUTDOWN" + options: + node_id: 1 + 24: + action: NODE_STARTUP + options: + node_id: 1 + 25: + action: NODE_RESET + options: + node_id: 1 + 26: # old action num: 18 + action: "NODE_OS_SCAN" + options: + node_id: 2 + 27: + action: "NODE_SHUTDOWN" + options: + node_id: 2 + 28: + action: NODE_STARTUP + options: + node_id: 2 + 29: + action: NODE_RESET + options: + node_id: 2 + 30: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 31: + action: "NODE_SHUTDOWN" + options: + node_id: 3 + 32: + action: NODE_STARTUP + options: + node_id: 3 + 33: + action: NODE_RESET + options: + node_id: 3 + 34: + action: "NODE_OS_SCAN" + options: + node_id: 4 + 35: + action: "NODE_SHUTDOWN" + options: + node_id: 4 + 36: + action: NODE_STARTUP + options: + node_id: 4 + 37: + action: NODE_RESET + options: + node_id: 4 + 38: + action: "NODE_OS_SCAN" + options: + node_id: 5 + 39: # old action num: 19 # shutdown client 1 + action: "NODE_SHUTDOWN" + options: + node_id: 5 + 40: # old action num: 20 + action: NODE_STARTUP + options: + node_id: 5 + 41: # old action num: 21 + action: NODE_RESET + options: + node_id: 5 + 42: + action: "NODE_OS_SCAN" + options: + node_id: 6 + 43: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 44: + action: NODE_STARTUP + options: + node_id: 6 + 45: + action: NODE_RESET + options: + node_id: 6 + + 46: # old action num: 22 # "ACL: ADDRULE - Block outgoing traffic from client 1" + action: "NETWORK_ACL_ADDRULE" + options: + 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 + 47: # old action num: 23 # "ACL: ADDRULE - Block outgoing traffic from client 2" + action: "NETWORK_ACL_ADDRULE" + options: + 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 + 48: # old action num: 24 # block tcp traffic from client 1 to web app + action: "NETWORK_ACL_ADDRULE" + options: + 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 + 49: # old action num: 25 # block tcp traffic from client 2 to web app + action: "NETWORK_ACL_ADDRULE" + options: + 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 + 50: # old action num: 26 + action: "NETWORK_ACL_ADDRULE" + options: + 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 + 51: # old action num: 27 + action: "NETWORK_ACL_ADDRULE" + options: + 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 + 52: # old action num: 28 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 0 + 53: # old action num: 29 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 1 + 54: # old action num: 30 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 2 + 55: # old action num: 31 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 3 + 56: # old action num: 32 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 4 + 57: # old action num: 33 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 5 + 58: # old action num: 34 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 6 + 59: # old action num: 35 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 7 + 60: # old action num: 36 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 8 + 61: # old action num: 37 + action: "NETWORK_ACL_REMOVERULE" + options: + position: 9 + 62: # old action num: 38 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 0 + nic_id: 0 + 63: # old action num: 39 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 0 + nic_id: 0 + 64: # old action num: 40 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 1 + nic_id: 0 + 65: # old action num: 41 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 1 + nic_id: 0 + 66: # old action num: 42 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 2 + nic_id: 0 + 67: # old action num: 43 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 2 + nic_id: 0 + 68: # old action num: 44 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 3 + nic_id: 0 + 69: # old action num: 45 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 3 + nic_id: 0 + 70: # old action num: 46 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 4 + nic_id: 0 + 71: # old action num: 47 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 4 + nic_id: 0 + 72: # old action num: 48 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 4 + nic_id: 1 + 73: # old action num: 49 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 4 + nic_id: 1 + 74: # old action num: 50 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 5 + nic_id: 0 + 75: # old action num: 51 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 5 + nic_id: 0 + 76: # old action num: 52 + action: "NETWORK_NIC_DISABLE" + options: + node_id: 6 + nic_id: 0 + 77: # old action num: 53 + action: "NETWORK_NIC_ENABLE" + options: + node_id: 6 + nic_id: 0 + + + + options: + nodes: + - node_name: domain_controller + - node_name: web_server + applications: + - application_name: DatabaseClient + services: + - service_name: WebServer + - node_name: database_server + folders: + - folder_name: database + files: + - file_name: database.db + services: + - service_name: DatabaseService + - node_name: backup_server + - node_name: security_suite + - node_name: client_1 + - node_name: client_2 + + max_folders_per_node: 2 + max_files_per_folder: 2 + max_services_per_node: 2 + max_nics_per_node: 8 + max_acl_rules: 10 + ip_address_order: + - node_name: domain_controller + nic_num: 1 + - node_name: web_server + nic_num: 1 + - node_name: database_server + nic_num: 1 + - node_name: backup_server + nic_num: 1 + - node_name: security_suite + nic_num: 1 + - node_name: client_1 + nic_num: 1 + - node_name: client_2 + nic_num: 1 + - node_name: security_suite + nic_num: 2 + + + reward_function: + reward_components: + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_1_green_user + - type: SHARED_REWARD + weight: 1.0 + options: + agent_name: client_2_green_user + + + + agent_settings: + flatten_obs: true + + + + + +simulation: + network: + nmne_config: + capture_nmne: true + nmne_capture_keywords: + - DELETE + nodes: + + - ref: router_1 + hostname: router_1 + type: router + num_ports: 5 + ports: + 1: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.10.1 + subnet_mask: 255.255.255.0 + acl: + 18: + action: PERMIT + src_port: POSTGRES_SERVER + dst_port: POSTGRES_SERVER + 19: + action: PERMIT + src_port: DNS + dst_port: DNS + 20: + action: PERMIT + src_port: FTP + dst_port: FTP + 21: + action: PERMIT + src_port: HTTP + dst_port: HTTP + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 23: + action: PERMIT + protocol: ICMP + + - ref: switch_1 + hostname: switch_1 + type: switch + num_ports: 8 + + - ref: switch_2 + hostname: switch_2 + type: switch + num_ports: 8 + + - ref: domain_controller + hostname: domain_controller + type: server + ip_address: 192.168.1.10 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + services: + - ref: domain_controller_dns_server + type: DNSServer + options: + domain_mapping: + arcd.com: 192.168.1.12 # web server + + - ref: web_server + hostname: web_server + type: server + ip_address: 192.168.1.12 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - ref: web_server_web_service + type: WebServer + applications: + - ref: web_server_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + + + - ref: database_server + hostname: database_server + type: server + ip_address: 192.168.1.14 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - ref: database_service + type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - ref: database_ftp_client + type: FTPClient + + - ref: backup_server + hostname: backup_server + type: server + ip_address: 192.168.1.16 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - ref: backup_service + type: FTPServer + + - ref: security_suite + hostname: security_suite + type: server + ip_address: 192.168.1.110 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + network_interfaces: + 2: # unfortunately this number is currently meaningless, they're just added in order and take up the next available slot + ip_address: 192.168.10.110 + subnet_mask: 255.255.255.0 + + - ref: client_1 + hostname: client_1 + type: computer + ip_address: 192.168.10.21 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - ref: data_manipulation_bot + type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - ref: client_1_web_browser + type: WebBrowser + options: + target_url: http://arcd.com/users/ + - ref: client_1_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + services: + - ref: client_1_dns_client + type: DNSClient + + - ref: client_2 + hostname: client_2 + type: computer + ip_address: 192.168.10.22 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.10.1 + dns_server: 192.168.1.10 + applications: + - ref: client_2_web_browser + type: WebBrowser + options: + target_url: http://arcd.com/users/ + - ref: data_manipulation_bot + type: DataManipulationBot + options: + port_scan_p_of_success: 0.8 + data_manipulation_p_of_success: 0.8 + payload: "DELETE" + server_ip: 192.168.1.14 + - ref: client_2_database_client + type: DatabaseClient + options: + db_server_ip: 192.168.1.14 + services: + - ref: client_2_dns_client + type: DNSClient + + + + links: + - ref: router_1___switch_1 + endpoint_a_ref: router_1 + endpoint_a_port: 1 + endpoint_b_ref: switch_1 + endpoint_b_port: 8 + - ref: router_1___switch_2 + endpoint_a_ref: router_1 + endpoint_a_port: 2 + endpoint_b_ref: switch_2 + endpoint_b_port: 8 + - ref: switch_1___domain_controller + endpoint_a_ref: switch_1 + endpoint_a_port: 1 + endpoint_b_ref: domain_controller + endpoint_b_port: 1 + - ref: switch_1___web_server + endpoint_a_ref: switch_1 + endpoint_a_port: 2 + endpoint_b_ref: web_server + endpoint_b_port: 1 + - ref: switch_1___database_server + endpoint_a_ref: switch_1 + endpoint_a_port: 3 + endpoint_b_ref: database_server + endpoint_b_port: 1 + - ref: switch_1___backup_server + endpoint_a_ref: switch_1 + endpoint_a_port: 4 + endpoint_b_ref: backup_server + endpoint_b_port: 1 + - ref: switch_1___security_suite + endpoint_a_ref: switch_1 + endpoint_a_port: 7 + endpoint_b_ref: security_suite + endpoint_b_port: 1 + - ref: switch_2___client_1 + endpoint_a_ref: switch_2 + endpoint_a_port: 1 + endpoint_b_ref: client_1 + endpoint_b_port: 1 + - ref: switch_2___client_2 + endpoint_a_ref: switch_2 + endpoint_a_port: 2 + endpoint_b_ref: client_2 + endpoint_b_port: 1 + - ref: switch_2___security_suite + endpoint_a_ref: switch_2 + endpoint_a_port: 7 + endpoint_b_ref: security_suite + endpoint_b_port: 2 diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 8edbf0ac..56ba2b8f 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,10 +1,15 @@ +import yaml + from primaite.game.agent.rewards import GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty +from primaite.game.game import PrimaiteGame +from primaite.session.environment import PrimaiteGymEnv from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.database.database_service import DatabaseService +from tests import TEST_ASSETS_ROOT from tests.conftest import ControlledAgent @@ -80,3 +85,25 @@ def test_uc2_rewards(game_and_agent): state = game.get_sim_state() reward_value = comp.calculate(state) assert reward_value == -1.0 + + +def test_shared_reward(): + CFG_PATH = TEST_ASSETS_ROOT / "configs/shared_rewards.yaml" + with open(CFG_PATH, "r") as f: + cfg = yaml.safe_load(f) + + env = PrimaiteGymEnv(game_config=cfg) + + env.reset() + + order = env.game._reward_calculation_order + assert order.index("defender") > order.index("client_1_green_user") + assert order.index("defender") > order.index("client_2_green_user") + + for step in range(256): + act = env.action_space.sample() + env.step(act) + g1_reward = env.game.agents["client_1_green_user"].reward_function.current_reward + g2_reward = env.game.agents["client_2_green_user"].reward_function.current_reward + blue_reward = env.game.agents["defender"].reward_function.current_reward + assert blue_reward == g1_reward + g2_reward From d33c80d0d61153a7fb599fefa6d209d3b0e602fe Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 14 Mar 2024 14:33:04 +0000 Subject: [PATCH 112/128] Minor fixes --- src/primaite/game/game.py | 9 +++++++-- src/primaite/session/environment.py | 4 ++-- tests/conftest.py | 2 ++ .../game_layer/test_rewards.py | 17 ++++++++++++++--- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 84e5e7df..05b76679 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -139,8 +139,12 @@ class PrimaiteGame: """ _LOGGER.debug(f"Stepping. Step counter: {self.step_counter}") + if self.step_counter == 0: + state = self.get_sim_state() + for agent in self.agents.values(): + agent.update_observation(state=state) # Apply all actions to simulation as requests - action_data = self.apply_agent_actions() + self.apply_agent_actions() # Advance timestep self.advance_timestep() @@ -149,7 +153,7 @@ class PrimaiteGame: sim_state = self.get_sim_state() # Update agents' observations and rewards based on the current state, and the response from the last action - self.update_agents(state=sim_state, action_data=action_data) + self.update_agents(state=sim_state) def get_sim_state(self) -> Dict: """Get the current state of the simulation.""" @@ -458,6 +462,7 @@ class PrimaiteGame: # Set the NMNE capture config set_nmne_config(network_config.get("nmne_config", {})) + game.update_agents(game.get_sim_state()) return game diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 64534b04..1795f14b 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -189,8 +189,8 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: """Reset the environment.""" if self.io.settings.save_agent_actions: - self.io.write_agent_actions(episode=self.episode_counter) - self.io.clear_agent_actions() + all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()} + self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter) self.game: PrimaiteGame = PrimaiteGame.from_config(cfg=copy.deepcopy(self.game_config)) self.game.setup_for_episode(episode=self.episode_counter) self.episode_counter += 1 diff --git a/tests/conftest.py b/tests/conftest.py index 20600e73..3a9e2655 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -531,4 +531,6 @@ def game_and_agent(): game.agents["test_agent"] = test_agent + game.setup_reward_sharing() + return (game, test_agent) diff --git a/tests/integration_tests/game_layer/test_rewards.py b/tests/integration_tests/game_layer/test_rewards.py index 56ba2b8f..cfd013bc 100644 --- a/tests/integration_tests/game_layer/test_rewards.py +++ b/tests/integration_tests/game_layer/test_rewards.py @@ -1,5 +1,6 @@ import yaml +from primaite.game.agent.interface import AgentActionHistoryItem from primaite.game.agent.rewards import GreenAdminDatabaseUnreachablePenalty, WebpageUnavailablePenalty from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv @@ -66,13 +67,18 @@ def test_uc2_rewards(game_and_agent): comp = GreenAdminDatabaseUnreachablePenalty("client_1") - db_client.apply_request( + response = db_client.apply_request( [ "execute", ] ) state = game.get_sim_state() - reward_value = comp.calculate(state) + reward_value = comp.calculate( + state, + last_action_response=AgentActionHistoryItem( + timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response + ), + ) assert reward_value == 1.0 router.acl.remove_rule(position=2) @@ -83,7 +89,12 @@ def test_uc2_rewards(game_and_agent): ] ) state = game.get_sim_state() - reward_value = comp.calculate(state) + reward_value = comp.calculate( + state, + last_action_response=AgentActionHistoryItem( + timestep=0, action="NODE_APPLICATION_EXECUTE", parameters={}, request=["execute"], response=response + ), + ) assert reward_value == -1.0 From 88a3c42f2fe3f2a0d9e06d78cb49c7bc4f0d7885 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 14 Mar 2024 22:15:27 +0000 Subject: [PATCH 113/128] #2369: commiting work done so far --- docs/_static/notebooks/extensions.png | Bin 0 -> 70034 bytes docs/_static/notebooks/install_extensions.png | Bin 0 -> 197959 bytes docs/index.rst | 1 + docs/source/example_notebooks.rst | 79 ++++++++++++++++++ docs/source/getting_started.rst | 6 +- docs/source/primaite_session.rst | 4 +- 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 docs/_static/notebooks/extensions.png create mode 100644 docs/_static/notebooks/install_extensions.png create mode 100644 docs/source/example_notebooks.rst diff --git a/docs/_static/notebooks/extensions.png b/docs/_static/notebooks/extensions.png new file mode 100644 index 0000000000000000000000000000000000000000..8441802d34e1a826bec15cceae3decaca24b38e9 GIT binary patch literal 70034 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 literal 0 HcmV?d00001 diff --git a/docs/_static/notebooks/install_extensions.png b/docs/_static/notebooks/install_extensions.png new file mode 100644 index 0000000000000000000000000000000000000000..db026ce38f37c6c7d164bda34ab6c81f7c98a62e GIT binary patch literal 197959 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

HCJfdohYhVgPk??uWYLyayWcAb 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

vz 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 page is completed as a prerequisite. + +Running Jupyter Notebooks +------------------------- + +1. Navigate to the PrimAITE directory + +.. code-block:: bash + :caption: Unix + + cd ~/primaite/PRIMAITE_VERSION_TOKEN + +.. code-block:: powershell + :caption: Windows (Powershell) + + cd ~\primaite\PRIMAITE_VERSION_TOKEN + +2. Run jupyter notebook + +.. code-block:: bash + :caption: Unix + + jupyter notebook + +.. code-block:: powershell + :caption: Windows (Powershell) + + jupyter notebook + +3. Opening the jupyter webpage (optional) + +The default web browser may automatically open the webpage. However, if that is not the case, open a web browser and navigate to |jupyter_index|. + +.. |jupyter_index| raw:: html + + http://localhost:8888/tree + +4. Navigate to the list of notebooks + +The example notebooks are located in notebooks/example_notebooks or by navigating to |jupyter_index_notebooks| + +.. |jupyter_index_notebooks| raw:: html + + http://localhost:8888/tree/notebooks/example_notebooks + +Running Jupyter Notebooks via VSCode +------------------------------------ + +It is also possible to view the Jupyter notebooks within VSCode. + +Installing extensions +""""""""""""""""""""" + +VSCode may need some extensions to be installed if not already done. +To do this, press the "Select Kernel" button on the top right. + +This should open a dialog which has the option to install python and jupyter extensions. + +.. image:: ../../_static/notebooks/install_extensions.png + :width: 700 + :align: center + :alt: :: The top dialog option that appears will automatically install the extensions + +The following extensions should now be installed + +.. image:: ../../_static/notebooks/extensions.png + :width: 300 + :align: center + +VSCode will then ask for a Python environment version to use. PrimAITE is compatible with Python versions 3.8 - 3.10 diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index a800ee56..c1559168 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -38,12 +38,12 @@ Install PrimAITE .. code-block:: bash :caption: Unix - mkdir ~/primaite/3.0.0 + mkdir -p ~/primaite/3.0.0b6 .. code-block:: powershell :caption: Windows (Powershell) - mkdir ~\primaite\3.0.0 + mkdir ~\primaite\3.0.0b6 2. Navigate to the primaite directory and create a new python virtual environment (venv) @@ -51,7 +51,7 @@ Install PrimAITE .. code-block:: bash :caption: Unix - cd ~/primaite/3.0.0 + cd ~/primaite/3.0.0b6 python3 -m venv .venv .. code-block:: powershell diff --git a/docs/source/primaite_session.rst b/docs/source/primaite_session.rst index 87a3f03d..b02e015e 100644 --- a/docs/source/primaite_session.rst +++ b/docs/source/primaite_session.rst @@ -35,7 +35,7 @@ Outputs ------- Running a session creates a session output directory in your user data folder. The filepath looks like this: -``~/primaite/3.0.0/sessions/YYYY-MM-DD/HH-MM-SS/``. This folder contains the simulation sys logs generated by each node, +``~/primaite/3.0.0b6/sessions/YYYY-MM-DD/HH-MM-SS/``. This folder contains the simulation sys logs generated by each node, the saved agent checkpoints, and final model. The folder also contains a .json file for each episode step that contains the action, reward, and simulation state. These can be found in -``~/primaite/3.0.0/sessions/YYYY-MM-DD/HH-MM-SS/simulation_output/episode_/step_metadata/step_.json`` +``~/primaite/3.0.0b6/sessions/YYYY-MM-DD/HH-MM-SS/simulation_output/episode_/step_metadata/step_.json`` From 1d09f0791a0dc4c89a8faafa8f545e348cf641e3 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 14 Mar 2024 23:17:34 +0000 Subject: [PATCH 114/128] #2369: Reduce dependency on manually replacing primaite version across documentation --- docs/conf.py | 19 +++++++++++++++++++ docs/source/example_notebooks.rst | 4 ++-- docs/source/getting_started.rst | 6 +++--- docs/source/primaite_session.rst | 4 ++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d246afe5..a666e460 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,7 @@ import datetime # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys +from typing import Any import furo # noqa @@ -63,3 +64,21 @@ html_theme = "furo" html_static_path = ["_static"] html_theme_options = {"globaltoc_collapse": True, "globaltoc_maxdepth": 2} html_copy_source = False + + +def replace_token(app: Any, docname: Any, source: Any): + """Replaces a token from the list of tokens.""" + result = source[0] + for key in app.config.tokens: + result = result.replace(key, app.config.tokens[key]) + source[0] = result + + +tokens = {"{VERSION}": release} # Token VERSION is replaced by the value of the PrimAITE version in the version file +"""Dict containing the tokens that need to be replaced in documentation.""" + + +def setup(app: Any): + """Custom setup for sphinx.""" + app.add_config_value("tokens", {}, True) + app.connect("source-read", replace_token) diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index 9fb5cc9e..1ea94249 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -17,12 +17,12 @@ Running Jupyter Notebooks .. code-block:: bash :caption: Unix - cd ~/primaite/PRIMAITE_VERSION_TOKEN + cd ~/primaite/{VERSION} .. code-block:: powershell :caption: Windows (Powershell) - cd ~\primaite\PRIMAITE_VERSION_TOKEN + cd ~\primaite\{VERSION} 2. Run jupyter notebook diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index c1559168..7a23e4f8 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -38,12 +38,12 @@ Install PrimAITE .. code-block:: bash :caption: Unix - mkdir -p ~/primaite/3.0.0b6 + mkdir -p ~/primaite/{VERSION} .. code-block:: powershell :caption: Windows (Powershell) - mkdir ~\primaite\3.0.0b6 + mkdir ~\primaite\{VERSION} 2. Navigate to the primaite directory and create a new python virtual environment (venv) @@ -51,7 +51,7 @@ Install PrimAITE .. code-block:: bash :caption: Unix - cd ~/primaite/3.0.0b6 + cd ~/primaite/{VERSION} python3 -m venv .venv .. code-block:: powershell diff --git a/docs/source/primaite_session.rst b/docs/source/primaite_session.rst index b02e015e..d0caeaad 100644 --- a/docs/source/primaite_session.rst +++ b/docs/source/primaite_session.rst @@ -35,7 +35,7 @@ Outputs ------- Running a session creates a session output directory in your user data folder. The filepath looks like this: -``~/primaite/3.0.0b6/sessions/YYYY-MM-DD/HH-MM-SS/``. This folder contains the simulation sys logs generated by each node, +``~/primaite/{VERSION}/sessions/YYYY-MM-DD/HH-MM-SS/``. This folder contains the simulation sys logs generated by each node, the saved agent checkpoints, and final model. The folder also contains a .json file for each episode step that contains the action, reward, and simulation state. These can be found in -``~/primaite/3.0.0b6/sessions/YYYY-MM-DD/HH-MM-SS/simulation_output/episode_/step_metadata/step_.json`` +``~/primaite/{VERSION}/sessions/YYYY-MM-DD/HH-MM-SS/simulation_output/episode_/step_metadata/step_.json`` From a9bf0981e6624569eb75f2ef0c54451c18ccd27f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 09:22:55 +0000 Subject: [PATCH 115/128] Doc fixes --- docs/source/game_layer.rst | 4 +--- src/primaite/game/agent/rewards.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index ba400ac2..af3eadc6 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -26,10 +26,8 @@ Agents All agents inherit from the :py:class:`primaite.game.agent.interface.AbstractAgent` class, which mandates that they have an ObservationManager, ActionManager, and RewardManager. The agent behaviour depends on the type of agent, but there are two main types: * RL agents action during each step is decided by an appropriate RL algorithm. The agent within PrimAITE just acts to format and forward actions decided by an RL policy. -* Deterministic agents perform all of their decision making within the PrimAITE game layer. They typically have a scripted policy which always performs the same action or a rule-based policy which performs actions based on the current state of the simulation. They can have a stochastic element, and their seed will be settable. +* Deterministic agents perform all of their decision making within the PrimAITE game layer. They typically have a scripted policy which always performs the same action or a rule-based policy which performs actions based on the current state of the simulation. They can have a stochastic element, and their seed is settable. -.. - TODO: add seed to stochastic scripted agents Observations ============ diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index d8cb1328..52bed9e2 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -319,11 +319,11 @@ class SharedReward(AbstractReward): """ Initialise the shared reward. - The agent_ref is a placeholder value. It starts off as none, but it must be set before this reward can work + The agent_name is a placeholder value. It starts off as none, but it must be set before this reward can work correctly. :param agent_name: The name whose reward is an input - :type agent_ref: Optional[str] + :type agent_name: Optional[str] """ self.agent_name = agent_name """Agent whose reward to track.""" From 04c86e30c9f488664ac880060460fe930e29fde2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 11:15:02 +0000 Subject: [PATCH 116/128] Fix some stuff --- .../config/_package_data/data_manipulation.yaml | 10 ++++------ .../_package_data/data_manipulation_marl.yaml | 12 ++++++------ src/primaite/session/io.py | 2 +- src/primaite/simulator/network/hardware/base.py | 2 +- .../simulator/system/core/packet_capture.py | 15 +++++++++------ 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/primaite/config/_package_data/data_manipulation.yaml b/src/primaite/config/_package_data/data_manipulation.yaml index 748b0d77..c561030a 100644 --- a/src/primaite/config/_package_data/data_manipulation.yaml +++ b/src/primaite/config/_package_data/data_manipulation.yaml @@ -11,16 +11,14 @@ training_config: - defender io_settings: - save_checkpoints: true - checkpoint_interval: 5 save_agent_actions: true save_step_metadata: false save_pcap_logs: false - save_sys_logs: true + save_sys_logs: false game: - max_episode_length: 256 + max_episode_length: 128 ports: - HTTP - POSTGRES_SERVER @@ -323,7 +321,7 @@ agents: folder_id: 0 file_id: 0 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 @@ -351,7 +349,7 @@ agents: node_id: 2 folder_id: 0 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 diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index be53d2c5..e4c93161 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -4,7 +4,7 @@ training_config: seed: 333 n_learn_episodes: 1 n_eval_episodes: 5 - max_steps_per_episode: 256 + max_steps_per_episode: 128 deterministic_eval: false n_agents: 2 agent_references: @@ -22,7 +22,7 @@ io_settings: game: - max_episode_length: 256 + max_episode_length: 128 ports: - ARP - DNS @@ -325,7 +325,7 @@ agents: folder_id: 0 file_id: 0 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 @@ -353,7 +353,7 @@ agents: node_id: 2 folder_id: 0 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 @@ -876,7 +876,7 @@ agents: folder_id: 0 file_id: 0 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 @@ -904,7 +904,7 @@ agents: node_id: 2 folder_id: 0 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 diff --git a/src/primaite/session/io.py b/src/primaite/session/io.py index ef77c63d..e57f88ae 100644 --- a/src/primaite/session/io.py +++ b/src/primaite/session/io.py @@ -92,5 +92,5 @@ class PrimaiteIO: @classmethod def from_config(cls, config: Dict) -> "PrimaiteIO": """Create an instance of PrimaiteIO based on a configuration dict.""" - new = cls() + new = cls(settings=cls.Settings(**config)) return new diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index a91a709c..3d8640a6 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -108,7 +108,7 @@ class NetworkInterface(SimComponent, ABC): """Reset the original state of the SimComponent.""" super().setup_for_episode(episode=episode) self.nmne = {} - if episode and self.pcap: + if episode and self.pcap and SIM_OUTPUT.save_pcap_logs: self.pcap.current_episode = episode self.pcap.setup_logger() self.enable() diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index 4916966d..cf38e94b 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -49,8 +49,9 @@ class PacketCapture: self.current_episode: int = 1 - self.setup_logger(outbound=False) - self.setup_logger(outbound=True) + if SIM_OUTPUT.save_pcap_logs: + self.setup_logger(outbound=False) + self.setup_logger(outbound=True) def setup_logger(self, outbound: bool = False): """Set up the logger configuration.""" @@ -108,8 +109,9 @@ class PacketCapture: :param frame: The PCAP frame to capture. """ - msg = frame.model_dump_json() - self.inbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL + if SIM_OUTPUT.save_pcap_logs: + msg = frame.model_dump_json() + self.inbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL def capture_outbound(self, frame): # noqa - I'll have a circular import and cant use if TYPE_CHECKING ;( """ @@ -117,5 +119,6 @@ class PacketCapture: :param frame: The PCAP frame to capture. """ - msg = frame.model_dump_json() - self.outbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL + if SIM_OUTPUT.save_pcap_logs: + msg = frame.model_dump_json() + self.outbound_logger.log(level=60, msg=msg) # Log at custom log level > CRITICAL From e0eef8e56edba45a3b17df895a52a58d41b673e5 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 12:19:56 +0000 Subject: [PATCH 117/128] Fix tests --- tests/assets/configs/shared_rewards.yaml | 6 ++---- tests/assets/configs/test_primaite_session.yaml | 7 +++++-- tests/e2e_integration_tests/test_primaite_session.py | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/assets/configs/shared_rewards.yaml b/tests/assets/configs/shared_rewards.yaml index 91ff20e7..daffa585 100644 --- a/tests/assets/configs/shared_rewards.yaml +++ b/tests/assets/configs/shared_rewards.yaml @@ -11,12 +11,10 @@ training_config: - defender io_settings: - save_checkpoints: true - checkpoint_interval: 5 - save_agent_actions: true + save_agent_actions: false save_step_metadata: false save_pcap_logs: false - save_sys_logs: true + save_sys_logs: false game: diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 199cf8cc..121cc7f1 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -11,8 +11,11 @@ training_config: - defender io_settings: - save_checkpoints: true - checkpoint_interval: 5 + save_agent_actions: true + save_step_metadata: true + save_pcap_logs: true + save_sys_logs: true + game: diff --git a/tests/e2e_integration_tests/test_primaite_session.py b/tests/e2e_integration_tests/test_primaite_session.py index da13dcd8..c45a4690 100644 --- a/tests/e2e_integration_tests/test_primaite_session.py +++ b/tests/e2e_integration_tests/test_primaite_session.py @@ -31,6 +31,7 @@ class TestPrimaiteSession: assert session.env.game.simulation.network assert len(session.env.game.simulation.network.nodes) == 10 + @pytest.mark.skip(reason="Session is not being maintained and will be removed in the subsequent beta release.") @pytest.mark.parametrize("temp_primaite_session", [[CFG_PATH]], indirect=True) def test_start_session(self, temp_primaite_session): """Make sure you can go all the way through the session without errors.""" From 1ed2f48f54cb8feb1477b7426c6dc1a3731d6a53 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 15 Mar 2024 13:13:54 +0000 Subject: [PATCH 118/128] #2369: missed items --- docs/source/example_notebooks.rst | 6 +++++- docs/source/getting_started.rst | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index 1ea94249..e0c4169f 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -5,7 +5,7 @@ Example Jupyter Notebooks ========================= -There are a few example notebooks included which helps with the understanding of PrimAITE's capabilities. +There are a few example notebooks included which help with the understanding of PrimAITE's capabilities. The Jupyter Notebooks can be run via the 2 examples below. These assume that the instructions to install PrimAITE from the :ref:`Getting Started ` page is completed as a prerequisite. @@ -57,6 +57,8 @@ Running Jupyter Notebooks via VSCode It is also possible to view the Jupyter notebooks within VSCode. +The best place to start is by opening a notebook file (.ipynb) in VSCode. If using VSCode to view a notebook for the first time, follow the steps below. + Installing extensions """"""""""""""""""""" @@ -77,3 +79,5 @@ The following extensions should now be installed :align: center VSCode will then ask for a Python environment version to use. PrimAITE is compatible with Python versions 3.8 - 3.10 + +You should now be able to interact with the notebook. diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 7a23e4f8..91db4693 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -57,7 +57,7 @@ Install PrimAITE .. code-block:: powershell :caption: Windows (Powershell) - cd ~\primaite\3.0.0 + cd ~\primaite\{VERSION} python3 -m venv .venv attrib +h .venv /s /d # Hides the .venv directory From 2fde07178930d3dd9b8cb2d5c01d03beea7ac8a2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 13:25:50 +0000 Subject: [PATCH 119/128] Update docs on example notebooks --- docs/source/example_notebooks.rst | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index 1ea94249..3bf14bcb 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -5,7 +5,7 @@ Example Jupyter Notebooks ========================= -There are a few example notebooks included which helps with the understanding of PrimAITE's capabilities. +There are a few example notebooks included which help with the understanding of PrimAITE's capabilities. The Jupyter Notebooks can be run via the 2 examples below. These assume that the instructions to install PrimAITE from the :ref:`Getting Started ` page is completed as a prerequisite. @@ -24,7 +24,7 @@ Running Jupyter Notebooks cd ~\primaite\{VERSION} -2. Run jupyter notebook +2. Run jupyter notebook (the python environment to which you installed PrimAITE must be active) .. code-block:: bash :caption: Unix @@ -38,19 +38,13 @@ Running Jupyter Notebooks 3. Opening the jupyter webpage (optional) -The default web browser may automatically open the webpage. However, if that is not the case, open a web browser and navigate to |jupyter_index|. +The default web browser may automatically open the webpage. However, if that is not the case, click the link shown in your command prompt output. It should look like this: ``http://localhost:8888/?token=ab83071fd13cb5a1384efba318...`` -.. |jupyter_index| raw:: html - - http://localhost:8888/tree 4. Navigate to the list of notebooks -The example notebooks are located in notebooks/example_notebooks or by navigating to |jupyter_index_notebooks| +The example notebooks are located in ``notebooks/example_notebooks/``. The file system shown in the jupyter webpage is relative to the location in which the ``jupyter notebook`` command was used. -.. |jupyter_index_notebooks| raw:: html - - http://localhost:8888/tree/notebooks/example_notebooks Running Jupyter Notebooks via VSCode ------------------------------------ From d9b65065728d41548851a5ed011240a4e5148610 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 13:42:59 +0000 Subject: [PATCH 120/128] Mention python version in getting started guide. --- docs/source/getting_started.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 91db4693..bb6e0019 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -11,7 +11,7 @@ Getting Started Pre-Requisites -In order to get **PrimAITE** installed, you will need to have a python version between 3.8 and 3.11 installed. If you don't already have it, this is how to install it: +In order to get **PrimAITE** installed, you will need Python, venv, and pip. If you don't already have them, this is how to install it: .. code-block:: bash @@ -30,6 +30,8 @@ In order to get **PrimAITE** installed, you will need to have a python version b **PrimAITE** is designed to be OS-agnostic, and thus should work on most variations/distros of Linux, Windows, and MacOS. +Installing PrimAITE has been tested with all supported python versions, venv 20.24.1, and pip 23. + Install PrimAITE **************** From 6f780f20d7316c91a845ead049530fd8977f00b1 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 15 Mar 2024 13:55:00 +0000 Subject: [PATCH 121/128] 2384: Updates for 3.0.0b7 release. --- CHANGELOG.md | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae40a36f..c01f0139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,27 +12,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed the red agent in the data manipulation scenario to randomly choose client 1 or client 2 to start its attack. - Changed the data manipulation scenario to include a second green agent on client 1. - Refactored actions and observations to be configurable via object name, instead of UUID. -- Fixed a bug where ACL rules were not resetting on episode reset. -- Fixed a bug where blue agent's ACL actions were being applied against the wrong IP addresses -- Fixed a bug where deleted files and folders did not reset correctly on episode reset. -- Fixed a bug where service health status was using the actual health state instead of the visible health state -- Fixed a bug where the database file health status was using the incorrect value for negative rewards -- Fixed a bug preventing file actions from reaching their intended file - Made database patch correctly take 2 timesteps instead of being immediate - Made database patch only possible when the software is compromised or good, it's no longer possible when the software is OFF or RESETTING -- Temporarily disable the blue agent file delete action due to crashes. This issue is resolved in another branch that will be merged into dev soon. -- Fix a bug where ACLs were not showing up correctly in the observation space. - Added a notebook which explains Data manipulation scenario, demonstrates the attack, and shows off blue agent's action space, observation space, and reward function. - Made packet capture and system logging optional (off by default). To turn on, change the io_settings.save_pcap_logs and io_settings.save_sys_logs settings in the config. -- Made observation space flattening optional (on by default). To turn off for an agent, change the agent_settings.flatten_obs setting in the config. -- Fixed an issue where the data manipulation attack was triggered at episode start. -- Fixed a bug where FTP STOR stored an additional copy on the client machine's filesystem -- Fixed a bug where the red agent acted to early -- Fixed the order of service health state -- Fixed an issue where starting a node didn't start the services on it +- Made observation space flattening optional (on by default). To turn off for an agent, change the `agent_settings.flatten_obs` setting in the config. - Added support for SQL INSERT command. - Added ability to log each agent's action choices in each step to a JSON file. +### Bug Fixes + +- ACL rules were not resetting on episode reset. +- ACLs were not showing up correctly in the observation space. +- Blue agent's ACL actions were being applied against the wrong IP addresses +- Deleted files and folders did not reset correctly on episode reset. +- Service health status was using the actual health state instead of the visible health state +- Database file health status was using the incorrect value for negative rewards +- Preventing file actions from reaching their intended file +- The data manipulation attack was triggered at episode start. +- FTP STOR stored an additional copy on the client machine's filesystem +- The red agent acted to early +- Order of service health state +- Starting a node didn't start the services on it +- Fixed an issue where the services were still able to run even though the node the service is installed on is turned off + ### Added @@ -51,8 +54,12 @@ a Service/Application another machine. SessionManager. - Permission System - each action can define criteria that will be used to permit or deny agent actions. - File System - ability to emulate a node's file system during a simulation -- Example notebooks - There is currently 1 jupyter notebook which walks through using PrimAITE - 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) +- Example notebooks - There are 5 jupyter notebook which walk through using PrimAITE + 1. Training a Stable Baselines 3 agent + 2. Training a single agent system using Ray RLLib + 3. Training a multi-agent system Ray RLLib + 4. Data manipulation end to end demonstration + 5. Data manipulation scenario with customised red agents - Database: - `DatabaseClient` and `DatabaseService` created to allow emulation of database actions - Ability for `DatabaseService` to backup its data to another server via FTP and restore data from backup @@ -62,7 +69,6 @@ SessionManager. - DNS Services: `DNSClient` and `DNSServer` - FTP Services: `FTPClient` and `FTPServer` - HTTP Services: `WebBrowser` to simulate a web client and `WebServer` -- Fixed an issue where the services were still able to run even though the node the service is installed on is turned off - NTP Services: `NTPClient` and `NTPServer` - **RouterNIC Class**: Introduced a new class `RouterNIC`, extending the standard `NIC` functionality. This class is specifically designed for router operations, optimizing the processing and routing of network traffic. - **Custom Layer-3 Processing**: The `RouterNIC` class includes custom handling for network frames, bypassing standard Node NIC's Layer 3 broadcast/unicast checks. This allows for more efficient routing behavior in network scenarios where router-specific frame processing is required. From 33dd6e4dcd742a4cde0d2a9ec2319861850a7aa8 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 14:03:37 +0000 Subject: [PATCH 122/128] Make sure notebook is using correct dict key --- .../Data-Manipulation-E2E-Demonstration.ipynb | 501 +++++++++++++++++- 1 file changed, 475 insertions(+), 26 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 946202b6..93e1f27f 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -352,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [] }, @@ -364,7 +364,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "tags": [] }, @@ -389,9 +389,154 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "env created successfully\n", + "{'ACL': {1: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 0,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 2: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 1,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 3: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 2,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 4: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 3,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 5: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 4,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 6: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 5,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 7: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 6,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 8: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 7,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 9: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 8,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0},\n", + " 10: {'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'permission': 0,\n", + " 'position': 9,\n", + " 'protocol': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0}},\n", + " 'ICS': 0,\n", + " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", + " 2: {'PROTOCOLS': {'ALL': 1}},\n", + " 3: {'PROTOCOLS': {'ALL': 1}},\n", + " 4: {'PROTOCOLS': {'ALL': 1}},\n", + " 5: {'PROTOCOLS': {'ALL': 1}},\n", + " 6: {'PROTOCOLS': {'ALL': 1}},\n", + " 7: {'PROTOCOLS': {'ALL': 1}},\n", + " 8: {'PROTOCOLS': {'ALL': 1}},\n", + " 9: {'PROTOCOLS': {'ALL': 1}},\n", + " 10: {'PROTOCOLS': {'ALL': 0}}},\n", + " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", + " 'health_status': 1}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", + " 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", + " 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}}\n" + ] + } + ], "source": [ "# create the env\n", "with open(data_manipulation_config_path(), 'r') as f:\n", @@ -419,7 +564,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -437,9 +582,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 1, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 2, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 3, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 4, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 5, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 6, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 7, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 8, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 9, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 10, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 11, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 12, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 13, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 14, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 15, Red action: DO NOTHING, Blue reward:0.90\n", + "step: 16, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 17, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 18, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 19, Red action: DO NOTHING, Blue reward:0.95\n", + "step: 20, Red action: DO NOTHING, Blue reward:1.00\n", + "step: 21, Red action: DO NOTHING, Blue reward:1.00\n", + "step: 22, Red action: DO NOTHING, Blue reward:1.00\n", + "step: 23, Red action: DO NOTHING, Blue reward:1.00\n", + "step: 24, Red action: DO NOTHING, Blue reward:1.00\n", + "step: 25, Red action: DO NOTHING, Blue reward:1.00\n", + "step: 26, Red action: ATTACK from client 1, Blue reward:0.20\n", + "step: 27, Red action: DO NOTHING, Blue reward:-0.30\n", + "step: 28, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 29, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 30, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 31, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 32, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 33, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 34, Red action: DO NOTHING, Blue reward:-0.80\n", + "step: 35, Red action: DO NOTHING, Blue reward:-0.80\n" + ] + } + ], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -455,9 +642,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}\n" + ] + } + ], "source": [ "pprint(obs['NODES'])" ] @@ -471,9 +700,51 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", + " 'operating_status': 1},\n", + " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1},\n", + " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", + " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", + " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", + " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", + " 'operating_status': 1}}\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -504,9 +775,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 38\n", + "Red action: DONOTHING\n", + "Green action: NODE_APPLICATION_EXECUTE\n", + "Green action: NODE_APPLICATION_EXECUTE\n", + "Blue reward:-0.8\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -529,9 +812,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 48\n", + "Red action: DONOTHING\n", + "Green action: timestep=47 action='NODE_APPLICATION_EXECUTE' parameters={'node_id': 0, 'application_id': 0} request=['network', 'node', 'client_2', 'application', 'WebBrowser', 'execute'] response=RequestResponse(status='failure', data={})\n", + "Green action: timestep=47 action='NODE_APPLICATION_EXECUTE' parameters={'node_id': 0, 'application_id': 0} request=['network', 'node', 'client_1', 'application', 'WebBrowser', 'execute'] response=RequestResponse(status='failure', data={})\n", + "Blue reward:-0.80\n" + ] + } + ], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -552,9 +847,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 49, Red action: DONOTHING, Blue reward:-0.80\n", + "step: 50, Red action: DONOTHING, Blue reward:-0.80\n", + "step: 51, Red action: DONOTHING, Blue reward:-0.80\n", + "step: 52, Red action: DONOTHING, Blue reward:1.00\n", + "step: 53, Red action: DONOTHING, Blue reward:0.90\n", + "step: 54, Red action: DONOTHING, Blue reward:0.90\n", + "step: 55, Red action: DONOTHING, Blue reward:0.90\n", + "step: 56, Red action: DONOTHING, Blue reward:0.90\n", + "step: 57, Red action: DONOTHING, Blue reward:0.90\n", + "step: 58, Red action: DONOTHING, Blue reward:0.90\n", + "step: 59, Red action: DONOTHING, Blue reward:0.80\n" + ] + } + ], "source": [ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", @@ -576,7 +889,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now, even though the red agent executes an attack, the reward stays at 0.8." + "Now, even though the red agent executes an attack, the reward will stay at 0.8." ] }, { @@ -588,9 +901,89 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{1: {'position': 0,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 2: {'position': 1,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 3: {'position': 2,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 4: {'position': 3,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 5: {'position': 4,\n", + " 'permission': 2,\n", + " 'source_node_id': 7,\n", + " 'source_port': 1,\n", + " 'dest_node_id': 4,\n", + " 'dest_port': 1,\n", + " 'protocol': 3},\n", + " 6: {'position': 5,\n", + " 'permission': 2,\n", + " 'source_node_id': 8,\n", + " 'source_port': 1,\n", + " 'dest_node_id': 4,\n", + " 'dest_port': 1,\n", + " 'protocol': 3},\n", + " 7: {'position': 6,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 8: {'position': 7,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 9: {'position': 8,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\n", + " 10: {'position': 9,\n", + " 'permission': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0}}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "obs['ACL']" ] @@ -604,9 +997,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "blocking client 1\n", + "\n" + ] + } + ], "source": [ "env.step(58) # Remove the ACL rule that blocks client 1\n", "env.step(57) # Remove the ACL rule that blocks client 2\n", @@ -616,12 +1018,12 @@ " tries += 1\n", " obs, reward, terminated, truncated, info = env.step(0)\n", "\n", - " if obs['NODES'][6]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", + " if obs['NODES'][6]['NICS'][1]['NMNE']['outbound'] == 1:\n", " # client 1 has NMNEs, let's block it\n", " obs, reward, terminated, truncated, info = env.step(50) # block client 1\n", " print(\"blocking client 1\")\n", " break\n", - " elif obs['NODES'][7]['NETWORK_INTERFACES'][1]['nmne']['outbound'] == 1:\n", + " elif obs['NODES'][7]['NICS'][1]['NMNE']['outbound'] == 1:\n", " # client 2 has NMNEs, so let's block it\n", " obs, reward, terminated, truncated, info = env.step(51) # block client 2\n", " print(\"blocking client 2\")\n", @@ -643,9 +1045,56 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 90, Red action: DONOTHING, Blue reward:0.00\n", + "step: 91, Red action: DONOTHING, Blue reward:1.00\n", + "step: 92, Red action: DONOTHING, Blue reward:1.00\n", + "step: 93, Red action: DONOTHING, Blue reward:1.00\n", + "step: 94, Red action: DONOTHING, Blue reward:1.00\n", + "step: 95, Red action: DONOTHING, Blue reward:1.00\n", + "step: 96, Red action: DONOTHING, Blue reward:0.90\n", + "step: 97, Red action: DONOTHING, Blue reward:0.90\n", + "step: 98, Red action: DONOTHING, Blue reward:0.90\n", + "step: 99, Red action: DONOTHING, Blue reward:0.90\n", + "step: 100, Red action: DONOTHING, Blue reward:0.90\n", + "step: 101, Red action: DONOTHING, Blue reward:0.90\n", + "step: 102, Red action: DONOTHING, Blue reward:0.90\n", + "step: 103, Red action: DONOTHING, Blue reward:0.90\n", + "step: 104, Red action: DONOTHING, Blue reward:0.90\n", + "step: 105, Red action: DONOTHING, Blue reward:0.90\n", + "step: 106, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.90\n", + "step: 107, Red action: DONOTHING, Blue reward:0.90\n", + "step: 108, Red action: DONOTHING, Blue reward:0.90\n", + "step: 109, Red action: DONOTHING, Blue reward:0.90\n", + "step: 110, Red action: DONOTHING, Blue reward:0.90\n", + "step: 111, Red action: DONOTHING, Blue reward:0.90\n", + "step: 112, Red action: DONOTHING, Blue reward:0.90\n", + "step: 113, Red action: DONOTHING, Blue reward:0.90\n", + "step: 114, Red action: DONOTHING, Blue reward:0.90\n", + "step: 115, Red action: DONOTHING, Blue reward:0.90\n", + "step: 116, Red action: DONOTHING, Blue reward:0.90\n", + "step: 117, Red action: DONOTHING, Blue reward:0.90\n", + "step: 118, Red action: DONOTHING, Blue reward:0.90\n", + "step: 119, Red action: DONOTHING, Blue reward:0.90\n", + "step: 120, Red action: DONOTHING, Blue reward:0.90\n", + "step: 121, Red action: DONOTHING, Blue reward:0.90\n", + "step: 122, Red action: DONOTHING, Blue reward:0.90\n", + "step: 123, Red action: DONOTHING, Blue reward:0.90\n", + "step: 124, Red action: DONOTHING, Blue reward:0.90\n", + "step: 125, Red action: DONOTHING, Blue reward:0.90\n", + "step: 126, Red action: DONOTHING, Blue reward:0.90\n", + "step: 127, Red action: DONOTHING, Blue reward:0.90\n", + "step: 128, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.90\n", + "step: 129, Red action: DONOTHING, Blue reward:0.90\n" + ] + } + ], "source": [ "\n", "for step in range(40):\n", From c8beb39facc2ae5ae364c002efb08d44095c78f2 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 14:03:49 +0000 Subject: [PATCH 123/128] clear notebook output --- .../Data-Manipulation-E2E-Demonstration.ipynb | 495 +----------------- 1 file changed, 23 insertions(+), 472 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb index 93e1f27f..7ec58b2c 100644 --- a/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-E2E-Demonstration.ipynb @@ -352,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, @@ -364,7 +364,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, @@ -389,154 +389,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "env created successfully\n", - "{'ACL': {1: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 0,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 2: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 1,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 3: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 2,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 4: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 3,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 5: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 4,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 6: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 5,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 7: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 6,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 8: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 7,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 9: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 8,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0},\n", - " 10: {'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'permission': 0,\n", - " 'position': 9,\n", - " 'protocol': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0}},\n", - " 'ICS': 0,\n", - " 'LINKS': {1: {'PROTOCOLS': {'ALL': 1}},\n", - " 2: {'PROTOCOLS': {'ALL': 1}},\n", - " 3: {'PROTOCOLS': {'ALL': 1}},\n", - " 4: {'PROTOCOLS': {'ALL': 1}},\n", - " 5: {'PROTOCOLS': {'ALL': 1}},\n", - " 6: {'PROTOCOLS': {'ALL': 1}},\n", - " 7: {'PROTOCOLS': {'ALL': 1}},\n", - " 8: {'PROTOCOLS': {'ALL': 1}},\n", - " 9: {'PROTOCOLS': {'ALL': 1}},\n", - " 10: {'PROTOCOLS': {'ALL': 0}}},\n", - " 'NODES': {1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}},\n", - " 'health_status': 1}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}},\n", - " 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0},\n", - " 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}}\n" - ] - } - ], + "outputs": [], "source": [ "# create the env\n", "with open(data_manipulation_config_path(), 'r') as f:\n", @@ -564,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -582,51 +437,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 1, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 2, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 3, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 4, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 5, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 6, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 7, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 8, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 9, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 10, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 11, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 12, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 13, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 14, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 15, Red action: DO NOTHING, Blue reward:0.90\n", - "step: 16, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 17, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 18, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 19, Red action: DO NOTHING, Blue reward:0.95\n", - "step: 20, Red action: DO NOTHING, Blue reward:1.00\n", - "step: 21, Red action: DO NOTHING, Blue reward:1.00\n", - "step: 22, Red action: DO NOTHING, Blue reward:1.00\n", - "step: 23, Red action: DO NOTHING, Blue reward:1.00\n", - "step: 24, Red action: DO NOTHING, Blue reward:1.00\n", - "step: 25, Red action: DO NOTHING, Blue reward:1.00\n", - "step: 26, Red action: ATTACK from client 1, Blue reward:0.20\n", - "step: 27, Red action: DO NOTHING, Blue reward:-0.30\n", - "step: 28, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 29, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 30, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 31, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 32, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 33, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 34, Red action: DO NOTHING, Blue reward:-0.80\n", - "step: 35, Red action: DO NOTHING, Blue reward:-0.80\n" - ] - } - ], + "outputs": [], "source": [ "for step in range(35):\n", " obs, reward, terminated, truncated, info = env.step(0)\n", @@ -642,51 +455,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 1}}, 'health_status': 1}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}\n" - ] - } - ], + "outputs": [], "source": [ "pprint(obs['NODES'])" ] @@ -700,51 +471,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{1: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 2: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 3, 'operating_status': 1}},\n", - " 'operating_status': 1},\n", - " 3: {'FOLDERS': {1: {'FILES': {1: {'health_status': 2}}, 'health_status': 1}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 4: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 5: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 6: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1},\n", - " 7: {'FOLDERS': {1: {'FILES': {1: {'health_status': 0}}, 'health_status': 0}},\n", - " 'NICS': {1: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 1},\n", - " 2: {'NMNE': {'inbound': 0, 'outbound': 0}, 'nic_status': 0}},\n", - " 'SERVICES': {1: {'health_status': 0, 'operating_status': 0}},\n", - " 'operating_status': 1}}\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", @@ -775,21 +504,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 38\n", - "Red action: DONOTHING\n", - "Green action: NODE_APPLICATION_EXECUTE\n", - "Green action: NODE_APPLICATION_EXECUTE\n", - "Blue reward:-0.8\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(13) # patch the database\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -812,21 +529,9 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 48\n", - "Red action: DONOTHING\n", - "Green action: timestep=47 action='NODE_APPLICATION_EXECUTE' parameters={'node_id': 0, 'application_id': 0} request=['network', 'node', 'client_2', 'application', 'WebBrowser', 'execute'] response=RequestResponse(status='failure', data={})\n", - "Green action: timestep=47 action='NODE_APPLICATION_EXECUTE' parameters={'node_id': 0, 'application_id': 0} request=['network', 'node', 'client_1', 'application', 'WebBrowser', 'execute'] response=RequestResponse(status='failure', data={})\n", - "Blue reward:-0.80\n" - ] - } - ], + "outputs": [], "source": [ "obs, reward, terminated, truncated, info = env.step(0) # do nothing\n", "print(f\"step: {env.game.step_counter}\")\n", @@ -847,27 +552,9 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 49, Red action: DONOTHING, Blue reward:-0.80\n", - "step: 50, Red action: DONOTHING, Blue reward:-0.80\n", - "step: 51, Red action: DONOTHING, Blue reward:-0.80\n", - "step: 52, Red action: DONOTHING, Blue reward:1.00\n", - "step: 53, Red action: DONOTHING, Blue reward:0.90\n", - "step: 54, Red action: DONOTHING, Blue reward:0.90\n", - "step: 55, Red action: DONOTHING, Blue reward:0.90\n", - "step: 56, Red action: DONOTHING, Blue reward:0.90\n", - "step: 57, Red action: DONOTHING, Blue reward:0.90\n", - "step: 58, Red action: DONOTHING, Blue reward:0.90\n", - "step: 59, Red action: DONOTHING, Blue reward:0.80\n" - ] - } - ], + "outputs": [], "source": [ "env.step(13) # Patch the database\n", "print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )\n", @@ -901,89 +588,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{1: {'position': 0,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 2: {'position': 1,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 3: {'position': 2,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 4: {'position': 3,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 5: {'position': 4,\n", - " 'permission': 2,\n", - " 'source_node_id': 7,\n", - " 'source_port': 1,\n", - " 'dest_node_id': 4,\n", - " 'dest_port': 1,\n", - " 'protocol': 3},\n", - " 6: {'position': 5,\n", - " 'permission': 2,\n", - " 'source_node_id': 8,\n", - " 'source_port': 1,\n", - " 'dest_node_id': 4,\n", - " 'dest_port': 1,\n", - " 'protocol': 3},\n", - " 7: {'position': 6,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 8: {'position': 7,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 9: {'position': 8,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0},\n", - " 10: {'position': 9,\n", - " 'permission': 0,\n", - " 'source_node_id': 0,\n", - " 'source_port': 0,\n", - " 'dest_node_id': 0,\n", - " 'dest_port': 0,\n", - " 'protocol': 0}}" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "obs['ACL']" ] @@ -997,18 +604,9 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "blocking client 1\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "env.step(58) # Remove the ACL rule that blocks client 1\n", "env.step(57) # Remove the ACL rule that blocks client 2\n", @@ -1045,56 +643,9 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "step: 90, Red action: DONOTHING, Blue reward:0.00\n", - "step: 91, Red action: DONOTHING, Blue reward:1.00\n", - "step: 92, Red action: DONOTHING, Blue reward:1.00\n", - "step: 93, Red action: DONOTHING, Blue reward:1.00\n", - "step: 94, Red action: DONOTHING, Blue reward:1.00\n", - "step: 95, Red action: DONOTHING, Blue reward:1.00\n", - "step: 96, Red action: DONOTHING, Blue reward:0.90\n", - "step: 97, Red action: DONOTHING, Blue reward:0.90\n", - "step: 98, Red action: DONOTHING, Blue reward:0.90\n", - "step: 99, Red action: DONOTHING, Blue reward:0.90\n", - "step: 100, Red action: DONOTHING, Blue reward:0.90\n", - "step: 101, Red action: DONOTHING, Blue reward:0.90\n", - "step: 102, Red action: DONOTHING, Blue reward:0.90\n", - "step: 103, Red action: DONOTHING, Blue reward:0.90\n", - "step: 104, Red action: DONOTHING, Blue reward:0.90\n", - "step: 105, Red action: DONOTHING, Blue reward:0.90\n", - "step: 106, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.90\n", - "step: 107, Red action: DONOTHING, Blue reward:0.90\n", - "step: 108, Red action: DONOTHING, Blue reward:0.90\n", - "step: 109, Red action: DONOTHING, Blue reward:0.90\n", - "step: 110, Red action: DONOTHING, Blue reward:0.90\n", - "step: 111, Red action: DONOTHING, Blue reward:0.90\n", - "step: 112, Red action: DONOTHING, Blue reward:0.90\n", - "step: 113, Red action: DONOTHING, Blue reward:0.90\n", - "step: 114, Red action: DONOTHING, Blue reward:0.90\n", - "step: 115, Red action: DONOTHING, Blue reward:0.90\n", - "step: 116, Red action: DONOTHING, Blue reward:0.90\n", - "step: 117, Red action: DONOTHING, Blue reward:0.90\n", - "step: 118, Red action: DONOTHING, Blue reward:0.90\n", - "step: 119, Red action: DONOTHING, Blue reward:0.90\n", - "step: 120, Red action: DONOTHING, Blue reward:0.90\n", - "step: 121, Red action: DONOTHING, Blue reward:0.90\n", - "step: 122, Red action: DONOTHING, Blue reward:0.90\n", - "step: 123, Red action: DONOTHING, Blue reward:0.90\n", - "step: 124, Red action: DONOTHING, Blue reward:0.90\n", - "step: 125, Red action: DONOTHING, Blue reward:0.90\n", - "step: 126, Red action: DONOTHING, Blue reward:0.90\n", - "step: 127, Red action: DONOTHING, Blue reward:0.90\n", - "step: 128, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.90\n", - "step: 129, Red action: DONOTHING, Blue reward:0.90\n" - ] - } - ], + "outputs": [], "source": [ "\n", "for step in range(40):\n", From 7f4f3e9bfe85fd9aa8834e755e8d0128dd5fcbbf Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 14:09:02 +0000 Subject: [PATCH 124/128] Calm logging --- src/primaite/game/agent/rewards.py | 2 +- src/primaite/simulator/network/airspace.py | 2 +- src/primaite/simulator/network/hardware/base.py | 6 +++--- src/primaite/simulator/system/applications/application.py | 2 +- src/primaite/simulator/system/services/service.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 52bed9e2..d214ecc9 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -111,7 +111,7 @@ class DatabaseFileIntegrity(AbstractReward): """ database_file_state = access_from_nested_dict(state, self.location_in_state) if database_file_state is NOT_PRESENT_IN_STATE: - _LOGGER.info( + _LOGGER.debug( f"Could not calculate {self.__class__} reward because " "simulation state did not contain enough information." ) diff --git a/src/primaite/simulator/network/airspace.py b/src/primaite/simulator/network/airspace.py index 5ceedc8e..a8343675 100644 --- a/src/primaite/simulator/network/airspace.py +++ b/src/primaite/simulator/network/airspace.py @@ -157,7 +157,7 @@ class WirelessNetworkInterface(NetworkInterface, ABC): return if not self._connected_node: - _LOGGER.error(f"Interface {self} cannot be enabled as it is not connected to a Node") + _LOGGER.warning(f"Interface {self} cannot be enabled as it is not connected to a Node") return if self._connected_node.operating_state != NodeOperatingState.ON: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index a91a709c..0695c1c7 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -297,7 +297,7 @@ class WiredNetworkInterface(NetworkInterface, ABC): return True if not self._connected_node: - _LOGGER.error(f"Interface {self} cannot be enabled as it is not connected to a Node") + _LOGGER.warning(f"Interface {self} cannot be enabled as it is not connected to a Node") return False if self._connected_node.operating_state != NodeOperatingState.ON: @@ -343,11 +343,11 @@ class WiredNetworkInterface(NetworkInterface, ABC): :param link: The Link instance to connect to this network interface. """ if self._connected_link: - _LOGGER.error(f"Cannot connect Link to network interface {self} as it already has a connection") + _LOGGER.warning(f"Cannot connect Link to network interface {self} as it already has a connection") return if self._connected_link == link: - _LOGGER.error(f"Cannot connect Link to network interface {self} as it is already connected") + _LOGGER.warning(f"Cannot connect Link to network interface {self} as it is already connected") return self._connected_link = link diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 74013681..b7422680 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -83,7 +83,7 @@ class Application(IOSoftware): if self.operating_state is not self.operating_state.RUNNING: # service is not running - _LOGGER.error(f"Cannot perform action: {self.name} is {self.operating_state.name}") + _LOGGER.debug(f"Cannot perform action: {self.name} is {self.operating_state.name}") return False return True diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index e15377a9..b2a6f685 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -59,7 +59,7 @@ class Service(IOSoftware): if self.operating_state is not ServiceOperatingState.RUNNING: # service is not running - _LOGGER.error(f"Cannot perform action: {self.name} is {self.operating_state.name}") + _LOGGER.debug(f"Cannot perform action: {self.name} is {self.operating_state.name}") return False return True From 8a9d8fb17cd7867a5ca58291d5cb8c7238c0afde Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 14:10:34 +0000 Subject: [PATCH 125/128] Calm logging again --- src/primaite/game/agent/rewards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index d214ecc9..2201b09e 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -231,7 +231,7 @@ class WebpageUnavailablePenalty(AbstractReward): # If the last request did actually go through, then check if the webpage also loaded web_browser_state = access_from_nested_dict(state, self.location_in_state) if web_browser_state is NOT_PRESENT_IN_STATE or "history" not in web_browser_state: - _LOGGER.info( + _LOGGER.debug( "Web browser reward could not be calculated because the web browser history on node", f"{self._node} was not reported in the simulation state. Returning 0.0", ) From 9a38fcdae263f11462bcab1c3dc0b88052004904 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 14:19:30 +0000 Subject: [PATCH 126/128] Fix broken config --- src/primaite/config/_package_data/data_manipulation_marl.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/primaite/config/_package_data/data_manipulation_marl.yaml b/src/primaite/config/_package_data/data_manipulation_marl.yaml index be53d2c5..652dd809 100644 --- a/src/primaite/config/_package_data/data_manipulation_marl.yaml +++ b/src/primaite/config/_package_data/data_manipulation_marl.yaml @@ -13,8 +13,6 @@ training_config: io_settings: - save_checkpoints: true - checkpoint_interval: 5 save_agent_actions: true save_step_metadata: false save_pcap_logs: false From fc67fc48337160f5e8dc291982161f004f475d94 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 14:37:28 +0000 Subject: [PATCH 127/128] Fix notebook parsing data --- .../Data-Manipulation-Customising-Red-Agent.ipynb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb index 779d89f6..56e9bf5a 100644 --- a/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb +++ b/src/primaite/notebooks/Data-Manipulation-Customising-Red-Agent.ipynb @@ -22,6 +22,7 @@ "# Imports\n", "\n", "from primaite.config.load import data_manipulation_config_path\n", + "from primaite.game.agent.interface import AgentActionHistoryItem\n", "from primaite.session.environment import PrimaiteGymEnv\n", "import yaml\n", "from pprint import pprint" @@ -62,12 +63,12 @@ "source": [ "def friendly_output_red_action(info):\n", " # parse the info dict form step output and write out what the red agent is doing\n", - " red_info = info['agent_actions']['data_manipulation_attacker']\n", - " red_action = red_info[0]\n", + " red_info : AgentActionHistoryItem = info['agent_actions']['data_manipulation_attacker']\n", + " red_action = red_info.action\n", " if red_action == 'DONOTHING':\n", " red_str = 'DO NOTHING'\n", " elif red_action == 'NODE_APPLICATION_EXECUTE':\n", - " client = \"client 1\" if red_info[1]['node_id'] == 0 else \"client 2\"\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" ] From b217869dd118a108f2eb61f318cf60340aea61ef Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 15 Mar 2024 15:03:27 +0000 Subject: [PATCH 128/128] Improve clarity in docs --- docs/source/example_notebooks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/example_notebooks.rst b/docs/source/example_notebooks.rst index e77b444e..99d47822 100644 --- a/docs/source/example_notebooks.rst +++ b/docs/source/example_notebooks.rst @@ -38,7 +38,7 @@ Running Jupyter Notebooks 3. Opening the jupyter webpage (optional) -The default web browser may automatically open the webpage. However, if that is not the case, click the link shown in your command prompt output. It should look like this: ``http://localhost:8888/?token=ab83071fd13cb5a1384efba318...`` +The default web browser may automatically open the webpage. However, if that is not the case, click the link shown in your command prompt output. It should look like this: ``http://localhost:8888/?token=0123456798abc0123456789abc`` 4. Navigate to the list of notebooks