From 28acb5dcaed89364bb920591a8d1bd82f51e6da1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 25 Jan 2024 12:04:09 +0000 Subject: [PATCH] Populate step info in environment, and finish notebook --- .../config/_package_data/example_config.yaml | 2 +- .../example_config_2_rl_agents.yaml | 2 +- src/primaite/game/game.py | 5 +- .../notebooks/_package_data/uc2_network.png | Bin 0 -> 70887 bytes src/primaite/notebooks/uc2_demo.ipynb | 1038 +++++++++++++---- src/primaite/session/environment.py | 8 +- .../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 +- 11 files changed, 850 insertions(+), 215 deletions(-) create mode 100644 src/primaite/notebooks/_package_data/uc2_network.png diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 7393f5a3..d8cd0099 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -31,7 +31,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: 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 c1e2ea81..6aa54487 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 @@ -25,7 +25,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 159f5bbb..146261f9 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -113,7 +113,7 @@ class PrimaiteGame: self.update_agents(sim_state) # Apply all actions to simulation as requests - self.apply_agent_actions() + agent_actions = self.apply_agent_actions() # noqa # Advance timestep self.advance_timestep() @@ -131,12 +131,15 @@ class PrimaiteGame: def apply_agent_actions(self) -> None: """Apply all actions to simulation as requests.""" + agent_actions = {} 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) + agent_actions[agent.agent_name] = (action_choice, options) request = agent.format_request(action_choice, options) self.simulation.apply_request(request) + return agent_actions def advance_timestep(self) -> None: """Advance timestep.""" diff --git a/src/primaite/notebooks/_package_data/uc2_network.png b/src/primaite/notebooks/_package_data/uc2_network.png new file mode 100644 index 0000000000000000000000000000000000000000..20fa43c996bb7c3bce61778a7aa233ee4b8852f6 GIT binary patch literal 70887 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](_package_data/uc2_network.png)\n", + "\n", + "_(click image to enlarge)_\n", + "\n", + "The red agent deletes the contents of the database. When this happens, the web app cannot fetch data and users navigating to the website get a 404 error.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Network\n", + "\n", + "- The web server has:\n", + " - a web service that replies to user HTTP requests\n", + " - a database client that fetches data for the web service\n", + "- The database server has:\n", + " - a POSTGRES database service\n", + " - a database file which is accessed by the database service\n", + " - FTP client used for backing up the data to the backup_server\n", + "- The backup server has:\n", + " - a copy of the database file in a known good state\n", + " - FTP server that can send the backed up file back to the database server\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Green agent\n", + "\n", + "The green agent is logged onto client 2. It sometimes uses the web browser on client 2 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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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 reaching the database server. This can be done by removing client 1's network connection or adding ACL rules on the router to stop the packets from arriving." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Reinforcement learning details" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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 can choose one of two action each timestep:\n", + "1. do nothing\n", + "2. execute the data manipulation application\n", + "The schedule for selecting when to execute the application is controlled by three parameters:\n", + "- start time\n", + "- frequency\n", + "- variance\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", + "Upon a successful attack, the database file becomes corrupted which incurs a negative reward for the RL defender.\n", + "\n", + "The red agent does not use information about the state of the network to decide its action.\n", + "\n", + "### Green\n", + "The green agent sits on client 2 and uses the web browser application to send requests to the web server. The schedule of the green agent is currently random, meaning it will request webpage with a 50% probability, and do nothing with a 50% probability.\n", + "\n", + "When the green agent is blocked from accessing the data through the webpage, this incurs a negative reward to the RL defender." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Observation Space\n", + "\n", + "The blue agent's observation space is structured as nested dictionary with the following information:\n", + "```\n", + "\n", + "- NODES\n", + " - \n", + " - SERVICES\n", + " - \n", + " - operating_status\n", + " - health_status\n", + " - FOLDERS\n", + " - \n", + " - health_status\n", + " - FILES\n", + " - \n", + " - health_status\n", + " - NICS\n", + " - \n", + " - nic_status\n", + " - operating_status\n", + "- LINKS\n", + " - \n", + " - PROTOCOLS\n", + " - ALL\n", + " - load\n", + "- ACL\n", + " - \n", + " - position\n", + " - permission\n", + " - source_node_id\n", + " - source_port\n", + " - dest_node_id\n", + " - dest_port\n", + " - protocol\n", + "- ICS\n", + "```\n", + "\n", + "### Mappings\n", + "\n", + "The dict keys for `node_id` are in the following order:\n", + "|node_id|node name|\n", + "|--|--|\n", + "|1|domain_controller|\n", + "|2|web_server|\n", + "|3|database_server|\n", + "|4|backup_server|\n", + "|5|security_suite|\n", + "|6|client_1|\n", + "|7|client_2|\n", + "\n", + "Service 1 on node 2 (web_server) corresponds to the Web Server service. Other services are only there for padding to ensure that each node's observation space has the same shape. They are filled with zeroes.\n", + "\n", + "Folder 1 on node 3 corresponds to the database folder. File 1 in that folder corresponds to the database storage file. Other files and folders are only there for padding to ensure that each node's observation space has the same shape. They are filled with zeroes.\n", + "\n", + "The dict keys for `link_id` are in the following order:\n", + "|link_id|endpoint_a|endpoint_b|\n", + "|--|--|--|\n", + "|1|router_1|switch_1|\n", + "|1|router_1|switch_2|\n", + "|1|switch_1|domain_controller|\n", + "|1|switch_1|web_server|\n", + "|1|switch_1|database_server|\n", + "|1|switch_1|backup_server|\n", + "|1|switch_1|security_suite|\n", + "|1|switch_2|client_1|\n", + "|1|switch_2|client_2|\n", + "|1|switch_2|security_suite|\n", + "\n", + "The ACL rules in the observation space appear in the same order that they do in the actual ACL. Though, only the first 10 rules are shown, there are default rules lower down that cannot be changed by the agent. The extra rules just allow the network to function normally, by allowing pings, ARP traffic, etc.\n", + "\n", + "Most nodes have only 1 nic, so the observation for those is placed at NIC index 1 in the observation space. Only the security suite has 2 NICs, the second NIC in the observation space is the one that connects the security suite with swtich_2.\n", + "\n", + "The meaning of the services' operating_state is:\n", + "|operating_state|label|\n", + "|--|--|\n", + "|0|UNUSED|\n", + "|1|RUNNING|\n", + "|2|STOPPED|\n", + "|3|PAUSED|\n", + "|4|DISABLED|\n", + "|5|INSTALLING|\n", + "|6|RESTARTING|\n", + "\n", + "The meaning of the services' health_state is:\n", + "|health_state|label|\n", + "|--|--|\n", + "|0|UNUSED|\n", + "|1|GOOD|\n", + "|2|PATCHING|\n", + "|3|COMPROMISED|\n", + "|4|OVERWHELMED|\n", + "\n", + "The meaning of the files' and folders' health_state is:\n", + "|health_state|label|\n", + "|--|--|\n", + "|0|UNUSED|\n", + "|1|GOOD|\n", + "|2|COMPROMISED|\n", + "|3|CORRUPT|\n", + "|4|RESTORING|\n", + "|5|REPAIRING|\n", + "\n", + "The meaning of the NICs' operating_status is:\n", + "|operating_status|label|\n", + "|--|--|\n", + "|0|UNUSED|\n", + "|1|ENABLED|\n", + "|2|DISABLED|\n", + "\n", + "Link load has the following meaning:\n", + "|load|percent utilisation|\n", + "|--|--|\n", + "|0|exactly 0%|\n", + "|1|0-11%|\n", + "|2|11-22%|\n", + "|3|22-33%|\n", + "|4|33-44%|\n", + "|5|44-55%|\n", + "|6|55-66%|\n", + "|7|66-77%|\n", + "|8|77-88%|\n", + "|9|88-99%|\n", + "|10|exactly 100%|\n", + "\n", + "ACL permission has the following meaning:\n", + "|permission|label|\n", + "|--|--|\n", + "|0|UNUSED|\n", + "|1|ALLOW|\n", + "|2|DENY|\n", + "\n", + "ACL source / destination node ids actually correspond to IP addresses (since ACLs work with IP addresses)\n", + "|source / dest node id|ip_address|label|\n", + "|--|--|--|\n", + "|0| | UNUSED|\n", + "|1| |ALL addresses|\n", + "|2| 192.168.1.10 | domain_controller|\n", + "|3| 192.168.1.12 | web_server \n", + "|4| 192.168.1.14 | database_server|\n", + "|5| 192.168.1.16 | backup_server|\n", + "|6| 192.168.1.110 | security_suite (eth-1)|\n", + "|7| 192.168.10.21 | client_1|\n", + "|8| 192.168.10.22 | client_2|\n", + "|9| 192.168.10.110| security_suite (eth-2)|\n", + "\n", + "ACL source / destination port ids have the following encoding:\n", + "|port id|port number| port use |\n", + "|--|--|--|\n", + "|0||UNUSED|\n", + "|1||ALL|\n", + "|2|219|ARP|\n", + "|3|53|DNS|\n", + "|4|80|HTTP|\n", + "|5|5432|POSTGRES_SERVER|\n", + "\n", + "ACL protocol ids have the following encoding:\n", + "|protocol id|label|\n", + "|--|--|\n", + "|0|UNUSED|\n", + "|1|ALL|\n", + "|2|ICMP|\n", + "|3|TCP|\n", + "|4|UDP|\n", + "\n", + "protocol" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Action Space\n", + "\n", + "The blue agent chooses from a list of 54 pre-defined actions. The full list is defined in the `action_map` in the config. The most important ones are explained here:\n", + "\n", + "- `0`: Do nothing\n", + "- `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", + "- `22`: Block outgoing traffic from client 1\n", + "- `26`: Block TCP traffic from client 1 to the database node\n", + "- `28-37`: Remove ACL rules 1-10\n", + "- `42`: Disconnect client 1 from 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 other actions, and learn about these actions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reward Function\n", + "\n", + "The blue agent's reward is calculated using two 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 the green agent's most recent webpage request was successful (+1 for a `200` return code, -1 for a `404` return code and 0 otherwise).\n", + "These two components are averaged to get the final reward.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Demonstration" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, load the required modules" + ] + }, { "cell_type": "code", "execution_count": 1, "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", - "2023-11-26 23:25:47,985\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", - "2023-11-26 23:25:51,213\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", - "2023-11-26 23:25:51,491\tWARNING __init__.py:10 -- PG has/have been moved to `rllib_contrib` and will no longer be maintained by the RLlib team. You can still use it/them normally inside RLlib util Ray 2.8, but from Ray 2.9 on, all `rllib_contrib` algorithms will no longer be part of the core repo, and will therefore have to be installed separately with pinned dependencies for e.g. ray[rllib] and other packages! See https://github.com/ray-project/ray/tree/master/rllib_contrib#rllib-contrib for more information on the RLlib contrib effort.\n" - ] - } - ], + "outputs": [], "source": [ - "from primaite.session.session import PrimaiteSession\n", - "from primaite.game.game import PrimaiteGame\n", - "from primaite.config.load import example_config_path\n", - "\n", - "from primaite.simulator.system.services.database.database_service import DatabaseService\n", - "\n", - "import yaml" + "%load_ext autoreload\n", + "%autoreload 2" ] }, { @@ -36,61 +339,181 @@ "name": "stderr", "output_type": "stream", "text": [ - "2023-11-26 23:25:51,579::ERROR::primaite.simulator.network.hardware.base::175::NIC a9:92:0a:5e:1b:e4/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,580::ERROR::primaite.simulator.network.hardware.base::175::NIC ef:03:23:af:3c:19/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,581::ERROR::primaite.simulator.network.hardware.base::175::NIC ae:cf:83:2f:94:17/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,582::ERROR::primaite.simulator.network.hardware.base::175::NIC 4c:b2:99:e2:4a:5d/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,583::ERROR::primaite.simulator.network.hardware.base::175::NIC b9:eb:f9:c2:17:2f/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,590::ERROR::primaite.simulator.network.hardware.base::175::NIC cb:df:ca:54:be:01/192.168.1.10 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,595::ERROR::primaite.simulator.network.hardware.base::175::NIC 6e:32:12:da:4d:0d/192.168.1.12 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,600::ERROR::primaite.simulator.network.hardware.base::175::NIC 58:6e:9b:a7:68:49/192.168.1.14 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,604::ERROR::primaite.simulator.network.hardware.base::175::NIC 33:db:a6:40:dd:a3/192.168.1.16 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,608::ERROR::primaite.simulator.network.hardware.base::175::NIC 72:aa:2b:c0:4c:5f/192.168.1.110 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,610::ERROR::primaite.simulator.network.hardware.base::175::NIC 11:d7:0e:90:d9:a4/192.168.10.110 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,614::ERROR::primaite.simulator.network.hardware.base::175::NIC 86:2b:a4:e5:4d:0f/192.168.10.21 cannot be enabled as it is not connected to a Link\n", - "2023-11-26 23:25:51,631::ERROR::primaite.simulator.network.hardware.base::175::NIC af:ad:8f:84:f1:db/192.168.10.22 cannot be enabled as it is not connected to a Link\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "installing DNSServer on node domain_controller\n", - "installing DatabaseClient on node web_server\n", - "installing WebServer on node web_server\n", - "installing DatabaseService on node database_server\n", - "installing FTPClient on node database_server\n", - "installing FTPServer on node backup_server\n", - "installing DNSClient on node client_1\n", - "installing DNSClient on node client_2\n" + "/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-01-25 11:19:29,199\tINFO util.py:159 -- Missing packages: ['ipywidgets']. Run `pip install -U ipywidgets`, then restart the notebook server for rich notebook output.\n", + "2024-01-25 11:19:31,924\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", + "from primaite.session.environment import PrimaiteGymEnv\n", + "from primaite.game.game import PrimaiteGame\n", + "import yaml\n", + "from pprint import pprint\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instantiate the environment. We also disable the agent observation flattening.\n", "\n", - "with open(example_config_path(),'r') as cfgfile:\n", - " cfg = yaml.safe_load(cfgfile)\n", - "game = PrimaiteGame.from_config(cfg)\n", - "net = game.simulation.network\n", - "database_server = net.get_node_by_hostname('database_server')\n", - "web_server = net.get_node_by_hostname('web_server')\n", - "client_1 = net.get_node_by_hostname('client_1')\n", - "\n", - "db_service = database_server.software_manager.software[\"DatabaseService\"]\n", - "db_client = web_server.software_manager.software[\"DatabaseClient\"]\n", - "# db_client.run()\n", - "db_manipulation_bot = client_1.software_manager.software[\"DataManipulationBot\"]\n", - "db_manipulation_bot.port_scan_p_of_success=1.0\n", - "db_manipulation_bot.data_manipulation_p_of_success=1.0\n" + "This cell will print the observation when the network is healthy. You should be able to verify Node file and service statuses against the description above." ] }, { "cell_type": "code", "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': 1}}},\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": [ - "db_client.run()" + "# create the env\n", + "with open(example_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\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", + "obs, info = env.reset()\n", + "print('env created successfully')\n", + "pprint(obs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The red agent will start attacking at some point between step 20 and 30.\n", + "\n", + "The red agent has a random chance of failing its attack, so you may need run the following cell multiple times until the reward goes from 1.0 to -1.0." ] }, { @@ -99,18 +522,53 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 1, Red action: DONOTHING, Blue reward:1.0\n", + "step: 2, Red action: DONOTHING, Blue reward:1.0\n", + "step: 3, Red action: DONOTHING, Blue reward:1.0\n", + "step: 4, Red action: DONOTHING, Blue reward:1.0\n", + "step: 5, Red action: DONOTHING, Blue reward:1.0\n", + "step: 6, Red action: DONOTHING, Blue reward:1.0\n", + "step: 7, Red action: DONOTHING, Blue reward:1.0\n", + "step: 8, Red action: DONOTHING, Blue reward:1.0\n", + "step: 9, Red action: DONOTHING, Blue reward:1.0\n", + "step: 10, Red action: DONOTHING, Blue reward:1.0\n", + "step: 11, Red action: DONOTHING, Blue reward:1.0\n", + "step: 12, Red action: DONOTHING, Blue reward:1.0\n", + "step: 13, Red action: DONOTHING, Blue reward:1.0\n", + "step: 14, Red action: DONOTHING, Blue reward:1.0\n", + "step: 15, Red action: DONOTHING, Blue reward:1.0\n", + "step: 16, Red action: DONOTHING, Blue reward:1.0\n", + "step: 17, Red action: DONOTHING, Blue reward:1.0\n", + "step: 18, Red action: DONOTHING, Blue reward:1.0\n", + "step: 19, Red action: DONOTHING, Blue reward:1.0\n", + "step: 20, Red action: DONOTHING, Blue reward:1.0\n", + "step: 21, Red action: DONOTHING, Blue reward:1.0\n", + "step: 22, Red action: DONOTHING, Blue reward:1.0\n", + "step: 23, Red action: DONOTHING, Blue reward:1.0\n", + "step: 24, Red action: DONOTHING, Blue reward:1.0\n", + "step: 25, Red action: DONOTHING, Blue reward:1.0\n", + "step: 26, Red action: DONOTHING, Blue reward:1.0\n", + "step: 27, Red action: NODE_APPLICATION_EXECUTE, Blue reward:0.0\n", + "step: 28, Red action: DONOTHING, Blue reward:-1.0\n", + "step: 29, Red action: DONOTHING, Blue reward:-1.0\n", + "step: 30, Red action: DONOTHING, Blue reward:-1.0\n" + ] } ], "source": [ - "db_service.backup_database()" + "for step in range(30):\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}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the reward is -1, let's have a look at blue agent's observation." ] }, { @@ -119,27 +577,110 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" + "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": [ - "db_client.query(\"SELECT\")" + "pprint(obs['NODES'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The true statuses of the database file and webapp are not updated. The blue agent needs to perform a scan to see that they have degraded." ] }, { "cell_type": "code", "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': 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": [ - "db_manipulation_bot.run()" + "obs, reward, terminated, truncated, info = env.step(9) # scan database file\n", + "obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n", + "pprint(obs['NODES'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now service 1 on node 2 has `health_status = 3`, indicating that the webapp is compromised.\n", + "File 1 in folder 1 on node 3 has `health_status = 2`, indicating that the database file is compromised." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The blue agent can now patch the database to restore the file to a good health status." ] }, { @@ -148,130 +689,221 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db_client.query(\"SELECT\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db_service.restore_backup()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "db_client.query(\"SELECT\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "db_manipulation_bot.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "client_1.ping(database_server.ethernet_port[1].ip_address)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "from pydantic import validate_call, BaseModel" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "class A(BaseModel):\n", - " x:int\n", - "\n", - " @validate_call\n", - " def increase_x(self, by:int) -> None:\n", - " self.x += 1" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "my_a = A(x=3)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "ename": "ValidationError", - "evalue": "1 validation error for increase_x\n0\n Input should be a valid integer, got a number with a fractional part [type=int_from_float, input_value=3.2, input_type=float]\n For further information visit https://errors.pydantic.dev/2.1/v/int_from_float", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValidationError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/home/cade/repos/PrimAITE/src/primaite/notebooks/uc2_demo.ipynb Cell 15\u001b[0m line \u001b[0;36m1\n\u001b[0;32m----> 1\u001b[0m my_a\u001b[39m.\u001b[39;49mincrease_x(\u001b[39m3.2\u001b[39;49m)\n", - "File \u001b[0;32m~/repos/PrimAITE/venv/lib/python3.10/site-packages/pydantic/_internal/_validate_call.py:91\u001b[0m, in \u001b[0;36mValidateCallWrapper.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 90\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m__call__\u001b[39m(\u001b[39mself\u001b[39m, \u001b[39m*\u001b[39margs: Any, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwargs: Any) \u001b[39m-\u001b[39m\u001b[39m>\u001b[39m Any:\n\u001b[0;32m---> 91\u001b[0m res \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m__pydantic_validator__\u001b[39m.\u001b[39;49mvalidate_python(pydantic_core\u001b[39m.\u001b[39;49mArgsKwargs(args, kwargs))\n\u001b[1;32m 92\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m__return_pydantic_validator__:\n\u001b[1;32m 93\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m__return_pydantic_validator__\u001b[39m.\u001b[39mvalidate_python(res)\n", - "\u001b[0;31mValidationError\u001b[0m: 1 validation error for increase_x\n0\n Input should be a valid integer, got a number with a fractional part [type=int_from_float, input_value=3.2, input_type=float]\n For further information visit https://errors.pydantic.dev/2.1/v/int_from_float" + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 33\n", + "Red action: DONOTHING\n", + "Green action: DONOTHING\n", + "Blue reward:-1.0\n" ] } ], "source": [ - "my_a.increase_x(3.2)" + "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\"Green action: {info['agent_actions']['client_2_green_user'][0]}\" )\n", + "print(f\"Blue reward:{reward}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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", + "\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": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 44\n", + "Red action: DONOTHING\n", + "Green action: NODE_APPLICATION_EXECUTE\n", + "Blue reward:-1.0\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\"Green action: {info['agent_actions']['client_2_green_user'][0]}\" )\n", + "print(f\"Blue reward:{reward}\" )" + ] + }, + { + "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.)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "step: 107, Red action: DONOTHING, Blue reward:1.0\n", + "step: 108, Red action: DONOTHING, Blue reward:1.0\n", + "step: 109, Red action: DONOTHING, Blue reward:1.0\n", + "step: 110, Red action: DONOTHING, Blue reward:1.0\n", + "step: 111, Red action: DONOTHING, Blue reward:1.0\n", + "step: 112, Red action: DONOTHING, Blue reward:1.0\n", + "step: 113, Red action: DONOTHING, Blue reward:1.0\n", + "step: 114, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.0\n", + "step: 115, Red action: DONOTHING, Blue reward:1.0\n", + "step: 116, Red action: DONOTHING, Blue reward:1.0\n", + "step: 117, Red action: DONOTHING, Blue reward:1.0\n", + "step: 118, Red action: DONOTHING, Blue reward:1.0\n", + "step: 119, Red action: DONOTHING, Blue reward:1.0\n", + "step: 120, Red action: DONOTHING, Blue reward:1.0\n", + "step: 121, Red action: DONOTHING, Blue reward:1.0\n", + "step: 122, Red action: DONOTHING, Blue reward:1.0\n", + "step: 123, Red action: DONOTHING, Blue reward:1.0\n", + "step: 124, Red action: DONOTHING, Blue reward:1.0\n", + "step: 125, Red action: DONOTHING, Blue reward:1.0\n", + "step: 126, Red action: DONOTHING, Blue reward:1.0\n", + "step: 127, Red action: DONOTHING, Blue reward:1.0\n", + "step: 128, Red action: DONOTHING, Blue reward:1.0\n", + "step: 129, Red action: DONOTHING, Blue reward:1.0\n", + "step: 130, Red action: DONOTHING, Blue reward:1.0\n", + "step: 131, Red action: DONOTHING, Blue reward:1.0\n", + "step: 132, Red action: DONOTHING, Blue reward:1.0\n", + "step: 133, Red action: DONOTHING, Blue reward:1.0\n", + "step: 134, Red action: NODE_APPLICATION_EXECUTE, Blue reward:1.0\n", + "step: 135, Red action: DONOTHING, Blue reward:1.0\n", + "step: 136, Red action: DONOTHING, Blue reward:1.0\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", + "\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", + "\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}\" )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, even though the red agent executes an attack, the reward stays at 1.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's also have a look at the ACL observation to verify our new ACL rule at position 5." + ] + }, + { + "cell_type": "code", + "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': 0,\n", + " 'source_node_id': 0,\n", + " 'source_port': 0,\n", + " 'dest_node_id': 0,\n", + " 'dest_port': 0,\n", + " 'protocol': 0},\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']" ] }, { diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 6701f183..a3831bc1 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -29,7 +29,7 @@ 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 - self.game.apply_agent_actions() + agent_actions = self.game.apply_agent_actions() self.game.advance_timestep() state = self.game.get_sim_state() @@ -39,7 +39,7 @@ class PrimaiteGymEnv(gymnasium.Env): reward = self.agent.reward_function.current_reward terminated = False truncated = self.game.calculate_truncated() - info = {} + 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) return next_obs, reward, terminated, truncated, info @@ -172,7 +172,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() @@ -186,7 +186,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 = {} + infos = {"agent_actions": agent_actions} terminateds["__all__"] = len(self.terminateds) == len(self.agents) truncateds["__all__"] = self.game.calculate_truncated() if self.game.save_step_metadata: diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 9070f246..e5458670 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -19,7 +19,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: diff --git a/tests/assets/configs/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index e67f6606..767279ce 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -23,7 +23,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: diff --git a/tests/assets/configs/multi_agent_session.yaml b/tests/assets/configs/multi_agent_session.yaml index 220ca21e..6290fa53 100644 --- a/tests/assets/configs/multi_agent_session.yaml +++ b/tests/assets/configs/multi_agent_session.yaml @@ -29,7 +29,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: diff --git a/tests/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index d7e94cb6..89b88475 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -27,7 +27,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: diff --git a/tests/assets/configs/train_only_primaite_session.yaml b/tests/assets/configs/train_only_primaite_session.yaml index b89349c0..b9fa1216 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -23,7 +23,7 @@ game: - UDP agents: - - ref: client_1_green_user + - ref: client_2_green_user team: GREEN type: GreenWebBrowsingAgent observation_space: