From 23fd9c3839288a9839d0dc3327aac769a4c201f1 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Mon, 13 Nov 2023 15:55:14 +0000 Subject: [PATCH 001/107] #1859 - Started giving the red agent some 'intelligence' and a sense of a state. Changed Application.run to .execute. --- src/primaite/game/agent/GATE_agents.py | 8 +- src/primaite/game/agent/interface.py | 2 + src/primaite/game/science.py | 16 +++ src/primaite/game/session.py | 4 +- .../system/applications/application.py | 4 + .../system/applications/database_client.py | 8 +- .../system/applications/web_browser.py | 2 +- .../red_services/data_manipulation_bot.py | 134 +++++++++++++++--- tests/conftest.py | 1 - .../system/test_web_client_server.py | 6 +- 10 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 src/primaite/game/science.py diff --git a/src/primaite/game/agent/GATE_agents.py b/src/primaite/game/agent/GATE_agents.py index e50d7831..e4ee16ca 100644 --- a/src/primaite/game/agent/GATE_agents.py +++ b/src/primaite/game/agent/GATE_agents.py @@ -19,10 +19,10 @@ class GATERLAgent(AbstractGATEAgent): def __init__( self, - agent_name: str | None, - action_space: ActionManager | None, - observation_space: ObservationSpace | None, - reward_function: RewardFunction | None, + agent_name: Optional[str], + action_space: Optional[ActionManager], + observation_space: Optional[ObservationSpace], + reward_function: Optional[RewardFunction], ) -> None: super().__init__(agent_name, action_space, observation_space, reward_function) self.most_recent_action: ActType diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 89f27f3f..78d18a68 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -109,6 +109,8 @@ class RandomAgent(AbstractScriptedAgent): """ return self.action_space.get_action(self.action_space.space.sample()) +class DataManipulationAgent(AbstractScriptedAgent): + pass class AbstractGATEAgent(AbstractAgent): """Base class for actors controlled via external messages, such as RL policies.""" diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py new file mode 100644 index 00000000..f6215127 --- /dev/null +++ b/src/primaite/game/science.py @@ -0,0 +1,16 @@ +from random import random + + +def simulate_trial(p_of_success: float): + """ + Simulates the outcome of a single trial in a Bernoulli process. + + This function returns True with a probability 'p_of_success', simulating a success outcome in a single + trial of a Bernoulli process. When this function is executed multiple times, the set of outcomes follows + a binomial distribution. This is useful in scenarios where one needs to model or simulate events that + have two possible outcomes (success or failure) with a fixed probability of success. + + :param p_of_success: The probability of success in a single trial, ranging from 0 to 1. + :returns: True if the trial is successful (with probability 'p_of_success'); otherwise, False. + """ + return random() < p_of_success diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index d40d0754..9c2bb6b7 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -60,7 +60,7 @@ class PrimaiteGATEClient(GATEClient): return self.parent_session.training_options.rl_algorithm @property - def seed(self) -> int | None: + def seed(self) -> Optional[int]: """The seed to use for the environment's random number generator.""" return self.parent_session.training_options.seed @@ -115,7 +115,7 @@ class PrimaiteGATEClient(GATEClient): info = {} return obs, rew, term, trunc, info - def reset(self, *, seed: int | None = None, options: dict[str, Any] | None = None) -> Tuple[ObsType, Dict]: + def reset(self, *, seed: Optional[int] = None, options: Optional[Dict[str, Any]] = None) -> Tuple[ObsType, Dict]: """Reset the environment. This method is called when the environment is initialized and at the end of each episode. diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index db323cf6..7f79ac2b 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -65,6 +65,10 @@ class Application(IOSoftware): self.sys_log.info(f"Running Application {self.name}") self.operating_state = ApplicationOperatingState.RUNNING + def _application_loop(self): + """THe main application loop.""" + pass + def close(self) -> None: """Close the Application.""" if self.operating_state == ApplicationOperatingState.RUNNING: diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index d021cb78..28e826fd 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -128,11 +128,11 @@ class DatabaseClient(Application): ) return self._query(sql=sql, query_id=query_id, is_reattempt=True) - def run(self) -> None: + def execute(self) -> None: """Run the DatabaseClient.""" - super().run() - self.operating_state = ApplicationOperatingState.RUNNING - self.connect() + super().execute() + if self.operating_state == ApplicationOperatingState.RUNNING: + self.connect() def query(self, sql: str) -> bool: """ diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ea9c3ac3..6799358d 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -30,7 +30,7 @@ class WebBrowser(Application): kwargs["port"] = Port.HTTP super().__init__(**kwargs) - self.run() + self.execute() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 996e6790..aec7bbd8 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -1,27 +1,46 @@ +from enum import IntEnum from ipaddress import IPv4Address from typing import Optional +from primaite.game.science import simulate_trial +from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient +class DataManipulationAttackStage(IntEnum): + """ + Enumeration representing different stages of a data manipulation attack. + + This enumeration defines the various stages a data manipulation attack can be in during its lifecycle in the + simulation. Each stage represents a specific phase in the attack process. + """ + NOT_STARTED = 0 + "Indicates that the attack has not started yet." + LOGON = 1 + "The stage where logon procedures are simulated." + PORT_SCAN = 2 + "Represents the stage of performing a horizontal port scan on the target." + ATTACKING = 3 + "Stage of actively attacking the target." + COMPLETE = 4 + "Indicates the attack has been successfully completed." + FAILED = 5 + "Signifies that the attack has failed." + + class DataManipulationBot(DatabaseClient): - """ - Red Agent Data Integration Service. - - The Service represents a bot that causes files/folders in the File System to - become corrupted. - """ - + """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 + attack_stage: DataManipulationAttackStage = DataManipulationAttackStage.NOT_STARTED def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "DataManipulationBot" def configure( - self, server_ip_address: IPv4Address, server_password: Optional[str] = None, payload: Optional[str] = None + self, server_ip_address: IPv4Address, server_password: Optional[str] = None, payload: Optional[str] = None ): """ Configure the DataManipulatorBot to communicate with a DatabaseService. @@ -37,15 +56,92 @@ class DataManipulationBot(DatabaseClient): f"{self.name}: Configured the {self.name} with {server_ip_address=}, {payload=}, {server_password=}." ) - def run(self): - """Run the DataManipulationBot.""" - if self.server_ip_address and self.payload: - self.sys_log.info(f"{self.name}: Attempting to start the {self.name}") - super().run() - if not self.connected: - self.connect() - if self.connected: - self.query(self.payload) - self.sys_log.info(f"{self.name} payload delivered: {self.payload}") + def _logon(self): + """ + Simulate the logon process as the initial stage of the attack. + + Advances the attack stage to `LOGON` if successful. + """ + if self.attack_stage == DataManipulationAttackStage.NOT_STARTED: + # Bypass this stage as we're not dealing with logon for now + self.sys_log.info(f"{self.name}: ") + self.attack_stage = DataManipulationAttackStage.LOGON + + def _perform_port_scan(self, p_of_success: Optional[float] = 0.1): + """ + Perform a simulated port scan to check for open SQL ports. + + Advances the attack stage to `PORT_SCAN` if successful. + + :param p_of_success: Probability of successful port scan, by default 0.1. + """ + if self.attack_stage == DataManipulationAttackStage.LOGON: + # perform a port scan to identify that the SQL port is open on the server + if simulate_trial(p_of_success): + self.sys_log.info(f"{self.name}: Performing port scan") + # perform the port scan + port_is_open = True # Temporary; later we can implement NMAP port scan. + if port_is_open: + self.sys_log.info(f"{self.name}: ") + self.attack_stage = DataManipulationAttackStage.PORT_SCAN + + def _perform_data_manipulation(self, p_of_success: Optional[float] = 0.1): + """ + Execute the data manipulation attack on the target. + + Advances the attack stage to `COMPLETE` if successful, or 'FAILED' if unsuccessful. + + :param p_of_success: Probability of successfully performing data manipulation, by default 0.1. + """ + 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 port scan") + # perform the attack + if not self.connected: + self.connect() + if self.connected: + self.query(self.payload) + self.sys_log.info(f"{self.name} payload delivered: {self.payload}") + attack_successful = True + if attack_successful: + self.sys_log.info(f"{self.name}: Performing port scan") + self.attack_stage = DataManipulationAttackStage.COMPLETE + else: + self.sys_log.info(f"{self.name}: Performing port scan") + self.attack_stage = DataManipulationAttackStage.FAILED + + def execute(self): + """ + Execute the Data Manipulation Bot + + Calls the parent classes execute method before starting the application loop. + """ + super().execute() + self._application_loop() + + def _application_loop(self): + """ + 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 self.operating_state != ApplicationOperatingState.RUNNING: + return + if self.server_ip_address and self.payload and self.operating_state: + self.sys_log.info(f"{self.name}: Running") + self._logon() + self._perform_port_scan() + self._perform_data_manipulation() else: - self.sys_log.error(f"Failed to start the {self.name} as it requires both a target_ip_address and payload.") + self.sys_log.error(f"{self.name}: Failed to start as it requires both a target_ip_address and payload.") + + def apply_timestep(self, timestep: int) -> None: + """ + Apply a timestep to the bot, triggering the application loop. + + :param timestep: The timestep value to update the bot's state. + """ + self._application_loop() diff --git a/tests/conftest.py b/tests/conftest.py index dc749cfc..c046ca0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ from pathlib import Path from typing import Any, Dict, Union from unittest.mock import patch -import nodeenv import pytest from primaite import getLogger diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f4546cbf..e36cff2b 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -10,7 +10,7 @@ def test_web_page_home_page(uc2_network): """Test to see if the browser is able to open the main page of the web server.""" client_1: Computer = uc2_network.get_node_by_hostname("client_1") web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() + web_client.execute() assert web_client.operating_state == ApplicationOperatingState.RUNNING assert web_client.get_webpage("http://arcd.com/") is True @@ -24,7 +24,7 @@ def test_web_page_get_users_page_request_with_domain_name(uc2_network): """Test to see if the client can handle requests with domain names""" client_1: Computer = uc2_network.get_node_by_hostname("client_1") web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() + web_client.execute() assert web_client.operating_state == ApplicationOperatingState.RUNNING assert web_client.get_webpage("http://arcd.com/users/") is True @@ -38,7 +38,7 @@ def test_web_page_get_users_page_request_with_ip_address(uc2_network): """Test to see if the client can handle requests that use ip_address.""" client_1: Computer = uc2_network.get_node_by_hostname("client_1") web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() + web_client.execute() web_server: Server = uc2_network.get_node_by_hostname("web_server") web_server_ip = web_server.nics.get(next(iter(web_server.nics))).ip_address From 1c5ff66d26599834f75c7cbc402fdf4f05a041c8 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Thu, 16 Nov 2023 13:26:30 +0000 Subject: [PATCH 002/107] Pass execution definition from config to agent --- .../config/_package_data/example_config.yaml | 4 ++++ src/primaite/game/agent/interface.py | 15 +++++++++++++-- src/primaite/game/session.py | 9 +++++++-- .../simulator/system/applications/web_browser.py | 3 +++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index ee42cf4f..f034f9ea 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -58,6 +58,10 @@ game_config: team: RED type: RedDatabaseCorruptingAgent + execution_definition: + port_scan_p_of_success: 0.1 + data_manipulation_p_of_success: 0.1 + observation_space: type: UC2RedObservation options: diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 78d18a68..d04b298e 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, TypeAlias, Union import numpy as np +from pydantic import BaseModel from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations import ObservationSpace @@ -11,6 +12,11 @@ from primaite.game.agent.rewards import RewardFunction ObsType: TypeAlias = Union[Dict, np.ndarray] +class AgentExecutionDefinition(BaseModel): + port_scan_p_of_success: float = 0.1 + data_manipulation_p_of_success: float = 0.1 + + class AbstractAgent(ABC): """Base class for scripted and RL agents.""" @@ -20,6 +26,7 @@ class AbstractAgent(ABC): action_space: Optional[ActionManager], observation_space: Optional[ObservationSpace], reward_function: Optional[RewardFunction], + execution_definition: Optional[AgentExecutionDefinition] ) -> None: """ Initialize an agent. @@ -40,7 +47,7 @@ class AbstractAgent(ABC): # exection definiton converts CAOS action to Primaite simulator request, sometimes having to enrich the info # by for example specifying target ip addresses, or converting a node ID into a uuid - self.execution_definition = None + self.execution_definition = execution_definition or AgentExecutionDefinition() def convert_state_to_obs(self, state: Dict) -> ObsType: """ @@ -110,7 +117,11 @@ class RandomAgent(AbstractScriptedAgent): return self.action_space.get_action(self.action_space.space.sample()) class DataManipulationAgent(AbstractScriptedAgent): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: + return self.action_space.get_action(self.action_space.space.sample()) class AbstractGATEAgent(AbstractAgent): """Base class for actors controlled via external messages, such as RL policies.""" diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index 9c2bb6b7..082ed281 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -10,7 +10,7 @@ from pydantic import BaseModel from primaite import getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, RandomAgent +from primaite.game.agent.interface import AbstractAgent, RandomAgent, DataManipulationAgent, AgentExecutionDefinition from primaite.game.agent.observations import ObservationSpace from primaite.game.agent.rewards import RewardFunction from primaite.simulator.network.hardware.base import Link, NIC, Node @@ -438,6 +438,8 @@ class PrimaiteSession: # CREATE REWARD FUNCTION rew_function = RewardFunction.from_config(reward_function_cfg, session=sess) + execution_definition = AgentExecutionDefinition(**agent_cfg.get("execution_definition", {})) + # CREATE AGENT if agent_type == "GreenWebBrowsingAgent": # TODO: implement non-random agents and fix this parsing @@ -446,6 +448,7 @@ class PrimaiteSession: action_space=action_space, observation_space=obs_space, reward_function=rew_function, + execution_definition=execution_definition, ) sess.agents.append(new_agent) elif agent_type == "GATERLAgent": @@ -454,15 +457,17 @@ class PrimaiteSession: action_space=action_space, observation_space=obs_space, reward_function=rew_function, + execution_definition=execution_definition, ) sess.agents.append(new_agent) sess.rl_agent = new_agent elif agent_type == "RedDatabaseCorruptingAgent": - new_agent = RandomAgent( + new_agent = DataManipulationAgent( agent_name=agent_cfg["ref"], action_space=action_space, observation_space=obs_space, reward_function=rew_function, + execution_definition=execution_definition, ) sess.agents.append(new_agent) else: diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 6799358d..964e1ce4 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -135,3 +135,6 @@ class WebBrowser(Application): self.sys_log.info(f"{self.name}: Received HTTP {payload.status_code.value}") self.latest_response = payload return True + + def execute(self): + pass From 227e73602f8468523da60e8fe983622959d9ae92 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 17 Nov 2023 11:51:19 +0000 Subject: [PATCH 003/107] Pass execution definition from config to agent --- src/primaite/game/agent/interface.py | 37 ++++++++++++++++++- src/primaite/game/session.py | 2 +- .../red_services/data_manipulation_bot.py | 10 +++-- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index d04b298e..c591c554 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, TypeAlias, Union +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING, TypeAlias, Union import numpy as np from pydantic import BaseModel @@ -8,13 +8,21 @@ from pydantic import BaseModel from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations import ObservationSpace from primaite.game.agent.rewards import RewardFunction +from primaite.simulator.network.hardware.base import Node + +if TYPE_CHECKING: + from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot ObsType: TypeAlias = Union[Dict, np.ndarray] class AgentExecutionDefinition(BaseModel): + """Additional configuration for agents.""" + port_scan_p_of_success: float = 0.1 + "The probability of a port scan succeeding." data_manipulation_p_of_success: float = 0.1 + "The probability of data manipulation succeeding." class AbstractAgent(ABC): @@ -26,7 +34,7 @@ class AbstractAgent(ABC): action_space: Optional[ActionManager], observation_space: Optional[ObservationSpace], reward_function: Optional[RewardFunction], - execution_definition: Optional[AgentExecutionDefinition] + execution_definition: Optional[AgentExecutionDefinition], ) -> None: """ Initialize an agent. @@ -116,13 +124,38 @@ class RandomAgent(AbstractScriptedAgent): """ return self.action_space.get_action(self.action_space.space.sample()) + class DataManipulationAgent(AbstractScriptedAgent): + """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # get node ids that are part of the agent's observation space + node_ids: List[str] = [n.where[-1] for n in self.observation_space.obs.nodes] + # get all nodes from their ids + nodes: List[Node] = [n for n_id, n in self.action_space.sim.network.nodes.items() if n_id in node_ids] + + # get execution definition for data manipulation bots + for node in nodes: + bot_sw: Optional["DataManipulationBot"] = node.software_manager.software.get("DataManipulationBot") + + if bot_sw is not None: + bot_sw.execution_definition = self.execution_definition + def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: + """Randomly sample an action from the action space. + + :param obs: _description_ + :type obs: ObsType + :param reward: _description_, defaults to None + :type reward: float, optional + :return: _description_ + :rtype: Tuple[str, Dict] + """ return self.action_space.get_action(self.action_space.space.sample()) + class AbstractGATEAgent(AbstractAgent): """Base class for actors controlled via external messages, such as RL policies.""" diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index 082ed281..5f3fb7b9 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -10,7 +10,7 @@ from pydantic import BaseModel from primaite import getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, RandomAgent, DataManipulationAgent, AgentExecutionDefinition +from primaite.game.agent.interface import AbstractAgent, AgentExecutionDefinition, DataManipulationAgent, RandomAgent from primaite.game.agent.observations import ObservationSpace from primaite.game.agent.rewards import RewardFunction from primaite.simulator.network.hardware.base import Link, NIC, Node diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index aec7bbd8..35ea413a 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -2,6 +2,7 @@ from enum import IntEnum from ipaddress import IPv4Address from typing import Optional +from primaite.game.agent.interface import AgentExecutionDefinition from primaite.game.science import simulate_trial from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient @@ -14,6 +15,7 @@ class DataManipulationAttackStage(IntEnum): This enumeration defines the various stages a data manipulation attack can be in during its lifecycle in the simulation. Each stage represents a specific phase in the attack process. """ + NOT_STARTED = 0 "Indicates that the attack has not started yet." LOGON = 1 @@ -30,17 +32,19 @@ class DataManipulationAttackStage(IntEnum): class DataManipulationBot(DatabaseClient): """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 attack_stage: DataManipulationAttackStage = DataManipulationAttackStage.NOT_STARTED + execution_definition: AgentExecutionDefinition = AgentExecutionDefinition() def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "DataManipulationBot" def configure( - self, server_ip_address: IPv4Address, server_password: Optional[str] = None, payload: Optional[str] = None + self, server_ip_address: IPv4Address, server_password: Optional[str] = None, payload: Optional[str] = None ): """ Configure the DataManipulatorBot to communicate with a DatabaseService. @@ -96,7 +100,6 @@ class DataManipulationBot(DatabaseClient): 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 port scan") # perform the attack if not self.connected: @@ -114,7 +117,7 @@ class DataManipulationBot(DatabaseClient): def execute(self): """ - Execute the Data Manipulation Bot + Execute the Data Manipulation Bot. Calls the parent classes execute method before starting the application loop. """ @@ -127,7 +130,6 @@ class DataManipulationBot(DatabaseClient): This is the core loop where the bot sequentially goes through the stages of the attack. """ - if self.operating_state != ApplicationOperatingState.RUNNING: return if self.server_ip_address and self.payload and self.operating_state: From 7e0e8a476817118005307fa129f025a00cad0360 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Mon, 20 Nov 2023 10:38:01 +0000 Subject: [PATCH 004/107] Pass agent settings from config to agent --- .../config/_package_data/example_config.yaml | 14 +++++++------ src/primaite/game/agent/interface.py | 21 +++++++++++++++++++ src/primaite/game/session.py | 12 ++++++++++- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index f034f9ea..700a45fd 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -50,9 +50,10 @@ game_config: - type: DUMMY agent_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_settings: + start_step: 5 + frequency: 4 + variance: 3 - ref: client_1_data_manipulation_red_bot team: RED @@ -106,9 +107,10 @@ game_config: - type: DUMMY agent_settings: # options specific to this particular agent type, basically args of __init__(self) - start_step: 25 - frequency: 20 - variance: 5 + start_settings: + start_step: 25 + frequency: 20 + variance: 5 - ref: defender team: BLUE diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index c591c554..70eb1980 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -25,6 +25,24 @@ class AgentExecutionDefinition(BaseModel): "The probability of data manipulation succeeding." +class AgentStartSettings(BaseModel): + """Configuration values for when an agent starts performing actions.""" + + start_step: int = 5 + "The timestep at which an agent begins performing it's actions" + frequency: int = 5 + "The number of timesteps to wait between performing actions" + variance: int = 0 + "The amount the frequency can randomly change to" + + +class AgentSettings(BaseModel): + """Settings for configuring the operation of an agent.""" + + start_settings: Optional[AgentStartSettings] = None + "Configuration for when an agent begins performing it's actions" + + class AbstractAgent(ABC): """Base class for scripted and RL agents.""" @@ -35,6 +53,7 @@ class AbstractAgent(ABC): observation_space: Optional[ObservationSpace], reward_function: Optional[RewardFunction], execution_definition: Optional[AgentExecutionDefinition], + agent_settings: Optional[AgentSettings], ) -> None: """ Initialize an agent. @@ -57,6 +76,8 @@ class AbstractAgent(ABC): # by for example specifying target ip addresses, or converting a node ID into a uuid self.execution_definition = execution_definition or AgentExecutionDefinition() + self.agent_settings = agent_settings or AgentSettings() + def convert_state_to_obs(self, state: Dict) -> ObsType: """ Convert a state from the simulator into an observation for the agent using the observation space. diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index 5f3fb7b9..9701fec9 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -10,7 +10,13 @@ from pydantic import BaseModel from primaite import getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, AgentExecutionDefinition, DataManipulationAgent, RandomAgent +from primaite.game.agent.interface import ( + AbstractAgent, + AgentExecutionDefinition, + AgentSettings, + DataManipulationAgent, + RandomAgent, +) from primaite.game.agent.observations import ObservationSpace from primaite.game.agent.rewards import RewardFunction from primaite.simulator.network.hardware.base import Link, NIC, Node @@ -439,6 +445,7 @@ class PrimaiteSession: rew_function = RewardFunction.from_config(reward_function_cfg, session=sess) execution_definition = AgentExecutionDefinition(**agent_cfg.get("execution_definition", {})) + agent_settings = AgentSettings(**agent_cfg.get("agent_settings", {})) # CREATE AGENT if agent_type == "GreenWebBrowsingAgent": @@ -449,6 +456,7 @@ class PrimaiteSession: observation_space=obs_space, reward_function=rew_function, execution_definition=execution_definition, + agent_settings=agent_settings, ) sess.agents.append(new_agent) elif agent_type == "GATERLAgent": @@ -458,6 +466,7 @@ class PrimaiteSession: observation_space=obs_space, reward_function=rew_function, execution_definition=execution_definition, + agent_settings=agent_settings, ) sess.agents.append(new_agent) sess.rl_agent = new_agent @@ -468,6 +477,7 @@ class PrimaiteSession: observation_space=obs_space, reward_function=rew_function, execution_definition=execution_definition, + agent_settings=agent_settings, ) sess.agents.append(new_agent) else: From 2975aa882774c3b5979072646de64c243ab880b4 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Tue, 21 Nov 2023 11:42:01 +0000 Subject: [PATCH 005/107] Execute data manipulation bots from agent --- src/primaite/game/agent/interface.py | 38 ++++++++++++++++++- src/primaite/game/session.py | 4 +- .../system/applications/database_client.py | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 70eb1980..94878947 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -24,6 +24,20 @@ class AgentExecutionDefinition(BaseModel): data_manipulation_p_of_success: float = 0.1 "The probability of data manipulation succeeding." + @classmethod + def from_config(cls, config: Optional[Dict]) -> "AgentExecutionDefinition": + """Construct an AgentExecutionDefinition from a config dictionary. + + :param config: A dict of options for the execution definition. + :type config: Dict + :return: The execution definition. + :rtype: AgentExecutionDefinition + """ + if config is None: + return cls() + + return cls(**config) + class AgentStartSettings(BaseModel): """Configuration values for when an agent starts performing actions.""" @@ -42,6 +56,20 @@ class AgentSettings(BaseModel): start_settings: Optional[AgentStartSettings] = None "Configuration for when an agent begins performing it's actions" + @classmethod + def from_config(cls, config: Optional[Dict]) -> "AgentSettings": + """Construct agent settings from a config dictionary. + + :param config: A dict of options for the agent settings. + :type config: Dict + :return: The agent settings. + :rtype: AgentSettings + """ + if config is None: + return cls() + + return cls(**config) + class AbstractAgent(ABC): """Base class for scripted and RL agents.""" @@ -149,6 +177,8 @@ class RandomAgent(AbstractScriptedAgent): class DataManipulationAgent(AbstractScriptedAgent): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" + data_manipulation_bots: List["DataManipulationBot"] = [] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -163,6 +193,7 @@ class DataManipulationAgent(AbstractScriptedAgent): if bot_sw is not None: bot_sw.execution_definition = self.execution_definition + self.data_manipulation_bots.append(bot_sw) def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: """Randomly sample an action from the action space. @@ -174,7 +205,12 @@ class DataManipulationAgent(AbstractScriptedAgent): :return: _description_ :rtype: Tuple[str, Dict] """ - return self.action_space.get_action(self.action_space.space.sample()) + # TODO: Move this to the appropriate place + # return self.action_space.get_action(self.action_space.space.sample()) + for bot in self.data_manipulation_bots: + bot.execute() + + return ("DONOTHING", {"dummy": 0}) class AbstractGATEAgent(AbstractAgent): diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index 9701fec9..1b086c35 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -444,8 +444,8 @@ class PrimaiteSession: # CREATE REWARD FUNCTION rew_function = RewardFunction.from_config(reward_function_cfg, session=sess) - execution_definition = AgentExecutionDefinition(**agent_cfg.get("execution_definition", {})) - agent_settings = AgentSettings(**agent_cfg.get("agent_settings", {})) + execution_definition = AgentExecutionDefinition.from_config(agent_cfg.get("execution_definition")) + agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) # CREATE AGENT if agent_type == "GreenWebBrowsingAgent": diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 28e826fd..e15249e3 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -130,7 +130,7 @@ class DatabaseClient(Application): def execute(self) -> None: """Run the DatabaseClient.""" - super().execute() + # super().execute() if self.operating_state == ApplicationOperatingState.RUNNING: self.connect() From d8154bbebd4e6d98aaf6cf51628be1b176ad00b8 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Tue, 21 Nov 2023 11:43:47 +0000 Subject: [PATCH 006/107] Add tests for data manipulation bot attack stages --- .../test_data_manipulation_bot.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py index dd785cc1..5127254c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py @@ -1,20 +1,73 @@ from ipaddress import IPv4Address +import pytest + from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot +from primaite.simulator.system.services.red_services.data_manipulation_bot import ( + DataManipulationAttackStage, + DataManipulationBot, +) -def test_creation(): +@pytest.fixture(scope="function") +def dm_client() -> Node: network = arcd_uc2_network() + return network.get_node_by_hostname("client_1") - client_1: Node = network.get_node_by_hostname("client_1") - data_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] +@pytest.fixture +def dm_bot(dm_client) -> DataManipulationBot: + return dm_client.software_manager.software["DataManipulationBot"] + + +def test_create_dm_bot(dm_client): + data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software["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.payload == "DROP TABLE IF EXISTS user;" + + +def test_dm_bot_logon(dm_bot): + dm_bot.attack_stage = DataManipulationAttackStage.NOT_STARTED + + dm_bot._logon() + + assert dm_bot.attack_stage == DataManipulationAttackStage.LOGON + + +def test_dm_bot_perform_port_scan_no_success(dm_bot): + dm_bot.attack_stage = DataManipulationAttackStage.LOGON + + dm_bot._perform_port_scan(p_of_success=0.0) + + assert dm_bot.attack_stage == DataManipulationAttackStage.LOGON + + +def test_dm_bot_perform_port_scan_success(dm_bot): + dm_bot.attack_stage = DataManipulationAttackStage.LOGON + + dm_bot._perform_port_scan(p_of_success=1.0) + + assert dm_bot.attack_stage == DataManipulationAttackStage.PORT_SCAN + + +def test_dm_bot_perform_data_manipulation_no_success(dm_bot): + dm_bot.attack_stage = DataManipulationAttackStage.PORT_SCAN + + dm_bot._perform_data_manipulation(p_of_success=0.0) + + assert dm_bot.attack_stage == DataManipulationAttackStage.PORT_SCAN + + +def test_dm_bot_perform_data_manipulation_success(dm_bot): + dm_bot.attack_stage = DataManipulationAttackStage.PORT_SCAN + + dm_bot._perform_data_manipulation(p_of_success=1.0) + + assert dm_bot.attack_stage in (DataManipulationAttackStage.COMPLETE, DataManipulationAttackStage.FAILED) + assert dm_bot.connected From 48af0229637726c9fc953ecf54b2329947151a1a Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Tue, 21 Nov 2023 13:41:38 +0000 Subject: [PATCH 007/107] Run agent at configured timesteps --- src/primaite/game/agent/interface.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 94878947..d2479b38 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,4 +1,5 @@ """Interface for agents.""" +import random from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, TYPE_CHECKING, TypeAlias, Union @@ -178,10 +179,13 @@ class DataManipulationAgent(AbstractScriptedAgent): """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" data_manipulation_bots: List["DataManipulationBot"] = [] + next_execution_timestep: int = 0 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.next_execution_timestep = self.agent_settings.start_settings.start_step + # get node ids that are part of the agent's observation space node_ids: List[str] = [n.where[-1] for n in self.observation_space.obs.nodes] # get all nodes from their ids @@ -207,10 +211,19 @@ class DataManipulationAgent(AbstractScriptedAgent): """ # TODO: Move this to the appropriate place # return self.action_space.get_action(self.action_space.space.sample()) + + timestep = self.action_space.session.step_counter + + if timestep < self.next_execution_timestep: + return "DONOTHING", {"dummy": 0} + + var = random.randint(-self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance) + self.next_execution_timestep = timestep + self.agent_settings.start_settings.frequency + var + for bot in self.data_manipulation_bots: bot.execute() - return ("DONOTHING", {"dummy": 0}) + return "DONOTHING", {"dummy": 0} class AbstractGATEAgent(AbstractAgent): From aa65c53a95ad33b356a588ed054b9e0a0dfaf3cc Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Tue, 21 Nov 2023 15:09:51 +0000 Subject: [PATCH 008/107] Pass probability of success through to functions --- .../system/services/red_services/data_manipulation_bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 35ea413a..5e4e2d3f 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -135,8 +135,8 @@ class DataManipulationBot(DatabaseClient): if self.server_ip_address and self.payload and self.operating_state: self.sys_log.info(f"{self.name}: Running") self._logon() - self._perform_port_scan() - self._perform_data_manipulation() + self._perform_port_scan(p_of_success=self.execution_definition.port_scan_p_of_success) + self._perform_data_manipulation(p_of_success=self.execution_definition.data_manipulation_p_of_success) else: self.sys_log.error(f"{self.name}: Failed to start as it requires both a target_ip_address and payload.") From 061e5081871a7f9143769b545ca6de8a44f8c158 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Wed, 22 Nov 2023 16:24:17 +0000 Subject: [PATCH 009/107] Add repeat option to data manipulation bot --- .../red_services/data_manipulation_bot.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 5e4e2d3f..eae3f0e3 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -38,13 +38,19 @@ class DataManipulationBot(DatabaseClient): server_password: Optional[str] = None attack_stage: DataManipulationAttackStage = DataManipulationAttackStage.NOT_STARTED execution_definition: AgentExecutionDefinition = AgentExecutionDefinition() + repeat: bool = False + "Whether to repeat attacking once finished." def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "DataManipulationBot" def configure( - self, server_ip_address: IPv4Address, server_password: Optional[str] = None, payload: Optional[str] = None + self, + server_ip_address: IPv4Address, + server_password: Optional[str] = None, + payload: Optional[str] = None, + repeat: bool = False, ): """ Configure the DataManipulatorBot to communicate with a DatabaseService. @@ -52,12 +58,15 @@ class DataManipulationBot(DatabaseClient): :param server_ip_address: The IP address of the Node the DatabaseService is on. :param server_password: The password on the DatabaseService. :param payload: The data manipulation query payload. + :param repeat: Whether to repeat attacking once finished. """ self.server_ip_address = server_ip_address self.payload = payload self.server_password = server_password + self.repeat = repeat self.sys_log.info( - f"{self.name}: Configured the {self.name} with {server_ip_address=}, {payload=}, {server_password=}." + f"{self.name}: Configured the {self.name} with {server_ip_address=}, {payload=}, {server_password=}, " + f"{repeat=}." ) def _logon(self): @@ -100,7 +109,7 @@ class DataManipulationBot(DatabaseClient): 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 port scan") + self.sys_log.info(f"{self.name}: Performing data manipulation") # perform the attack if not self.connected: self.connect() @@ -109,10 +118,10 @@ class DataManipulationBot(DatabaseClient): self.sys_log.info(f"{self.name} payload delivered: {self.payload}") attack_successful = True if attack_successful: - self.sys_log.info(f"{self.name}: Performing port scan") + self.sys_log.info(f"{self.name}: Data manipulation successful") self.attack_stage = DataManipulationAttackStage.COMPLETE else: - self.sys_log.info(f"{self.name}: Performing port scan") + self.sys_log.info(f"{self.name}: Data manipulation failed") self.attack_stage = DataManipulationAttackStage.FAILED def execute(self): @@ -137,6 +146,12 @@ class DataManipulationBot(DatabaseClient): self._logon() self._perform_port_scan(p_of_success=self.execution_definition.port_scan_p_of_success) self._perform_data_manipulation(p_of_success=self.execution_definition.data_manipulation_p_of_success) + + if self.repeat and self.attack_stage in ( + DataManipulationAttackStage.COMPLETE, + DataManipulationAttackStage.FAILED, + ): + self.attack_stage = DataManipulationAttackStage.NOT_STARTED else: self.sys_log.error(f"{self.name}: Failed to start as it requires both a target_ip_address and payload.") From c93705867f28d2b68e3e98888a5de1cb424bc890 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Thu, 23 Nov 2023 15:53:47 +0000 Subject: [PATCH 010/107] Move configuration from agent to data manipulation bot --- .../config/_package_data/example_config.yaml | 14 +++--- src/primaite/game/agent/interface.py | 43 ------------------- src/primaite/game/session.py | 22 +++++----- .../red_services/data_manipulation_bot.py | 11 ++++- 4 files changed, 25 insertions(+), 65 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 700a45fd..274da7aa 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -59,10 +59,6 @@ game_config: team: RED type: RedDatabaseCorruptingAgent - execution_definition: - port_scan_p_of_success: 0.1 - data_manipulation_p_of_success: 0.1 - observation_space: type: UC2RedObservation options: @@ -83,11 +79,6 @@ game_config: - type: DONOTHING # "AgentExecutionDefinition": - """Construct an AgentExecutionDefinition from a config dictionary. - - :param config: A dict of options for the execution definition. - :type config: Dict - :return: The execution definition. - :rtype: AgentExecutionDefinition - """ - if config is None: - return cls() - - return cls(**config) - - class AgentStartSettings(BaseModel): """Configuration values for when an agent starts performing actions.""" @@ -81,7 +57,6 @@ class AbstractAgent(ABC): action_space: Optional[ActionManager], observation_space: Optional[ObservationSpace], reward_function: Optional[RewardFunction], - execution_definition: Optional[AgentExecutionDefinition], agent_settings: Optional[AgentSettings], ) -> None: """ @@ -100,11 +75,6 @@ class AbstractAgent(ABC): self.action_space: Optional[ActionManager] = action_space self.observation_space: Optional[ObservationSpace] = observation_space self.reward_function: Optional[RewardFunction] = reward_function - - # exection definiton converts CAOS action to Primaite simulator request, sometimes having to enrich the info - # by for example specifying target ip addresses, or converting a node ID into a uuid - self.execution_definition = execution_definition or AgentExecutionDefinition() - self.agent_settings = agent_settings or AgentSettings() def convert_state_to_obs(self, state: Dict) -> ObsType: @@ -186,19 +156,6 @@ class DataManipulationAgent(AbstractScriptedAgent): self.next_execution_timestep = self.agent_settings.start_settings.start_step - # get node ids that are part of the agent's observation space - node_ids: List[str] = [n.where[-1] for n in self.observation_space.obs.nodes] - # get all nodes from their ids - nodes: List[Node] = [n for n_id, n in self.action_space.sim.network.nodes.items() if n_id in node_ids] - - # get execution definition for data manipulation bots - for node in nodes: - bot_sw: Optional["DataManipulationBot"] = node.software_manager.software.get("DataManipulationBot") - - if bot_sw is not None: - bot_sw.execution_definition = self.execution_definition - self.data_manipulation_bots.append(bot_sw) - def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: """Randomly sample an action from the action space. diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index 1b086c35..f675e33c 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -10,13 +10,7 @@ from pydantic import BaseModel from primaite import getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import ( - AbstractAgent, - AgentExecutionDefinition, - AgentSettings, - DataManipulationAgent, - RandomAgent, -) +from primaite.game.agent.interface import AbstractAgent, AgentSettings, DataManipulationAgent, RandomAgent from primaite.game.agent.observations import ObservationSpace from primaite.game.agent.rewards import RewardFunction from primaite.simulator.network.hardware.base import Link, NIC, Node @@ -366,6 +360,16 @@ class PrimaiteSession: if "domain_mapping" in opt: for domain, ip in opt["domain_mapping"].items(): new_service.dns_register(domain, ip) + if service_type == "DataManipulationBot": + if "options" in service_cfg: + opt = service_cfg["options"] + new_service.configure( + server_ip_address=opt.get("server_ip"), + payload=opt.get("payload"), + 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")), + ) + if "applications" in node_cfg: for application_cfg in node_cfg["applications"]: application_ref = application_cfg["ref"] @@ -444,7 +448,6 @@ class PrimaiteSession: # CREATE REWARD FUNCTION rew_function = RewardFunction.from_config(reward_function_cfg, session=sess) - execution_definition = AgentExecutionDefinition.from_config(agent_cfg.get("execution_definition")) agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) # CREATE AGENT @@ -455,7 +458,6 @@ class PrimaiteSession: action_space=action_space, observation_space=obs_space, reward_function=rew_function, - execution_definition=execution_definition, agent_settings=agent_settings, ) sess.agents.append(new_agent) @@ -465,7 +467,6 @@ class PrimaiteSession: action_space=action_space, observation_space=obs_space, reward_function=rew_function, - execution_definition=execution_definition, agent_settings=agent_settings, ) sess.agents.append(new_agent) @@ -476,7 +477,6 @@ class PrimaiteSession: action_space=action_space, observation_space=obs_space, reward_function=rew_function, - execution_definition=execution_definition, agent_settings=agent_settings, ) sess.agents.append(new_agent) diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index eae3f0e3..e3f5b95d 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -2,7 +2,6 @@ from enum import IntEnum from ipaddress import IPv4Address from typing import Optional -from primaite.game.agent.interface import AgentExecutionDefinition from primaite.game.science import simulate_trial from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient @@ -36,8 +35,10 @@ class DataManipulationBot(DatabaseClient): 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 + attack_stage: DataManipulationAttackStage = DataManipulationAttackStage.NOT_STARTED - execution_definition: AgentExecutionDefinition = AgentExecutionDefinition() repeat: bool = False "Whether to repeat attacking once finished." @@ -50,6 +51,8 @@ class DataManipulationBot(DatabaseClient): server_ip_address: IPv4Address, server_password: Optional[str] = None, payload: Optional[str] = None, + port_scan_p_of_success: float = 0.1, + data_manipulation_p_of_success: float = 0.1, repeat: bool = False, ): """ @@ -58,11 +61,15 @@ class DataManipulationBot(DatabaseClient): :param server_ip_address: The IP address of the Node the DatabaseService is on. :param server_password: The password on the DatabaseService. :param payload: The data manipulation query payload. + :param port_scan_p_of_success: The probability of success for the port scan stage. + :param data_manipulation_p_of_success: The probability of success for the data manipulation stage. :param repeat: Whether to repeat attacking once finished. """ self.server_ip_address = server_ip_address self.payload = payload self.server_password = server_password + 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 self.sys_log.info( f"{self.name}: Configured the {self.name} with {server_ip_address=}, {payload=}, {server_password=}, " From 5f1a5af1b45eccf5154f35e0143282dc3491e089 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Thu, 23 Nov 2023 16:06:19 +0000 Subject: [PATCH 011/107] Add data manipulation bot action manager --- .../config/_package_data/example_config.yaml | 8 +-- src/primaite/game/agent/actions.py | 49 +++++++++++++++++++ src/primaite/game/agent/interface.py | 27 +++++----- .../red_services/data_manipulation_bot.py | 8 +++ .../test_data_manipulation_bot.py | 2 - 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 274da7aa..aff54d62 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -78,12 +78,12 @@ game_config: action_list: - type: DONOTHING # None: + super().__init__(manager=manager) + self.shape: Dict[str, int] = {"node_id": num_nodes, "application_id": num_applications} + self.verb: str + + def form_request(self, node_id: int, application_id: int) -> List[str]: + """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" + node_uuid = self.manager.get_node_uuid_by_idx(node_id) + application_uuid = self.manager.get_application_uuid_by_idx(node_id, application_id) + if node_uuid is None or application_uuid is None: + return ["do_nothing"] + return ["network", "node", node_uuid, "application", application_uuid, self.verb] + + +class NodeApplicationExecuteAction(NodeApplicationAbstractAction): + """Action which executes an application.""" + + def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: + super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications) + self.verb = "execute" + + class NodeFolderAbstractAction(AbstractAction): """ Base class for folder actions. @@ -536,6 +567,7 @@ class ActionManager: "NODE_SERVICE_RESTART": NodeServiceRestartAction, "NODE_SERVICE_DISABLE": NodeServiceDisableAction, "NODE_SERVICE_ENABLE": NodeServiceEnableAction, + "NODE_APPLICATION_EXECUTE": NodeApplicationExecuteAction, "NODE_FILE_SCAN": NodeFileScanAction, "NODE_FILE_CHECKHASH": NodeFileCheckhashAction, "NODE_FILE_DELETE": NodeFileDeleteAction, @@ -565,6 +597,7 @@ class ActionManager: max_folders_per_node: int = 2, # allows calculating shape max_files_per_folder: int = 2, # allows calculating shape max_services_per_node: int = 2, # allows calculating shape + max_applications_per_node: int = 10, # allows calculating shape max_nics_per_node: int = 8, # allows calculating shape max_acl_rules: int = 10, # allows calculating shape protocols: List[str] = ["TCP", "UDP", "ICMP"], # allow mapping index to protocol @@ -622,6 +655,7 @@ class ActionManager: "num_folders": max_folders_per_node, "num_files": max_files_per_folder, "num_services": max_services_per_node, + "num_applications": max_applications_per_node, "num_nics": max_nics_per_node, "num_acl_rules": max_acl_rules, "num_protocols": len(self.protocols), @@ -775,6 +809,21 @@ class ActionManager: service_uuids = list(node.services.keys()) return service_uuids[service_idx] if len(service_uuids) > service_idx else None + def get_application_uuid_by_idx(self, node_idx: int, application_idx: int) -> Optional[str]: + """Get the application UUID corresponding to the given node and service indices. + + :param node_idx: The index of the node. + :type node_idx: int + :param application_idx: The index of the service on the node. + :type application_idx: int + :return: The UUID of the service. Or None if the node has fewer services than the given index. + :rtype: Optional[str] + """ + node_uuid = self.get_node_uuid_by_idx(node_idx) + node = self.sim.network.nodes[node_uuid] + application_uuids = list(node.applications.keys()) + return application_uuids[application_idx] if len(application_uuids) > application_idx else None + def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: """Get the internet protocol corresponding to the given index. diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 5e73a423..33932df2 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -154,7 +154,17 @@ class DataManipulationAgent(AbstractScriptedAgent): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.next_execution_timestep = self.agent_settings.start_settings.start_step + self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) + + def _set_next_execution_timestep(self, timestep: int) -> None: + """Set the next execution timestep with a configured random variance. + + :param timestep: The timestep to add variance to. + """ + random_timestep_increment = random.randint( + -self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance + ) + self.next_execution_timestep = timestep + random_timestep_increment def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: """Randomly sample an action from the action space. @@ -166,21 +176,14 @@ class DataManipulationAgent(AbstractScriptedAgent): :return: _description_ :rtype: Tuple[str, Dict] """ - # TODO: Move this to the appropriate place - # return self.action_space.get_action(self.action_space.space.sample()) + current_timestep = self.action_space.session.step_counter - timestep = self.action_space.session.step_counter - - if timestep < self.next_execution_timestep: + if current_timestep < self.next_execution_timestep: return "DONOTHING", {"dummy": 0} - var = random.randint(-self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance) - self.next_execution_timestep = timestep + self.agent_settings.start_settings.frequency + var + self._set_next_execution_timestep(current_timestep + self.agent_settings.start_settings.frequency) - for bot in self.data_manipulation_bots: - bot.execute() - - return "DONOTHING", {"dummy": 0} + return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} class AbstractGATEAgent(AbstractAgent): diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index e3f5b95d..f4b31cb1 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address from typing import Optional from primaite.game.science import simulate_trial +from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient @@ -46,6 +47,13 @@ class DataManipulationBot(DatabaseClient): super().__init__(**kwargs) self.name = "DataManipulationBot" + def _init_request_manager(self) -> RequestManager: + rm = super()._init_request_manager() + + rm.add_request(name="execute", request_type=RequestType(func=self.execute)) + + return rm + def configure( self, server_ip_address: IPv4Address, diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py index 5127254c..04e23e84 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py @@ -1,5 +1,3 @@ -from ipaddress import IPv4Address - import pytest from primaite.simulator.network.hardware.base import Node From 47112aafcf0b9a207440febf649ed39ec2f43bbf Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 23 Nov 2023 16:19:39 +0000 Subject: [PATCH 012/107] #2068: Removed references to ARCD GATE --- CHANGELOG.md | 2 +- README.md | 2 -- docs/source/about.rst | 1 - docs/source/custom_agent.rst | 2 +- docs/source/game_layer.rst | 8 ++++---- docs/source/getting_started.rst | 30 +----------------------------- 6 files changed, 7 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af5c14c..a2044858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ SessionManager. ### Removed - Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol` -- Removed legacy training modules, they are replaced by the new ARCD GATE dependency +- Removed legacy training modules - Removed tests for legacy code diff --git a/README.md b/README.md index 7fc41681..ec335108 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,6 @@ PrimAITE presents the following features: - Routers with traffic routing and firewall capabilities -- Integration with ARCD GATE for agent training - - Support for multiple agents, each having their own customisable observation space, action space, and reward function definition, and either deterministic or RL-directed behaviour ## Getting Started with PrimAITE diff --git a/docs/source/about.rst b/docs/source/about.rst index 993dec0c..56c8b551 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -18,7 +18,6 @@ PrimAITE provides the following features: * Highly configurable network hosts, including definition of software, file system, and network interfaces, * Realistic network traffic simulation, including address and sending packets via internet protocols like TCP, UDP, ICMP, etc. * Routers with traffic routing and firewall capabilities -* Interfaces with ARCD GATE to allow training of agents * Simulation of customisable deterministic agents * Support for multiple agents, each having their own customisable observation space, action space, and reward function definition. diff --git a/docs/source/custom_agent.rst b/docs/source/custom_agent.rst index 0a08ae74..7a9d83c1 100644 --- a/docs/source/custom_agent.rst +++ b/docs/source/custom_agent.rst @@ -11,4 +11,4 @@ Integrating a user defined blue agent .. note:: - PrimAITE uses ARCD GATE for agent integration. In order to use a custom agent with PrimAITE, you must integrate it with ARCD GATE. Please look at the ARCD GATE documentation for more information. + TBA diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 27905c85..18b42e7b 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -4,9 +4,9 @@ PrimAITE Game layer 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, including ARCD GATE. +* ``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. -These two components have been decoupled to allow the agent training code in ARCD GATE to be reused with other simulators. The simulator and game layer communicate using the PrimAITE State API and the PrimAITE Request API. The game layer communicates with ARCD gate using the `Farama Gymnasium Spaces API `_. + 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. @@ -20,13 +20,13 @@ The game layer is responsible for managing agents and getting them to interface 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. It also sends messages to ARCD GATE to perform reinforcement learning. ``PrimaiteSession`` keeps track of multiple agents of different types. +``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: -* RL agents action during each step is decided by an RL algorithm which lives inside of ARCD GATE. The agent within PrimAITE just acts to format and forward actions decided by an RL policy. +* 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. .. diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index aebabf66..a800ee56 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -87,22 +87,7 @@ Install PrimAITE pip install path\to\your\primaite.whl - -5. Install ARCD GATE from wheel file - - -.. code-block:: bash - :caption: Unix - - pip install path/to/your/arcd_gate-0.1.0-py3-none-any.whl - -.. code-block:: powershell - :caption: Windows (Powershell) - - pip install path\to\your\arcd_gate-0.1.0-py3-none-any.whl - - -6. Perform the PrimAITE setup +5. Perform the PrimAITE setup .. code-block:: bash :caption: Unix @@ -153,17 +138,4 @@ of your choice: pip install -e .[dev] - -4. Install ARCD GATE from wheel file - -.. code-block:: bash - :caption: Unix - - pip install GATE/arcd_gate-0.1.0-py3-none-any.whl - -.. code-block:: powershell - :caption: Windows (Powershell) - - pip install GATE\arcd_gate-0.1.0-py3-none-any.whl - To view the complete list of packages installed during PrimAITE installation, go to the dependencies page (:ref:`Dependencies`). From 3894a9615d7ee856a7e9c946565f8700f1acfd3a Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 23 Nov 2023 17:42:26 +0000 Subject: [PATCH 013/107] #2068: Replace refs to OpenAI Gym with Gymnasium --- docs/index.rst | 10 +++++----- docs/source/about.rst | 5 +++-- docs/source/glossary.rst | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index fa877064..2dfc8a65 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,7 +30,7 @@ PrimAITE incorporates the following features: - Uses the concept of Information Exchange Requirements (IERs) to model background pattern of life and adversarial behaviour; - An Access Control List (ACL) function, mimicking the behaviour of a network firewall, is applied across the model, following standard ACL rule format (e.g. DENY/ALLOW, source IP address, destination IP address, protocol and port); - Application of traffic to the links of the platform / system laydown adheres to the ACL ruleset; -- Presents both an OpenAI gym and Ray RLLib interface to the environment, allowing integration with any compliant defensive agents; +- Presents both a Gymnasium and Ray RLLib interface to the environment, allowing integration with any compliant defensive agents; - Allows for the saving and loading of trained defensive agents; - Stochastic adversarial agent behaviour; - Full capture of discrete logs relating to agent training or evaluation (system state, agent actions taken, instantaneous and average reward for every step of every episode); @@ -40,18 +40,18 @@ PrimAITE incorporates the following features: Architecture ^^^^^^^^^^^^ -PrimAITE is a Python application and is therefore Operating System agnostic. The OpenAI gym and Ray RLLib frameworks are employed to provide an interface and source for AI agents. Configuration of PrimAITE is achieved via included YAML files which support full control over the platform / system laydown being modelled, background pattern of life, adversarial (red agent) behaviour, and step and episode count. NetworkX based nodes and links host Python classes to present attributes and methods, and hence a more representative platform / system can be modelled within the simulation. +PrimAITE is a Python application and is therefore Operating System agnostic. The Gymnasium and Ray RLLib frameworks are employed to provide an interface and source for AI agents. Configuration of PrimAITE is achieved via included YAML files which support full control over the platform / system laydown being modelled, background pattern of life, adversarial (red agent) behaviour, and step and episode count. NetworkX based nodes and links host Python classes to present attributes and methods, and hence a more representative platform / system can be modelled within the simulation. Training & Evaluation Capability ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -PrimAITE provides a training and evaluation capability to AI agents in the context of cyber-attack, via its OpenAI Gym and RLLib compliant interface. Scenarios can be constructed to reflect platform / system laydowns consisting of any configuration of nodes (e.g. PCs, servers, switches etc.) and network links between them. All nodes can be configured to model services (and their status) and the traffic loading between them over the network links. Traffic loading is broken down into a per service granularity, relating directly to a protocol (e.g. Service A would be configured as a TCP service, and TCP traffic then flows between instances of Service A under the direction of a tailored IER). Highlights of PrimAITE’s training and evaluation capability are: +PrimAITE provides a training and evaluation capability to AI agents in the context of cyber-attack, via its Gymnasium and RLLib compliant interface. Scenarios can be constructed to reflect platform / system laydowns consisting of any configuration of nodes (e.g. PCs, servers, switches etc.) and network links between them. All nodes can be configured to model services (and their status) and the traffic loading between them over the network links. Traffic loading is broken down into a per service granularity, relating directly to a protocol (e.g. Service A would be configured as a TCP service, and TCP traffic then flows between instances of Service A under the direction of a tailored IER). Highlights of PrimAITE’s training and evaluation capability are: - The scenario is not bound to a representation of any platform, system, or technology; - Fully configurable (network / system laydown, IERs, node pattern-of-life, ACL, number of episodes, steps per episode) and repeatable to suit the requirements of AI agents; -- Can integrate with any OpenAI Gym or RLLib compliant AI agent. +- Can integrate with any Gymnasium or RLLib compliant AI agent. Use of PrimAITE default scenarios within ARCD is supported by a “Use Case Profile” tailored to the scenario. @@ -75,7 +75,7 @@ Logs are available in CSV format and provide coverage of the above data for ever What is PrimAITE built with --------------------------- -* `OpenAI's Gym `_ is used as the basis for AI blue agent interaction with the PrimAITE environment +* `Gymnasium `_ is used as the basis for AI blue agent interaction with the PrimAITE environment * `Networkx `_ is used as the underlying data structure used for the PrimAITE environment * `Stable Baselines 3 `_ is used as a default source of RL algorithms (although PrimAITE is not limited to SB3 agents) * `Ray RLlib `_ is used as an additional source of RL algorithms diff --git a/docs/source/about.rst b/docs/source/about.rst index 56c8b551..32b54eee 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -18,6 +18,7 @@ PrimAITE provides the following features: * Highly configurable network hosts, including definition of software, file system, and network interfaces, * Realistic network traffic simulation, including address and sending packets via internet protocols like TCP, UDP, ICMP, etc. * Routers with traffic routing and firewall capabilities +* Interfaces with ARCD GATE to allow training of agents * Simulation of customisable deterministic agents * Support for multiple agents, each having their own customisable observation space, action space, and reward function definition. @@ -147,7 +148,7 @@ The game layer is built on top of the simulator and it consumes the simulation a Observation Spaces ****************** The observation space provides the blue agent with information about the current status of nodes and links. - PrimAITE builds on top of Gym Spaces to create an observation space that is easily configurable for users. It's made up of components which are managed by the :py:class:`primaite.environment.observations.ObservationsHandler`. Each training scenario can define its own observation space, and the user can choose which information to inlude, and how it should be formatted. + PrimAITE builds on top of Gymnasium Spaces to create an observation space that is easily configurable for users. It's made up of components which are managed by the :py:class:`primaite.environment.observations.ObservationsHandler`. Each training scenario can define its own observation space, and the user can choose which information to inlude, and how it should be formatted. NodeLinkTable component ----------------------- For example, the :py:class:`primaite.environment.observations.NodeLinkTable` component represents the status of nodes and links as a ``gym.spaces.Box`` with an example format shown below: @@ -278,7 +279,7 @@ The game layer is built on top of the simulator and it consumes the simulation a 3. Any (Agent can take both node-based and ACL-based actions) The choice of action space used during a training session is determined in the config_[name].yaml file. **Node-Based** - The agent is able to influence the status of nodes by switching them off, resetting, or patching operating systems and services. In this instance, the action space is an OpenAI Gym spaces.Discrete type, as follows: + The agent is able to influence the status of nodes by switching them off, resetting, or patching operating systems and services. In this instance, the action space is an Gymnasium spaces.Discrete type, as follows: * Dictionary item {... ,1: [x1, x2, x3,x4] ...} The placeholders inside the list under the key '1' mean the following: * [0, num nodes] - Node ID (0 = nothing, node ID) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 8340d559..4c0869f2 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -74,8 +74,8 @@ Glossary Laydown The laydown is a file which defines the training scenario. It contains the network topology, firewall rules, services, protocols, and details about green and red agent behaviours. - Gym - PrimAITE uses the Gym reinforcement learning framework API to create a training environment and interface with RL agents. Gym defines a common way of creating observations, actions, and rewards. + Gymnasium + PrimAITE uses the Gymnasium reinforcement learning framework API to create a training environment and interface with RL agents. Gymnasium defines a common way of creating observations, actions, and rewards. User app home PrimAITE supports upgrading software version while retaining user data. The user data directory is where configs, notebooks, and results are stored, this location is `~/primaite` on linux/darwin and `C:\Users\\primaite\` on Windows. From 3dfd7a2e14149e525dae930f5bde51dc82ba3a89 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Thu, 23 Nov 2023 17:57:51 +0000 Subject: [PATCH 014/107] #2068: Fix malformed Windows path --- docs/source/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 4c0869f2..67fd7aaa 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -78,4 +78,4 @@ Glossary PrimAITE uses the Gymnasium reinforcement learning framework API to create a training environment and interface with RL agents. Gymnasium defines a common way of creating observations, actions, and rewards. User app home - PrimAITE supports upgrading software version while retaining user data. The user data directory is where configs, notebooks, and results are stored, this location is `~/primaite` on linux/darwin and `C:\Users\\primaite\` on Windows. + PrimAITE supports upgrading software version while retaining user data. The user data directory is where configs, notebooks, and results are stored, this location is `~/primaite` on linux/darwin and `C:\\Users\\\\primaite\\` on Windows. From bd6c27244c349940a0ec8fa21ca7845786071301 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 23 Nov 2023 19:49:03 +0000 Subject: [PATCH 015/107] #2064: Edited services and applications to handle when they are shut down --- src/primaite/simulator/network/container.py | 11 +++++ .../simulator/network/hardware/base.py | 40 +++++++++++------- .../network/hardware/node_operating_state.py | 14 +++++++ .../simulator/network/protocols/ftp.py | 3 ++ .../system/applications/web_browser.py | 21 ++++++++-- .../system/services/ftp/ftp_client.py | 8 ++-- .../system/services/ftp/ftp_server.py | 3 ++ .../simulator/system/services/service.py | 23 +++++++++- .../system/services/web_server/web_server.py | 3 ++ src/primaite/simulator/system/software.py | 6 ++- tests/conftest.py | 7 ++++ .../system/test_ftp_client_server.py | 37 ++++++++++++++++ .../system/test_service_on_node.py | 42 +++++++++++++++++++ .../system/test_web_client_server.py | 30 ++++++++++++- .../_simulator/_system/_services/test_ftp.py | 14 +++++-- 15 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 src/primaite/simulator/network/hardware/node_operating_state.py create mode 100644 tests/integration_tests/system/test_service_on_node.py diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 9fbafc29..a356549a 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -52,6 +52,17 @@ class Network(SimComponent): ) return rm + def apply_timestep(self, timestep: int) -> None: + """Apply a timestep evolution to this the network and its nodes and links.""" + super().apply_timestep(timestep=timestep) + # apply timestep to nodes + for node_id in self.nodes: + self.nodes[node_id].apply_timestep(timestep=timestep) + + # apply timestep to links + for link_id in self.links: + self.links[link_id].apply_timestep(timestep=timestep) + @property def routers(self) -> List[Router]: """The Routers in the Network.""" diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 29d3a05c..ebf669eb 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -2,7 +2,6 @@ from __future__ import annotations import re import secrets -from enum import Enum from ipaddress import IPv4Address, IPv4Network from pathlib import Path from typing import Any, Dict, Literal, Optional, Tuple, Union @@ -15,6 +14,7 @@ from primaite.simulator import SIM_OUTPUT 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.protocols.arp import ARPEntry, ARPPacket from primaite.simulator.network.transmission.data_link_layer import EthernetHeader, Frame from primaite.simulator.network.transmission.network_layer import ICMPPacket, ICMPType, IPPacket, IPProtocol @@ -856,19 +856,6 @@ class ICMP: return sequence, icmp_packet.identifier -class NodeOperatingState(Enum): - """Enumeration of Node Operating States.""" - - ON = 1 - "The node is powered on." - OFF = 2 - "The node is powered off." - BOOTING = 3 - "The node is in the process of booting up." - SHUTTING_DOWN = 4 - "The node is in the process of shutting down." - - class Node(SimComponent): """ A basic Node class that represents a node on the network. @@ -1090,18 +1077,21 @@ class Node(SimComponent): else: if self.operating_state == NodeOperatingState.BOOTING: self.operating_state = NodeOperatingState.ON - self.sys_log.info("Turned on") + self.sys_log.info(f"{self.hostname}: Turned on") for nic in self.nics.values(): if nic._connected_link: nic.enable() + self._start_up_actions() + # count down to shut down if self.shut_down_countdown > 0: self.shut_down_countdown -= 1 else: if self.operating_state == NodeOperatingState.SHUTTING_DOWN: self.operating_state = NodeOperatingState.OFF - self.sys_log.info("Turned off") + self.sys_log.info(f"{self.hostname}: Turned off") + self._shut_down_actions() # if resetting turn back on if self.is_resetting: @@ -1418,6 +1408,24 @@ class Node(SimComponent): _LOGGER.info(f"Removed application {application.uuid} from node {self.uuid}") self._application_request_manager.remove_request(application.uuid) + def _shut_down_actions(self): + """Actions to perform when the node is shut down.""" + # Turn off all the services in the node + for service_id in self.services: + self.services[service_id].stop() + + # Turn off all the applications in the node + for app_id in self.applications: + self.applications[app_id].close() + + # Turn off all processes in the node + # for process_id in self.processes: + # self.processes[process_id] + + def _start_up_actions(self): + """Actions to perform when the node is starting up.""" + pass + def __contains__(self, item: Any) -> bool: if isinstance(item, Service): return item.uuid in self.services diff --git a/src/primaite/simulator/network/hardware/node_operating_state.py b/src/primaite/simulator/network/hardware/node_operating_state.py new file mode 100644 index 00000000..1fd1225f --- /dev/null +++ b/src/primaite/simulator/network/hardware/node_operating_state.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class NodeOperatingState(Enum): + """Enumeration of Node Operating States.""" + + ON = 1 + "The node is powered on." + OFF = 2 + "The node is powered off." + BOOTING = 3 + "The node is in the process of booting up." + SHUTTING_DOWN = 4 + "The node is in the process of shutting down." diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py index 9ecc7df8..0fd3fe43 100644 --- a/src/primaite/simulator/network/protocols/ftp.py +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -35,6 +35,9 @@ class FTPCommand(Enum): class FTPStatusCode(Enum): """Status code of the current FTP request.""" + NOT_FOUND = 14 + """Destination not found.""" + OK = 200 """Command successful.""" diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ea9c3ac3..bb9552d8 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -2,7 +2,12 @@ from ipaddress import IPv4Address from typing import Dict, Optional from urllib.parse import urlparse -from primaite.simulator.network.protocols.http import HttpRequestMethod, HttpRequestPacket, HttpResponsePacket +from primaite.simulator.network.protocols.http import ( + HttpRequestMethod, + HttpRequestPacket, + HttpResponsePacket, + HttpStatusCode, +) from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application @@ -61,7 +66,7 @@ class WebBrowser(Application): :type: url: str """ # reset latest response - self.latest_response = None + self.latest_response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND) try: parsed_url = urlparse(url) @@ -91,11 +96,19 @@ class WebBrowser(Application): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url=url) # send request - return self.send( + if self.send( payload=payload, dest_ip_address=self.domain_name_ip_address, dest_port=parsed_url.port if parsed_url.port else Port.HTTP, - ) + ): + self.sys_log.info( + f"{self.name}: Received HTTP {payload.request_method.name} " + f"Response {payload.request_url} - {self.latest_response.status_code.value}" + ) + return self.latest_response.status_code is HttpStatusCode.OK + else: + self.sys_log.error(f"Error sending Http Packet {str(payload)}") + return False def send( self, diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 3e286da1..649b9b50 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -72,10 +72,7 @@ class FTPClient(FTPServiceABC): # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now # create FTP packet - payload: FTPPacket = FTPPacket( - ftp_command=FTPCommand.PORT, - ftp_command_args=Port.FTP, - ) + payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.PORT, ftp_command_args=Port.FTP) if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id): if payload.status_code == FTPStatusCode.OK: @@ -271,7 +268,10 @@ class FTPClient(FTPServiceABC): the same node. """ if payload.status_code is None: + self.sys_log.error(f"FTP Server could not be found - Error Code: {payload.status_code.value}") return False + self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}") + self._process_ftp_command(payload=payload, session_id=session_id) return True diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 23414601..bc21dec3 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -89,5 +89,8 @@ class FTPServer(FTPServiceABC): if payload.status_code is not None: return False + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id) return True diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index e2b04c15..3a1a4c9d 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -1,8 +1,9 @@ from enum import Enum -from typing import Dict, Optional +from typing import Any, Dict, Optional from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.system.software import IOSoftware, SoftwareHealthState _LOGGER = getLogger(__name__) @@ -40,6 +41,21 @@ class Service(IOSoftware): restart_countdown: Optional[int] = None "If currently restarting, how many timesteps remain until the restart is finished." + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: + """ + Receives a payload from the SessionManager. + + The specifics of how the payload is processed and whether a response payload + is generated should be implemented in subclasses. + + + :param payload: The payload to receive. + :param session_id: The identifier of the session that the payload is associated with. + :param kwargs: Additional keyword arguments specific to the implementation. + :return: True if the payload was successfully received and processed, False otherwise. + """ + return super().receive(payload=payload, session_id=session_id, **kwargs) + def __init__(self, **kwargs): super().__init__(**kwargs) @@ -91,6 +107,11 @@ class Service(IOSoftware): def start(self, **kwargs) -> None: """Start the service.""" + # cant start the service if the node it is on is off + if self.software_manager and self.software_manager.node.operating_state is not NodeOperatingState.ON: + self.sys_log.error(f"Unable to start service. {self.software_manager.node.hostname} is not turned on.") + return + if self.operating_state == ServiceOperatingState.STOPPED: self.sys_log.info(f"Starting service {self.name}") self.operating_state = ServiceOperatingState.RUNNING 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 cb1a4738..76176cd8 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -160,4 +160,7 @@ class WebServer(Service): self.sys_log.error("Payload is not an HTTPPacket") return False + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + return self._process_http_request(payload=payload, session_id=session_id) diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index f2627557..c29bec20 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Optional from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.file_system.file_system import FileSystem, Folder +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.session_manager import Session from primaite.simulator.system.core.sys_log import SysLog @@ -261,4 +262,7 @@ class IOSoftware(Software): :param kwargs: Additional keyword arguments specific to the implementation. :return: True if the payload was successfully received and processed, False otherwise. """ - pass + # return false if node that software is on is off + if self.software_manager and self.software_manager.node.operating_state is NodeOperatingState.OFF: + return False + return True diff --git a/tests/conftest.py b/tests/conftest.py index 6a65b12f..4cc36e6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ from primaite.game.session import PrimaiteSession # from primaite.primaite_session import PrimaiteSession from primaite.simulator.network.container import Network from primaite.simulator.network.networks import arcd_uc2_network +from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.sys_log import SysLog @@ -38,6 +39,12 @@ from primaite.simulator.network.hardware.base import Node class TestService(Service): """Test Service class""" + def __init__(self, **kwargs): + kwargs["name"] = "TestService" + kwargs["port"] = Port.HTTP + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: pass diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index 48dc2960..d8968b2d 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -60,3 +60,40 @@ def test_ftp_client_retrieve_file_from_server(uc2_network): # client should have retrieved the file assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") + + +def test_ftp_client_tries_to_connect_to_offline_server(uc2_network): + """Test checks to make sure that the client can't do anything when the server is offline.""" + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + backup_server: Server = uc2_network.get_node_by_hostname("backup_server") + + ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] + ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp server + ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + + backup_server.power_off() + + for i in range(backup_server.shut_down_duration + 1): + uc2_network.apply_timestep(timestep=i) + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.STOPPED + + assert ( + ftp_client.request_file( + src_folder_name="file_share", + src_file_name="test_file.txt", + dest_folder_name="downloads", + dest_file_name="test_file.txt", + dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + ) + is False + ) + + # client should have retrieved the file + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") is None diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py new file mode 100644 index 00000000..e596dcd8 --- /dev/null +++ b/tests/integration_tests/system/test_service_on_node.py @@ -0,0 +1,42 @@ +from typing import Tuple + +import pytest +from conftest import TestService + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.system.services.service import Service, ServiceOperatingState + + +@pytest.fixture(scope="function") +def service_on_node() -> Tuple[Server, Service]: + server = Server( + hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON + ) + server.software_manager.install(TestService) + + service = server.software_manager.software["TestService"] + service.start() + + return server, service + + +def test_server_turns_off_service(service_on_node): + """Check that the service is turned off when the server is turned off""" + server, service = service_on_node + + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING + + server.power_off() + + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) + + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + + +def test_server_turns_on_service(service_on_node): + """Check that turning on the server turns on service.""" + pass diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f4546cbf..f3995c84 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -1,3 +1,4 @@ +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.http import HttpStatusCode @@ -47,6 +48,33 @@ def test_web_page_get_users_page_request_with_ip_address(uc2_network): assert web_client.get_webpage(f"http://{web_server_ip}/users/") is True - # latest reponse should have status code 200 + # latest response should have status code 200 assert web_client.latest_response is not None assert web_client.latest_response.status_code == HttpStatusCode.OK + + +def test_web_page_request_from_shut_down_server(uc2_network): + """Test to see that the web server does not respond when the server is off.""" + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] + web_client.run() + + web_server: Server = uc2_network.get_node_by_hostname("web_server") + + assert web_client.operating_state == ApplicationOperatingState.RUNNING + + assert web_client.get_webpage("http://arcd.com/users/") is True + + # latest response should have status code 200 + assert web_client.latest_response.status_code == HttpStatusCode.OK + + web_server.power_off() + + for i in range(web_server.shut_down_duration + 1): + uc2_network.apply_timestep(timestep=i) + + # node should be off + assert web_server.operating_state is NodeOperatingState.OFF + + assert web_client.get_webpage("http://arcd.com/users/") is False + assert web_client.latest_response.status_code == HttpStatusCode.NOT_FOUND diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py index d382b8dd..9957b6f6 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode @@ -15,17 +16,24 @@ from primaite.simulator.system.services.ftp.ftp_server import FTPServer @pytest.fixture(scope="function") def ftp_server() -> Node: node = Server( - hostname="ftp_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + hostname="ftp_server", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) node.software_manager.install(software_class=FTPServer) - node.software_manager.software["FTPServer"].start() return node @pytest.fixture(scope="function") def ftp_client() -> Node: node = Computer( - hostname="ftp_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + hostname="ftp_client", + ip_address="192.168.1.11", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) return node From f0fc6518a0edbb1685825acc5173393b626f8a73 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 23 Nov 2023 21:48:11 +0000 Subject: [PATCH 016/107] #2064: add handling of offline service to dns, ftp and database --- .../services/database/database_service.py | 3 ++ .../system/services/dns/dns_server.py | 4 ++ .../system/services/ftp/ftp_server.py | 4 +- .../system/services/web_server/web_server.py | 6 +-- src/primaite/simulator/system/software.py | 3 +- .../system/test_database_on_node.py | 30 ++++++++++++++- .../system/test_dns_client_server.py | 37 +++++++++++++++++++ .../_simulator/_system/_services/test_dns.py | 8 +++- 8 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index d7277e1e..e3adb8e1 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -173,6 +173,9 @@ class DatabaseService(Service): :param session_id: The session identifier. :return: True if the Status Code is 200, otherwise False. """ + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + result = {"status_code": 500, "data": []} if isinstance(payload, dict) and payload.get("type"): if payload["type"] == "connect_request": diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 90a350c8..2c8f3003 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -88,10 +88,14 @@ class DNSServer(Service): :return: True if DNS request returns a valid IP, otherwise, False """ + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + # The payload should be a DNS packet if not isinstance(payload, DNSPacket): _LOGGER.debug(f"{payload} is not a DNSPacket") return False + # cast payload into a DNS packet payload: DNSPacket = payload if payload.dns_request is not None: diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index bc21dec3..cd128339 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -86,10 +86,10 @@ class FTPServer(FTPServiceABC): prevents an FTP request loop - FTP client and servers can exist on the same node. """ - if payload.status_code is not None: + if not super().receive(payload=payload, session_id=session_id, **kwargs): return False - if not super().receive(payload=payload, session_id=session_id, **kwargs): + if payload.status_code is not None: return False self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id) 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 76176cd8..63df2f7d 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -155,12 +155,12 @@ class WebServer(Service): :param: payload: The payload to send. :param: session_id: The id of the session. Optional. """ + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + # check if the payload is an HTTPPacket if not isinstance(payload, HttpRequestPacket): self.sys_log.error("Payload is not an HTTPPacket") return False - if not super().receive(payload=payload, session_id=session_id, **kwargs): - return False - return self._process_http_request(payload=payload, session_id=session_id) diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index c29bec20..830e3d79 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -3,7 +3,7 @@ from enum import Enum from ipaddress import IPv4Address from typing import Any, Dict, Optional -from primaite.simulator.core import RequestManager, RequestType, SimComponent +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 from primaite.simulator.network.transmission.transport_layer import Port @@ -264,5 +264,6 @@ class IOSoftware(Software): """ # return false if node that software is on is off if self.software_manager and self.software_manager.node.operating_state is NodeOperatingState.OFF: + _LOGGER.debug(f"{self.name} Error: {self.software_manager.node.hostname} is not online.") return False return True diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index 027fae4a..ef2b2956 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -1,9 +1,11 @@ from ipaddress import IPv4Address +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState def test_database_client_server_connection(uc2_network): @@ -55,7 +57,8 @@ def test_database_client_query(uc2_network): """Tests DB query across the network returns HTTP status 200 and date.""" web_server: Server = uc2_network.get_node_by_hostname("web_server") db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] - db_client.connect() + + assert db_client.connected assert db_client.query("SELECT") @@ -92,3 +95,28 @@ def test_restore_backup(uc2_network): assert db_service.restore_backup() is True assert db_service.file_system.get_file(folder_name="database", file_name="database.db") is not None + + +def test_database_client_cannot_query_offline_database_server(uc2_network): + """Tests DB query across the network returns HTTP status 404 when db server is offline.""" + db_server: Server = uc2_network.get_node_by_hostname("database_server") + db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + + assert db_server.operating_state is NodeOperatingState.ON + assert db_service.operating_state is ServiceOperatingState.RUNNING + + web_server: Server = uc2_network.get_node_by_hostname("web_server") + db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + assert db_client.connected + + assert db_client.query("SELECT") is True + + db_server.power_off() + + for i in range(db_server.shut_down_duration + 1): + uc2_network.apply_timestep(timestep=i) + + assert db_server.operating_state is NodeOperatingState.OFF + assert db_service.operating_state is ServiceOperatingState.STOPPED + + assert db_client.query("SELECT") is False diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index e82d97a4..81a223ef 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -1,3 +1,4 @@ +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.system.services.dns.dns_client import DNSClient @@ -24,3 +25,39 @@ def test_dns_client_server(uc2_network): # arcd.com is registered in dns server and should be saved to cache assert dns_client.check_domain_exists(target_domain="arcd.com") assert dns_client.dns_cache.get("arcd.com", None) is not None + + assert len(dns_client.dns_cache) == 1 + + +def test_dns_client_requests_offline_dns_server(uc2_network): + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + domain_controller: Server = uc2_network.get_node_by_hostname("domain_controller") + + dns_client: DNSClient = client_1.software_manager.software["DNSClient"] + dns_server: DNSServer = domain_controller.software_manager.software["DNSServer"] + + assert dns_client.operating_state == ServiceOperatingState.RUNNING + assert dns_server.operating_state == ServiceOperatingState.RUNNING + + dns_server.show() + + # arcd.com is registered in dns server + assert dns_client.check_domain_exists(target_domain="arcd.com") + assert dns_client.dns_cache.get("arcd.com", None) is not None + + assert len(dns_client.dns_cache) == 1 + dns_client.dns_cache = {} + + domain_controller.power_off() + + for i in range(domain_controller.shut_down_duration + 1): + uc2_network.apply_timestep(timestep=i) + + assert domain_controller.operating_state == NodeOperatingState.OFF + assert dns_server.operating_state == ServiceOperatingState.STOPPED + + # this time it should not cache because dns server is not online + assert dns_client.check_domain_exists(target_domain="arcd.com") is False + assert dns_client.dns_cache.get("arcd.com", None) is None + + assert len(dns_client.dns_cache) == 0 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py index dc6df5d4..469c8548 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address import pytest from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest @@ -15,10 +16,13 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer @pytest.fixture(scope="function") def dns_server() -> Node: node = Server( - hostname="dns_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + hostname="dns_server", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) node.software_manager.install(software_class=DNSServer) - node.software_manager.software["DNSServer"].start() return node From 2ce03e0262a781741fa3cf6bbf5a4aacdf18bcc9 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 23 Nov 2023 22:10:53 +0000 Subject: [PATCH 017/107] #2064: turn on everything when node is turned on --- .../simulator/network/hardware/base.py | 12 ++- .../system/applications/application.py | 5 + .../red_services/data_manipulation_bot.py | 13 ++- tests/conftest.py | 6 ++ .../test_uc2_data_manipulation_scenario.py | 1 + .../system/test_app_service_on_node.py | 95 +++++++++++++++++++ .../system/test_service_on_node.py | 42 -------- 7 files changed, 126 insertions(+), 48 deletions(-) create mode 100644 tests/integration_tests/system/test_app_service_on_node.py delete mode 100644 tests/integration_tests/system/test_service_on_node.py diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ebf669eb..ad101f1d 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1424,7 +1424,17 @@ class Node(SimComponent): def _start_up_actions(self): """Actions to perform when the node is starting up.""" - pass + # Turn on all the services in the node + for service_id in self.services: + self.services[service_id].start() + + # Turn on all the applications in the node + for app_id in self.applications: + self.applications[app_id].run() + + # Turn off all processes in the node + # for process_id in self.processes: + # self.processes[process_id] def __contains__(self, item: Any) -> bool: if isinstance(item, Service): diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index db323cf6..fb65354f 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -2,6 +2,7 @@ from abc import abstractmethod from enum import Enum from typing import Any, Dict, Set +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.system.software import IOSoftware, SoftwareHealthState @@ -61,6 +62,10 @@ class Application(IOSoftware): def run(self) -> None: """Open the Application.""" + if self.software_manager and self.software_manager.node.operating_state is not NodeOperatingState.ON: + self.sys_log.error(f"Unable to run application. {self.software_manager.node.hostname} is not turned on.") + return + if self.operating_state == ApplicationOperatingState.CLOSED: self.sys_log.info(f"Running Application {self.name}") self.operating_state = ApplicationOperatingState.RUNNING diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 996e6790..f6662762 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -42,10 +42,13 @@ class DataManipulationBot(DatabaseClient): if self.server_ip_address and self.payload: self.sys_log.info(f"{self.name}: Attempting to start the {self.name}") super().run() - if not self.connected: - self.connect() - if self.connected: - self.query(self.payload) - self.sys_log.info(f"{self.name} payload delivered: {self.payload}") else: self.sys_log.error(f"Failed to start the {self.name} as it requires both a target_ip_address and payload.") + + def attack(self): + """Run the datab manipulation attack.""" + if not self.connected: + self.connect() + if self.connected: + self.query(self.payload) + self.sys_log.info(f"{self.name} payload delivered: {self.payload}") diff --git a/tests/conftest.py b/tests/conftest.py index 4cc36e6b..d39e96e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,6 +52,12 @@ class TestService(Service): class TestApplication(Application): """Test Application class""" + def __init__(self, **kwargs): + kwargs["name"] = "TestApplication" + kwargs["port"] = Port.HTTP + kwargs["protocol"] = IPProtocol.TCP + super().__init__(**kwargs) + def describe_state(self) -> Dict: pass diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 81bbfc96..fe7bab5f 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -23,6 +23,7 @@ def test_data_manipulation(uc2_network): # Now we run the DataManipulationBot db_manipulation_bot.run() + db_manipulation_bot.attack() # Now check that the DB client on the web_server cannot query the users table on the database assert not db_client.query("SELECT") diff --git a/tests/integration_tests/system/test_app_service_on_node.py b/tests/integration_tests/system/test_app_service_on_node.py new file mode 100644 index 00000000..cbcb4ff6 --- /dev/null +++ b/tests/integration_tests/system/test_app_service_on_node.py @@ -0,0 +1,95 @@ +from typing import Tuple + +import pytest +from conftest import TestApplication, TestService + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.system.applications.application import Application, ApplicationOperatingState +from primaite.simulator.system.services.service import Service, ServiceOperatingState + + +@pytest.fixture(scope="function") +def populated_node() -> Tuple[Application, Server, Service]: + server = Server( + hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON + ) + server.software_manager.install(TestService) + server.software_manager.install(TestApplication) + + app = server.software_manager.software["TestApplication"] + app.run() + service = server.software_manager.software["TestService"] + service.start() + + return app, server, service + + +def test_server_turns_off_service(populated_node): + """Check that the service is turned off when the server is turned off""" + app, server, service = populated_node + + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING + assert app.operating_state is ApplicationOperatingState.RUNNING + + server.power_off() + + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) + + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_service_cannot_be_turned_on_when_server_is_off(populated_node): + """Check that the service cannot be started when the server is off.""" + app, server, service = populated_node + + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING + assert app.operating_state is ApplicationOperatingState.RUNNING + + server.power_off() + + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) + + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + assert app.operating_state is ApplicationOperatingState.CLOSED + + service.start() + app.run() + + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_server_turns_on_service(populated_node): + """Check that turning on the server turns on service.""" + app, server, service = populated_node + + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING + assert app.operating_state is ApplicationOperatingState.RUNNING + + server.power_off() + + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) + + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + assert app.operating_state is ApplicationOperatingState.CLOSED + + server.power_on() + + for i in range(server.start_up_duration + 1): + server.apply_timestep(timestep=i) + + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING + assert app.operating_state is ApplicationOperatingState.RUNNING diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py deleted file mode 100644 index e596dcd8..00000000 --- a/tests/integration_tests/system/test_service_on_node.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Tuple - -import pytest -from conftest import TestService - -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.server import Server -from primaite.simulator.system.services.service import Service, ServiceOperatingState - - -@pytest.fixture(scope="function") -def service_on_node() -> Tuple[Server, Service]: - server = Server( - hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON - ) - server.software_manager.install(TestService) - - service = server.software_manager.software["TestService"] - service.start() - - return server, service - - -def test_server_turns_off_service(service_on_node): - """Check that the service is turned off when the server is turned off""" - server, service = service_on_node - - assert server.operating_state is NodeOperatingState.ON - assert service.operating_state is ServiceOperatingState.RUNNING - - server.power_off() - - for i in range(server.shut_down_duration + 1): - server.apply_timestep(timestep=i) - - assert server.operating_state is NodeOperatingState.OFF - assert service.operating_state is ServiceOperatingState.STOPPED - - -def test_server_turns_on_service(service_on_node): - """Check that turning on the server turns on service.""" - pass From 8aa743188f60fa95756d1076e8fa5415e89d8dc8 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 23 Nov 2023 22:28:08 +0000 Subject: [PATCH 018/107] #2064: fix layout of test so it passes in pipeline --- tests/conftest.py | 10 ++++++++++ .../system/test_app_service_on_node.py | 7 +++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d39e96e0..168ef3e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,6 +74,11 @@ def service(file_system) -> TestService: ) +@pytest.fixture(scope="function") +def service_class(): + return TestService + + @pytest.fixture(scope="function") def application(file_system) -> TestApplication: return TestApplication( @@ -81,6 +86,11 @@ def application(file_system) -> TestApplication: ) +@pytest.fixture(scope="function") +def application_class(): + return TestApplication + + @pytest.fixture(scope="function") def file_system() -> FileSystem: return Node(hostname="fs_node").file_system diff --git a/tests/integration_tests/system/test_app_service_on_node.py b/tests/integration_tests/system/test_app_service_on_node.py index cbcb4ff6..7777a810 100644 --- a/tests/integration_tests/system/test_app_service_on_node.py +++ b/tests/integration_tests/system/test_app_service_on_node.py @@ -1,7 +1,6 @@ from typing import Tuple import pytest -from conftest import TestApplication, TestService from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.server import Server @@ -10,12 +9,12 @@ from primaite.simulator.system.services.service import Service, ServiceOperating @pytest.fixture(scope="function") -def populated_node() -> Tuple[Application, Server, Service]: +def populated_node(service_class, application_class) -> Tuple[Application, Server, Service]: server = Server( hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON ) - server.software_manager.install(TestService) - server.software_manager.install(TestApplication) + server.software_manager.install(service_class) + server.software_manager.install(application_class) app = server.software_manager.software["TestApplication"] app.run() From b13a9d3daf34f38992b19f7854cbbf0eeb3e2723 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 09:25:55 +0000 Subject: [PATCH 019/107] Add application execution action for data manipulation bot --- .../config/_package_data/example_config.yaml | 9 ++++++--- src/primaite/game/agent/actions.py | 7 +++---- src/primaite/game/session.py | 14 ++++++++++++++ .../system/applications/database_client.py | 2 +- .../services/red_services/data_manipulation_bot.py | 2 +- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index aff54d62..8ea1c83c 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -67,8 +67,8 @@ game_config: observations: - logon_status - operating_status - services: - - service_ref: data_manipulation_bot + applications: + - application_ref: data_manipulation_bot observations: operating_status health_status @@ -89,6 +89,8 @@ game_config: options: nodes: - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot max_folders_per_node: 1 max_files_per_folder: 1 max_services_per_node: 1 @@ -650,7 +652,7 @@ simulation: subnet_mask: 255.255.255.0 default_gateway: 192.168.10.1 dns_server: 192.168.1.10 - services: + applications: - ref: data_manipulation_bot type: DataManipulationBot options: @@ -658,6 +660,7 @@ simulation: data_manipulation_p_of_success: 0.1 payload: "DROP TABLE IF EXISTS user;" server_ip: 192.168.1.14 + services: - ref: client_1_dns_client type: DNSClient diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 0c78dac7..64d89722 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -594,6 +594,7 @@ class ActionManager: session: "PrimaiteSession", # reference to session for looking up stuff actions: List[str], # stores list of actions available to agent node_uuids: List[str], # allows mapping index to node + application_uuids: List[List[str]], # allows mapping index to application max_folders_per_node: int = 2, # allows calculating shape max_files_per_folder: int = 2, # allows calculating shape max_services_per_node: int = 2, # allows calculating shape @@ -635,6 +636,7 @@ class ActionManager: self.session: "PrimaiteSession" = session self.sim: Simulation = self.session.simulation self.node_uuids: List[str] = node_uuids + self.application_uuids: List[List[str]] = application_uuids self.protocols: List[str] = protocols self.ports: List[str] = ports @@ -819,10 +821,7 @@ class ActionManager: :return: The UUID of the service. Or None if the node has fewer services than the given index. :rtype: Optional[str] """ - node_uuid = self.get_node_uuid_by_idx(node_idx) - node = self.sim.network.nodes[node_uuid] - application_uuids = list(node.applications.keys()) - return application_uuids[application_idx] if len(application_uuids) > application_idx else None + return self.application_uuids[node_idx][application_idx] def get_internet_protocol_by_idx(self, protocol_idx: int) -> str: """Get the internet protocol corresponding to the given index. diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index f675e33c..cc4036ef 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -426,11 +426,25 @@ class PrimaiteSession: # CREATE ACTION SPACE action_space_cfg["options"]["node_uuids"] = [] + action_space_cfg["options"]["application_uuids"] = [] + # if a list of nodes is defined, convert them from node references to node UUIDs for action_node_option in action_space_cfg.get("options", {}).pop("nodes", {}): if "node_ref" in action_node_option: node_uuid = sess.ref_map_nodes[action_node_option["node_ref"]] action_space_cfg["options"]["node_uuids"].append(node_uuid) + + if "applications" in action_node_option: + node_application_uuids = [] + for application_option in action_node_option["applications"]: + # TODO: remove inconsistency with the above nodes + application_uuid = sess.ref_map_applications[application_option["application_ref"]].uuid + node_application_uuids.append(application_uuid) + + action_space_cfg["options"]["application_uuids"].append(node_application_uuids) + else: + action_space_cfg["options"]["application_uuids"].append([]) + # Each action space can potentially have a different list of nodes that it can apply to. Therefore, # we will pass node_uuids as a part of the action space config. # However, it's not possible to specify the node uuids directly in the config, as they are generated diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index e15249e3..9d85221e 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -141,7 +141,7 @@ class DatabaseClient(Application): :param sql: The SQL query. :return: True if the query was successful, otherwise False. """ - if self.connected and self.operating_state.RUNNING: + if self.connected and self.operating_state == ApplicationOperatingState.RUNNING: query_id = str(uuid4()) # Initialise the tracker of this ID to False diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index f4b31cb1..0ec64950 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -50,7 +50,7 @@ class DataManipulationBot(DatabaseClient): def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() - rm.add_request(name="execute", request_type=RequestType(func=self.execute)) + rm.add_request(name="execute", request_type=RequestType(func=lambda request, context: self.execute())) return rm From 92dabe59f7d31a270d7e4b937e3075eeb114f913 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 10:04:19 +0000 Subject: [PATCH 020/107] Fix data manipulation bot configuration --- src/primaite/game/session.py | 26 +++++++++++-------- .../red_services/data_manipulation_bot.py | 4 +-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index cc4036ef..286de498 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -331,6 +331,7 @@ class PrimaiteSession: print("invalid node type") if "services" in node_cfg: for service_cfg in node_cfg["services"]: + new_service = None service_ref = service_cfg["ref"] service_type = service_cfg["type"] service_types_mapping = { @@ -339,7 +340,6 @@ class PrimaiteSession: "DatabaseClient": DatabaseClient, "DatabaseService": DatabaseService, "WebServer": WebServer, - "DataManipulationBot": DataManipulationBot, } if service_type in service_types_mapping: print(f"installing {service_type} on node {new_node.hostname}") @@ -360,22 +360,15 @@ class PrimaiteSession: if "domain_mapping" in opt: for domain, ip in opt["domain_mapping"].items(): new_service.dns_register(domain, ip) - if service_type == "DataManipulationBot": - if "options" in service_cfg: - opt = service_cfg["options"] - new_service.configure( - server_ip_address=opt.get("server_ip"), - payload=opt.get("payload"), - 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")), - ) if "applications" in node_cfg: for application_cfg in node_cfg["applications"]: + new_application = None application_ref = application_cfg["ref"] application_type = application_cfg["type"] application_types_mapping = { "WebBrowser": WebBrowser, + "DataManipulationBot": DataManipulationBot, } if application_type in application_types_mapping: new_node.software_manager.install(application_types_mapping[application_type]) @@ -383,6 +376,16 @@ class PrimaiteSession: sess.ref_map_applications[application_ref] = new_application else: print(f"application type not found {application_type}") + + if application_type == "DataManipulationBot": + if "options" in application_cfg: + opt = application_cfg["options"] + new_application.configure( + server_ip_address=opt.get("server_ip"), + payload=opt.get("payload"), + 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")), + ) if "nics" in node_cfg: for nic_num, nic_cfg in node_cfg["nics"].items(): new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) @@ -437,7 +440,8 @@ class PrimaiteSession: if "applications" in action_node_option: node_application_uuids = [] for application_option in action_node_option["applications"]: - # TODO: remove inconsistency with the above nodes + # TODO: fix inconsistency with node uuids and application uuids. The node object get added to + # node_uuid, whereas here the application gets added by uuid. application_uuid = sess.ref_map_applications[application_option["application_ref"]].uuid node_application_uuids.append(application_uuid) diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 0ec64950..2b0bed30 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -159,8 +159,8 @@ class DataManipulationBot(DatabaseClient): if self.server_ip_address and self.payload and self.operating_state: self.sys_log.info(f"{self.name}: Running") self._logon() - self._perform_port_scan(p_of_success=self.execution_definition.port_scan_p_of_success) - self._perform_data_manipulation(p_of_success=self.execution_definition.data_manipulation_p_of_success) + self._perform_port_scan(p_of_success=self.port_scan_p_of_success) + self._perform_data_manipulation(p_of_success=self.data_manipulation_p_of_success) if self.repeat and self.attack_stage in ( DataManipulationAttackStage.COMPLETE, From 178d911be005fc7f888d1aa1e679d6268a66cda3 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 10:05:36 +0000 Subject: [PATCH 021/107] Update data manipulation bot --- .../system/data_manipulation_bot.rst | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/source/simulation_components/system/data_manipulation_bot.rst b/docs/source/simulation_components/system/data_manipulation_bot.rst index c9f8977a..e93c4e54 100644 --- a/docs/source/simulation_components/system/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/data_manipulation_bot.rst @@ -8,6 +8,8 @@ DataManipulationBot The ``DataManipulationBot`` class provides functionality to connect to a ``DatabaseService`` and execute malicious SQL statements. +The bot is controlled by a ``DataManipulationAgent``. + Overview -------- @@ -16,15 +18,25 @@ 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: + +- Logon - *The bot gains access to the node.* +- Port Scan - *The bot finds accessible database servers on the network.* +- Attacking - *The bot delivers the payload to the discovered database servers.* + +Each of these stages has a random, configurable probability of succeeding. 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 - - Database password (if needed) - - SQL statement payload + - Target database server IP + - Database password (if needed) + - SQL statement payload + - Probabilities for succeeding each of the above attack stages - Call ``run`` to connect and execute the statement. The bot handles connecting, executing the statement, and disconnecting. @@ -52,7 +64,7 @@ Implementation The bot extends ``DatabaseClient`` and leverages its connectivity. - Uses the Application base class for lifecycle management. -- Credentials and target IP set via ``configure``. +- 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. From ff8b773c102243549d66eeaa357fa56df9be4094 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 11:10:34 +0000 Subject: [PATCH 022/107] Database Manipulation Bot bug fixes --- .../config/_package_data/example_config.yaml | 2 +- src/primaite/game/agent/interface.py | 4 +- src/primaite/simulator/network/networks.py | 7 ++- .../system/applications/database_client.py | 4 +- .../red_services/data_manipulation_bot.py | 8 +-- .../assets/configs/bad_primaite_session.yaml | 51 ++++++++++++------- .../configs/eval_only_primaite_session.yaml | 45 +++++++++------- .../assets/configs/test_primaite_session.yaml | 41 ++++++++------- .../configs/train_only_primaite_session.yaml | 45 +++++++++------- 9 files changed, 124 insertions(+), 83 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 270760f5..af872a01 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -665,7 +665,7 @@ simulation: options: port_scan_p_of_success: 0.1 data_manipulation_p_of_success: 0.1 - payload: "DROP TABLE IF EXISTS user;" + payload: "DELETE" server_ip: 192.168.1.14 services: - ref: client_1_dns_client diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index ff0986a8..38116987 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -58,7 +58,7 @@ class AbstractAgent(ABC): action_space: Optional[ActionManager], observation_space: Optional[ObservationManager], reward_function: Optional[RewardFunction], - agent_settings: Optional[AgentSettings], + agent_settings: Optional[AgentSettings] = None, ) -> None: """ Initialize an agent. @@ -217,7 +217,7 @@ class DataManipulationAgent(AbstractScriptedAgent): :return: _description_ :rtype: Tuple[str, Dict] """ - current_timestep = self.action_space.session.step_counter + current_timestep = self.action_manager.session.step_counter if current_timestep < self.next_execution_timestep: return "DONOTHING", {"dummy": 0} diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index c0f9a07e..ea767b54 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -140,7 +140,12 @@ def arcd_uc2_network() -> Network: network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] - db_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE") + db_manipulation_bot.configure( + server_ip_address=IPv4Address("192.168.1.14"), + payload="DELETE", + port_scan_p_of_success=1.0, + data_manipulation_p_of_success=1.0, + ) # Client 2 client_2 = Computer( diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index a5c213cd..da2299c4 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -129,9 +129,9 @@ class DatabaseClient(Application): ) return self._query(sql=sql, query_id=query_id, is_reattempt=True) - def execute(self) -> None: + def run(self) -> None: """Run the DatabaseClient.""" - super().execute() + super().run() if self.operating_state == ApplicationOperatingState.RUNNING: self.connect() diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 2b0bed30..17b89386 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -50,7 +50,7 @@ class DataManipulationBot(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.execute())) + rm.add_request(name="execute", request_type=RequestType(func=lambda request, context: self.run())) return rm @@ -139,13 +139,13 @@ class DataManipulationBot(DatabaseClient): self.sys_log.info(f"{self.name}: Data manipulation failed") self.attack_stage = DataManipulationAttackStage.FAILED - def execute(self): + def run(self): """ - Execute the Data Manipulation Bot. + Run the Data Manipulation Bot. Calls the parent classes execute method before starting the application loop. """ - super().execute() + super().run() self._application_loop() def _application_loop(self): diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 80567aea..6344eac0 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -2,9 +2,17 @@ training_config: rl_framework: SB3 rl_algorithm: PPO se3ed: 333 # Purposeful typo to check that error is raised with bad configuration. - n_learn_steps: 2560 + n_learn_episodes: 25 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 game_config: @@ -49,9 +57,10 @@ game_config: - type: DUMMY agent_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_settings: + start_step: 5 + frequency: 4 + variance: 3 - ref: client_1_data_manipulation_red_bot team: RED @@ -65,8 +74,8 @@ game_config: observations: - logon_status - operating_status - services: - - service_ref: data_manipulation_bot + applications: + - application_ref: data_manipulation_bot observations: operating_status health_status @@ -76,22 +85,19 @@ game_config: action_list: - type: DONOTHING # Date: Fri, 24 Nov 2023 11:21:25 +0000 Subject: [PATCH 023/107] #2068: Remove duplicated index entries. --- docs/source/simulation.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/source/simulation.rst b/docs/source/simulation.rst index 5e259c6f..e5c0d2c8 100644 --- a/docs/source/simulation.rst +++ b/docs/source/simulation.rst @@ -23,8 +23,3 @@ Contents simulation_components/network/network simulation_components/system/internal_frame_processing simulation_components/system/software - simulation_components/system/data_manipulation_bot - simulation_components/system/database_client_server - simulation_components/system/dns_client_server - simulation_components/system/ftp_client_server - simulation_components/system/web_browser_and_web_server_service From dfb08b8cf31ba15801f1a1770a4df1cf46617638 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Fri, 24 Nov 2023 11:52:33 +0000 Subject: [PATCH 024/107] #1859 - DB query now returns false if the query isn't ran due to the node being off --- src/primaite/game/agent/interface.py | 5 +---- .../simulator/system/applications/database_client.py | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 38116987..b321b17c 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,9 +1,8 @@ """Interface for agents.""" import random from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Tuple, TYPE_CHECKING, TypeAlias, Union +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING -import numpy as np from gymnasium.core import ActType, ObsType from pydantic import BaseModel @@ -14,8 +13,6 @@ from primaite.game.agent.rewards import RewardFunction if TYPE_CHECKING: from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot -ObsType: TypeAlias = Union[Dict, np.ndarray] - class AgentStartSettings(BaseModel): """Configuration values for when an agent starts performing actions.""" diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index da2299c4..3c4f1b75 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -148,6 +148,7 @@ class DatabaseClient(Application): # Initialise the tracker of this ID to False self._query_success_tracker[query_id] = False return self._query(sql=sql, query_id=query_id) + return False def receive(self, payload: Any, session_id: str, **kwargs) -> bool: """ From e609f8eb50e935515a0d63ad85e9321404f8fd98 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 24 Nov 2023 14:56:17 +0000 Subject: [PATCH 025/107] Fix misconfiguration in uc2 config and session --- .../config/_package_data/example_config.yaml | 18 +++++++++-- src/primaite/game/session.py | 31 ++++++++++++++++--- .../assets/configs/bad_primaite_session.yaml | 18 +++++++++-- .../configs/eval_only_primaite_session.yaml | 18 +++++++++-- .../assets/configs/test_primaite_session.yaml | 18 +++++++++-- .../configs/train_only_primaite_session.yaml | 18 +++++++++-- 6 files changed, 102 insertions(+), 19 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index af872a01..6455272c 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -560,7 +560,7 @@ simulation: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 2: - ip_address: 192.168.1.1 + ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: 0: @@ -571,6 +571,14 @@ simulation: action: PERMIT src_port: DNS dst_port: DNS + 2: + action: PERMIT + src_port: FTP + dst_port: FTP + 3: + action: PERMIT + src_port: HTTP + dst_port: HTTP 22: action: PERMIT src_port: ARP @@ -607,7 +615,7 @@ 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_database_client @@ -628,6 +636,10 @@ simulation: services: - ref: database_service type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - ref: database_ftp_client + type: FTPClient - ref: backup_server type: server @@ -638,7 +650,7 @@ simulation: dns_server: 192.168.1.10 services: - ref: backup_service - type: DatabaseBackup + type: FTPServer - ref: security_suite type: server diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index 7856cc9f..f0dcdd61 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -16,7 +16,7 @@ from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction from primaite.game.io import SessionIO, SessionIOSettings from primaite.game.policy.policy import PolicyABC -from primaite.simulator.network.hardware.base import Link, NIC, Node +from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.server import Server @@ -30,6 +30,8 @@ from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService 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.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer @@ -334,6 +336,7 @@ class PrimaiteSession: subnet_mask=node_cfg["subnet_mask"], default_gateway=node_cfg["default_gateway"], dns_server=node_cfg["dns_server"], + operating_state=NodeOperatingState.ON, ) elif n_type == "server": new_node = Server( @@ -342,16 +345,26 @@ class PrimaiteSession: subnet_mask=node_cfg["subnet_mask"], default_gateway=node_cfg["default_gateway"], dns_server=node_cfg.get("dns_server"), + operating_state=NodeOperatingState.ON, ) elif n_type == "switch": - new_node = Switch(hostname=node_cfg["hostname"], num_ports=node_cfg.get("num_ports")) + new_node = Switch( + hostname=node_cfg["hostname"], + num_ports=node_cfg.get("num_ports"), + operating_state=NodeOperatingState.ON, + ) elif n_type == "router": - new_node = Router(hostname=node_cfg["hostname"], num_ports=node_cfg.get("num_ports")) + new_node = Router( + hostname=node_cfg["hostname"], + num_ports=node_cfg.get("num_ports"), + operating_state=NodeOperatingState.ON, + ) if "ports" in node_cfg: for port_num, port_cfg in node_cfg["ports"].items(): new_node.configure_port( port=port_num, ip_address=port_cfg["ip_address"], subnet_mask=port_cfg["subnet_mask"] ) + # new_node.enable_port(port_num) if "acl" in node_cfg: for r_num, r_cfg in node_cfg["acl"].items(): # excuse the uncommon walrus operator ` := `. It's just here as a shorthand, to avoid repeating @@ -379,6 +392,8 @@ class PrimaiteSession: "DatabaseClient": DatabaseClient, "DatabaseService": DatabaseService, "WebServer": WebServer, + "FTPClient": FTPClient, + "FTPServer": FTPServer, } if service_type in service_types_mapping: print(f"installing {service_type} on node {new_node.hostname}") @@ -399,6 +414,12 @@ class PrimaiteSession: if "domain_mapping" in opt: for domain, ip in opt["domain_mapping"].items(): new_service.dns_register(domain, ip) + if service_type == "DatabaseService": + if "options" in service_cfg: + opt = service_cfg["options"] + if "backup_server_ip" in opt: + new_service.configure_backup(backup_server=IPv4Address(opt["backup_server_ip"])) + new_service.start() if "applications" in node_cfg: for application_cfg in node_cfg["applications"]: @@ -435,7 +456,7 @@ class PrimaiteSession: node_ref ] = ( new_node.uuid - ) # TODO: fix incosistency with service and link. Node gets added by uuid, but service by object + ) # TODO: fix inconsistency with service and link. Node gets added by uuid, but service by object # 2. create links between nodes for link_cfg in links_cfg: @@ -451,6 +472,8 @@ class PrimaiteSession: endpoint_b = node_b.ethernet_port[link_cfg["endpoint_b_port"]] new_link = net.connect(endpoint_a=endpoint_a, endpoint_b=endpoint_b) sess.ref_map_links[link_cfg["ref"]] = new_link.uuid + # endpoint_a.enable() + # endpoint_b.enable() # 3. create agents game_cfg = cfg["game_config"] diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 6344eac0..4d8e4669 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -560,7 +560,7 @@ simulation: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 2: - ip_address: 192.168.1.1 + ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: 0: @@ -571,6 +571,14 @@ simulation: action: PERMIT src_port: DNS dst_port: DNS + 2: + action: PERMIT + src_port: FTP + dst_port: FTP + 3: + action: PERMIT + src_port: HTTP + dst_port: HTTP 22: action: PERMIT src_port: ARP @@ -607,7 +615,7 @@ 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_database_client @@ -628,6 +636,10 @@ simulation: services: - ref: database_service type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - ref: database_ftp_client + type: FTPClient - ref: backup_server type: server @@ -638,7 +650,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/eval_only_primaite_session.yaml b/tests/assets/configs/eval_only_primaite_session.yaml index aa8c8b1f..27a18d9f 100644 --- a/tests/assets/configs/eval_only_primaite_session.yaml +++ b/tests/assets/configs/eval_only_primaite_session.yaml @@ -560,7 +560,7 @@ simulation: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 2: - ip_address: 192.168.1.1 + ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: 0: @@ -571,6 +571,14 @@ simulation: action: PERMIT src_port: DNS dst_port: DNS + 2: + action: PERMIT + src_port: FTP + dst_port: FTP + 3: + action: PERMIT + src_port: HTTP + dst_port: HTTP 22: action: PERMIT src_port: ARP @@ -607,7 +615,7 @@ 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_database_client @@ -628,6 +636,10 @@ simulation: services: - ref: database_service type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - ref: database_ftp_client + type: FTPClient - ref: backup_server type: server @@ -638,7 +650,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 8133c5d9..64be5488 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -560,7 +560,7 @@ simulation: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 2: - ip_address: 192.168.1.1 + ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: 0: @@ -571,6 +571,14 @@ simulation: action: PERMIT src_port: DNS dst_port: DNS + 2: + action: PERMIT + src_port: FTP + dst_port: FTP + 3: + action: PERMIT + src_port: HTTP + dst_port: HTTP 22: action: PERMIT src_port: ARP @@ -607,7 +615,7 @@ 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_database_client @@ -628,6 +636,10 @@ simulation: services: - ref: database_service type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - ref: database_ftp_client + type: FTPClient - ref: backup_server type: server @@ -638,7 +650,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 f1e317d3..4cfe4df4 100644 --- a/tests/assets/configs/train_only_primaite_session.yaml +++ b/tests/assets/configs/train_only_primaite_session.yaml @@ -560,7 +560,7 @@ simulation: ip_address: 192.168.1.1 subnet_mask: 255.255.255.0 2: - ip_address: 192.168.1.1 + ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: 0: @@ -571,6 +571,14 @@ simulation: action: PERMIT src_port: DNS dst_port: DNS + 2: + action: PERMIT + src_port: FTP + dst_port: FTP + 3: + action: PERMIT + src_port: HTTP + dst_port: HTTP 22: action: PERMIT src_port: ARP @@ -607,7 +615,7 @@ 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_database_client @@ -628,6 +636,10 @@ simulation: services: - ref: database_service type: DatabaseService + options: + backup_server_ip: 192.168.1.16 + - ref: database_ftp_client + type: FTPClient - ref: backup_server type: server @@ -638,7 +650,7 @@ simulation: dns_server: 192.168.1.10 services: - ref: backup_service - type: DatabaseBackup + type: FTPServer - ref: security_suite type: server From e6f75f8b320f188475782b5564cc3f0bcc3413fe Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 15:15:24 +0000 Subject: [PATCH 026/107] Improve data manipulation bot documentation --- .../system/data_manipulation_bot.rst | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/source/simulation_components/system/data_manipulation_bot.rst b/docs/source/simulation_components/system/data_manipulation_bot.rst index 03f2208b..eeae0b0a 100644 --- a/docs/source/simulation_components/system/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/data_manipulation_bot.rst @@ -8,8 +8,6 @@ DataManipulationBot The ``DataManipulationBot`` class provides functionality to connect to a ``DatabaseService`` and execute malicious SQL statements. -The bot is controlled by a ``DataManipulationAgent``. - Overview -------- @@ -23,11 +21,11 @@ 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: -- Logon - *The bot gains access to the node.* +- Logon - *The bot gains credentials and accesses the node.* - Port Scan - *The bot finds accessible database servers on the network.* - Attacking - *The bot delivers the payload to the discovered database servers.* -Each of these stages has a random, configurable probability of succeeding. The bot can also be configured to repeat the attack once complete. +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 ----- @@ -41,6 +39,8 @@ Usage 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 ------- @@ -58,6 +58,74 @@ 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: + # ... + agents: + - ref: data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent + + observation_space: + type: UC2RedObservation + options: + nodes: + - node_ref: client_1 + observations: + - logon_status + - operating_status + applications: + - application_ref: data_manipulation_bot + observations: + operating_status + health_status + folders: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + # ... + + simulation: + network: + nodes: + - ref: client_1 + type: computer + # ... additional configuration here + applications: + - ref: data_manipulation_bot + type: DataManipulationBot + options: + port_scan_p_of_success: 0.1 + data_manipulation_p_of_success: 0.1 + payload: "DELETE" + server_ip: 192.168.1.14 + Implementation -------------- From c5cfbb825a275398d56799253b11cc3656d20777 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 15:15:45 +0000 Subject: [PATCH 027/107] Fix database client connect method --- src/primaite/simulator/system/applications/database_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 3c4f1b75..b24b6062 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -54,7 +54,7 @@ class DatabaseClient(Application): def connect(self) -> bool: """Connect to a Database Service.""" - if not self.connected and self.operating_state.RUNNING: + if not self.connected and self.operating_state == ApplicationOperatingState.RUNNING: return self._connect(self.server_ip_address, self.server_password) return False From b7b718f25d142a53526876b20fbdeb9abc47ab06 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 24 Nov 2023 15:15:56 +0000 Subject: [PATCH 028/107] #2064: added a method that checks if the class can perform actions and added it where necessary + tests everywhere --- src/primaite/game/agent/observations.py | 2 +- .../system/applications/application.py | 33 +++++- .../system/applications/database_client.py | 23 +++- .../system/applications/web_browser.py | 3 + .../services/database/database_service.py | 8 ++ .../system/services/dns/dns_client.py | 10 +- .../system/services/dns/dns_server.py | 6 + .../simulator/system/services/service.py | 23 +++- src/primaite/simulator/system/software.py | 25 +++- .../system/test_application_on_node.py | 110 ++++++++++++++++++ ...ice_on_node.py => test_service_on_node.py} | 60 +++++++--- .../system/test_web_client_server.py | 22 ++++ .../_simulator/_system/_services/test_dns.py | 41 +++++++ ...sim_conatiner.py => test_sim_container.py} | 0 14 files changed, 328 insertions(+), 38 deletions(-) create mode 100644 tests/integration_tests/system/test_application_on_node.py rename tests/integration_tests/system/{test_app_service_on_node.py => test_service_on_node.py} (64%) rename tests/unit_tests/_primaite/_simulator/{test_sim_conatiner.py => test_sim_container.py} (100%) diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index a74771c0..dcb03d00 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -263,7 +263,7 @@ class FolderObservation(AbstractObservation): self.files.append(FileObservation()) while len(self.files) > num_files_per_folder: truncated_file = self.files.pop() - msg = f"Too many files in folde observation. Truncating file {truncated_file}" + msg = f"Too many files in folder observation. Truncating file {truncated_file}" _LOGGER.warn(msg) self.default_observation = { diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index fb65354f..d2f9772d 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -2,9 +2,11 @@ from abc import abstractmethod from enum import Enum from typing import Any, Dict, Set -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite import getLogger from primaite.simulator.system.software import IOSoftware, SoftwareHealthState +_LOGGER = getLogger(__name__) + class ApplicationOperatingState(Enum): """Enumeration of Application Operating States.""" @@ -52,7 +54,7 @@ class Application(IOSoftware): state = super().describe_state() state.update( { - "opearting_state": self.operating_state.value, + "operating_state": self.operating_state.value, "execution_control_status": self.execution_control_status, "num_executions": self.num_executions, "groups": list(self.groups), @@ -60,10 +62,28 @@ class Application(IOSoftware): ) return state + def _can_perform_action(self) -> bool: + """ + Checks if the application can perform actions. + + This is done by checking if the application is operating properly or the node it is installed + in is operational. + + Returns true if the software can perform actions. + """ + if not super()._can_perform_action(): + return False + + 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}") + return False + + return True + def run(self) -> None: """Open the Application.""" - if self.software_manager and self.software_manager.node.operating_state is not NodeOperatingState.ON: - self.sys_log.error(f"Unable to run application. {self.software_manager.node.hostname} is not turned on.") + if not super()._can_perform_action(): return if self.operating_state == ApplicationOperatingState.CLOSED: @@ -78,6 +98,9 @@ class Application(IOSoftware): def install(self) -> None: """Install Application.""" + if self._can_perform_action(): + return + super().install() if self.operating_state == ApplicationOperatingState.CLOSED: self.sys_log.info(f"Installing Application {self.name}") @@ -102,4 +125,4 @@ class Application(IOSoftware): :param payload: The payload to receive. :return: True if successful, False otherwise. """ - pass + return super().receive(payload=payload, session_id=session_id, **kwargs) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 37f89371..9cb87bf6 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -54,7 +54,10 @@ class DatabaseClient(Application): def connect(self) -> bool: """Connect to a Database Service.""" - if not self.connected and self.operating_state.RUNNING: + if not self._can_perform_action(): + return False + + if not self.connected: return self._connect(self.server_ip_address, self.server_password) return False @@ -135,19 +138,31 @@ class DatabaseClient(Application): self.operating_state = ApplicationOperatingState.RUNNING self.connect() - def query(self, sql: str) -> bool: + def query(self, sql: str, is_reattempt: bool = False) -> bool: """ Send a query to the Database Service. - :param sql: The SQL query. + :param: sql: The SQL query. + :param: is_reattempt: If true, the action has been reattempted. :return: True if the query was successful, otherwise False. """ - if self.connected and self.operating_state.RUNNING: + if not self._can_perform_action(): + return False + + if self.connected: query_id = str(uuid4()) # Initialise the tracker of this ID to False self._query_success_tracker[query_id] = False return self._query(sql=sql, query_id=query_id) + else: + if is_reattempt: + return False + + if not self.connect(): + return False + + self.query(sql=sql, is_reattempt=True) def receive(self, payload: Any, session_id: str, **kwargs) -> bool: """ diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index bb9552d8..71e30c7f 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -65,6 +65,9 @@ class WebBrowser(Application): :param: url: The address of the web page the browser requests :type: url: str """ + if not self._can_perform_action(): + return False + # reset latest response self.latest_response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND) diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index e3adb8e1..740ed4fd 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -48,6 +48,10 @@ class DatabaseService(Service): def backup_database(self) -> bool: """Create a backup of the database to the configured backup server.""" + # check if this action can be performed + if not self._can_perform_action(): + return False + # check if the backup server was configured if self.backup_server is None: self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.") @@ -73,6 +77,10 @@ class DatabaseService(Service): def restore_backup(self) -> bool: """Restore a backup from backup server.""" + # check if this action can be performed + if not self._can_perform_action(): + return False + software_manager: SoftwareManager = self.software_manager ftp_client_service: FTPClient = software_manager.software["FTPClient"] diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 266ac4f6..a0965009 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address -from typing import Dict, Optional +from typing import Dict, Optional, Union from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest @@ -51,13 +51,16 @@ class DNSClient(Service): """ pass - def add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address): + def add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address) -> Union[bool, None]: """ Adds a domain name to the DNS Client cache. :param: domain_name: The domain name to save to cache :param: ip_address: The IP Address to attach the domain name to """ + if not self._can_perform_action(): + return False + self.dns_cache[domain_name] = ip_address def check_domain_exists( @@ -72,6 +75,9 @@ class DNSClient(Service): :param: session_id: The Session ID the payload is to originate from. Optional. :param: is_reattempt: Checks if the request has been reattempted. Default is False. """ + if not self._can_perform_action(): + return False + # check if DNS server is configured if self.dns_server is None: self.sys_log.error(f"{self.name}: DNS Server is not configured") diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 2c8f3003..b6d4961f 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -48,6 +48,9 @@ class DNSServer(Service): :param target_domain: The single domain name requested by a DNS client. :return ip_address: The IP address of that domain name or None. """ + if not self._can_perform_action(): + return + return self.dns_table.get(target_domain) def dns_register(self, domain_name: str, domain_ip_address: IPv4Address): @@ -60,6 +63,9 @@ class DNSServer(Service): :param: domain_ip_address: The IP address that the domain should route to :type: domain_ip_address: IPv4Address """ + if not self._can_perform_action(): + return + self.dns_table[domain_name] = domain_ip_address def reset_component_for_episode(self, episode: int): diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 3a1a4c9d..04a4603a 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -3,7 +3,6 @@ from typing import Any, Dict, Optional from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType -from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.system.software import IOSoftware, SoftwareHealthState _LOGGER = getLogger(__name__) @@ -41,6 +40,25 @@ class Service(IOSoftware): restart_countdown: Optional[int] = None "If currently restarting, how many timesteps remain until the restart is finished." + def _can_perform_action(self) -> bool: + """ + Checks if the service can perform actions. + + This is done by checking if the service is operating properly or the node it is installed + in is operational. + + Returns true if the software can perform actions. + """ + if not super()._can_perform_action(): + return False + + 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}") + return False + + return True + def receive(self, payload: Any, session_id: str, **kwargs) -> bool: """ Receives a payload from the SessionManager. @@ -108,8 +126,7 @@ class Service(IOSoftware): def start(self, **kwargs) -> None: """Start the service.""" # cant start the service if the node it is on is off - if self.software_manager and self.software_manager.node.operating_state is not NodeOperatingState.ON: - self.sys_log.error(f"Unable to start service. {self.software_manager.node.hostname} is not turned on.") + if not super()._can_perform_action(): return if self.operating_state == ServiceOperatingState.STOPPED: diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 830e3d79..5564bd48 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -226,6 +226,21 @@ class IOSoftware(Software): ) return state + @abstractmethod + def _can_perform_action(self) -> bool: + """ + Checks if the software can perform actions. + + This is done by checking if the software is operating properly or the node it is installed + in is operational. + + Returns true if the software can perform actions. + """ + if self.software_manager and self.software_manager.node.operating_state is NodeOperatingState.OFF: + _LOGGER.debug(f"{self.name} Error: {self.software_manager.node.hostname} is not online.") + return False + return True + def send( self, payload: Any, @@ -244,6 +259,9 @@ class IOSoftware(Software): :return: True if successful, False otherwise. """ + if not self._can_perform_action(): + return False + return self.software_manager.send_payload_to_session_manager( payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id ) @@ -262,8 +280,5 @@ class IOSoftware(Software): :param kwargs: Additional keyword arguments specific to the implementation. :return: True if the payload was successfully received and processed, False otherwise. """ - # return false if node that software is on is off - if self.software_manager and self.software_manager.node.operating_state is NodeOperatingState.OFF: - _LOGGER.debug(f"{self.name} Error: {self.software_manager.node.hostname} is not online.") - return False - return True + # return false if not allowed to perform actions + return self._can_perform_action() diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py new file mode 100644 index 00000000..7ac7b492 --- /dev/null +++ b/tests/integration_tests/system/test_application_on_node.py @@ -0,0 +1,110 @@ +from typing import Tuple + +import pytest + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.system.applications.application import Application, ApplicationOperatingState + + +@pytest.fixture(scope="function") +def populated_node(application_class) -> Tuple[Application, Computer]: + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + computer.software_manager.install(application_class) + + app = computer.software_manager.software["TestApplication"] + app.run() + + return app, computer + + +def test_service_on_offline_node(application_class): + """Test to check that the service cannot be interacted with when node it is on is off.""" + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + computer.software_manager.install(application_class) + + app: Application = computer.software_manager.software["TestApplication"] + + computer.power_off() + + for i in range(computer.shut_down_duration + 1): + computer.apply_timestep(timestep=i) + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + app.run() + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_server_turns_off_service(populated_node): + """Check that the service is turned off when the server is turned off""" + app, computer = populated_node + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + for i in range(computer.shut_down_duration + 1): + computer.apply_timestep(timestep=i) + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_service_cannot_be_turned_on_when_server_is_off(populated_node): + """Check that the service cannot be started when the server is off.""" + app, computer = populated_node + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + for i in range(computer.shut_down_duration + 1): + computer.apply_timestep(timestep=i) + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + app.run() + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + +def test_server_turns_on_service(populated_node): + """Check that turning on the server turns on service.""" + app, computer = populated_node + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.power_off() + + for i in range(computer.shut_down_duration + 1): + computer.apply_timestep(timestep=i) + + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + computer.power_on() + + for i in range(computer.start_up_duration + 1): + computer.apply_timestep(timestep=i) + + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING diff --git a/tests/integration_tests/system/test_app_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py similarity index 64% rename from tests/integration_tests/system/test_app_service_on_node.py rename to tests/integration_tests/system/test_service_on_node.py index 7777a810..b23df58b 100644 --- a/tests/integration_tests/system/test_app_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -3,34 +3,66 @@ from typing import Tuple import pytest from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server -from primaite.simulator.system.applications.application import Application, ApplicationOperatingState from primaite.simulator.system.services.service import Service, ServiceOperatingState @pytest.fixture(scope="function") -def populated_node(service_class, application_class) -> Tuple[Application, Server, Service]: +def populated_node( + service_class, +) -> Tuple[Server, Service]: server = Server( hostname="server", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON ) server.software_manager.install(service_class) - server.software_manager.install(application_class) - app = server.software_manager.software["TestApplication"] - app.run() service = server.software_manager.software["TestService"] service.start() - return app, server, service + return server, service + + +def test_service_on_offline_node(service_class): + """Test to check that the service cannot be interacted with when node it is on is off.""" + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + computer.software_manager.install(service_class) + + service: Service = computer.software_manager.software["TestService"] + + computer.power_off() + + for i in range(computer.shut_down_duration + 1): + computer.apply_timestep(timestep=i) + + assert computer.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + + service.start() + assert service.operating_state is ServiceOperatingState.STOPPED + + service.resume() + assert service.operating_state is ServiceOperatingState.STOPPED + + service.restart() + assert service.operating_state is ServiceOperatingState.STOPPED + + service.pause() + assert service.operating_state is ServiceOperatingState.STOPPED def test_server_turns_off_service(populated_node): """Check that the service is turned off when the server is turned off""" - app, server, service = populated_node + server, service = populated_node assert server.operating_state is NodeOperatingState.ON assert service.operating_state is ServiceOperatingState.RUNNING - assert app.operating_state is ApplicationOperatingState.RUNNING server.power_off() @@ -39,16 +71,14 @@ def test_server_turns_off_service(populated_node): assert server.operating_state is NodeOperatingState.OFF assert service.operating_state is ServiceOperatingState.STOPPED - assert app.operating_state is ApplicationOperatingState.CLOSED def test_service_cannot_be_turned_on_when_server_is_off(populated_node): """Check that the service cannot be started when the server is off.""" - app, server, service = populated_node + server, service = populated_node assert server.operating_state is NodeOperatingState.ON assert service.operating_state is ServiceOperatingState.RUNNING - assert app.operating_state is ApplicationOperatingState.RUNNING server.power_off() @@ -57,23 +87,19 @@ def test_service_cannot_be_turned_on_when_server_is_off(populated_node): assert server.operating_state is NodeOperatingState.OFF assert service.operating_state is ServiceOperatingState.STOPPED - assert app.operating_state is ApplicationOperatingState.CLOSED service.start() - app.run() assert server.operating_state is NodeOperatingState.OFF assert service.operating_state is ServiceOperatingState.STOPPED - assert app.operating_state is ApplicationOperatingState.CLOSED def test_server_turns_on_service(populated_node): """Check that turning on the server turns on service.""" - app, server, service = populated_node + server, service = populated_node assert server.operating_state is NodeOperatingState.ON assert service.operating_state is ServiceOperatingState.RUNNING - assert app.operating_state is ApplicationOperatingState.RUNNING server.power_off() @@ -82,7 +108,6 @@ def test_server_turns_on_service(populated_node): assert server.operating_state is NodeOperatingState.OFF assert service.operating_state is ServiceOperatingState.STOPPED - assert app.operating_state is ApplicationOperatingState.CLOSED server.power_on() @@ -91,4 +116,3 @@ def test_server_turns_on_service(populated_node): assert server.operating_state is NodeOperatingState.ON assert service.operating_state is ServiceOperatingState.RUNNING - assert app.operating_state is ApplicationOperatingState.RUNNING diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f3995c84..8f87ef27 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -78,3 +78,25 @@ def test_web_page_request_from_shut_down_server(uc2_network): assert web_client.get_webpage("http://arcd.com/users/") is False assert web_client.latest_response.status_code == HttpStatusCode.NOT_FOUND + + +def test_web_page_request_from_closed_web_browser(uc2_network): + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] + web_client.run() + + web_server: Server = uc2_network.get_node_by_hostname("web_server") + + assert web_client.operating_state == ApplicationOperatingState.RUNNING + + assert web_client.get_webpage("http://arcd.com/users/") is True + + # latest response should have status code 200 + assert web_client.latest_response.status_code == HttpStatusCode.OK + + web_client.close() + + # node should be off + assert web_client.operating_state is ApplicationOperatingState.CLOSED + + assert web_client.get_webpage("http://arcd.com/users/") is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py index 469c8548..2b4082d9 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py @@ -11,6 +11,7 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port 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 ServiceOperatingState @pytest.fixture(scope="function") @@ -54,6 +55,44 @@ def test_create_dns_client(dns_client): assert dns_client_service.protocol is IPProtocol.TCP +def test_dns_client_add_domain_to_cache_when_not_running(dns_client): + dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + assert dns_client.operating_state is NodeOperatingState.OFF + assert dns_client_service.operating_state is ServiceOperatingState.STOPPED + + assert ( + dns_client_service.add_domain_to_cache(domain_name="test.com", ip_address=IPv4Address("192.168.1.100")) is False + ) + + assert dns_client_service.dns_cache.get("test.com") is None + + +def test_dns_client_check_domain_exists_when_not_running(dns_client): + dns_client.operating_state = NodeOperatingState.ON + dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service.start() + + assert dns_client.operating_state is NodeOperatingState.ON + assert dns_client_service.operating_state is ServiceOperatingState.RUNNING + + assert ( + dns_client_service.add_domain_to_cache(domain_name="test.com", ip_address=IPv4Address("192.168.1.100")) + is not False + ) + + assert dns_client_service.check_domain_exists("test.com") is True + + dns_client.power_off() + + for i in range(dns_client.shut_down_duration + 1): + dns_client.apply_timestep(timestep=i) + + assert dns_client.operating_state is NodeOperatingState.OFF + assert dns_client_service.operating_state is ServiceOperatingState.STOPPED + + assert dns_client_service.check_domain_exists("test.com") is False + + def test_dns_server_domain_name_registration(dns_server): """Test to check if the domain name registration works.""" dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] @@ -68,7 +107,9 @@ def test_dns_server_domain_name_registration(dns_server): def test_dns_client_check_domain_in_cache(dns_client): """Test to make sure that the check_domain_in_cache returns the correct values.""" + dns_client.operating_state = NodeOperatingState.ON dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service.start() # add a domain to the dns client cache dns_client_service.add_domain_to_cache("real-domain.com", IPv4Address("192.168.1.12")) diff --git a/tests/unit_tests/_primaite/_simulator/test_sim_conatiner.py b/tests/unit_tests/_primaite/_simulator/test_sim_container.py similarity index 100% rename from tests/unit_tests/_primaite/_simulator/test_sim_conatiner.py rename to tests/unit_tests/_primaite/_simulator/test_sim_container.py From 355cbedbae9d17d33ae0d099d41d65709cd9d2ac Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 24 Nov 2023 15:17:08 +0000 Subject: [PATCH 029/107] #2068: Further typo and formatting changes. --- docs/source/game_layer.rst | 1 + docs/source/request_system.rst | 6 +++--- .../system/data_manipulation_bot.rst | 4 +++- .../system/database_client_server.rst | 9 +-------- docs/source/state_system.rst | 4 ++-- src/primaite/exceptions.py | 2 +- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/source/game_layer.rst b/docs/source/game_layer.rst index 18b42e7b..cdae17dd 100644 --- a/docs/source/game_layer.rst +++ b/docs/source/game_layer.rst @@ -26,6 +26,7 @@ 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. diff --git a/docs/source/request_system.rst b/docs/source/request_system.rst index cdaf2d99..1b06e2d9 100644 --- a/docs/source/request_system.rst +++ b/docs/source/request_system.rst @@ -5,12 +5,12 @@ Request System ============== -``SimComponent`` 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``. +``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``. -Just like other aspects of SimComponent, the request typess are not managed centrally for the whole simulation, but instead they are dynamically created and updated based on the nodes, links, and other components that currently exist. This was achieved in the following way: +Just like other aspects of SimComponent, the request types are not managed centrally for the whole simulation, but instead they are dynamically created and updated based on the nodes, links, and other components that currently exist. This was achieved in the following way: - API - An ``RequestType`` contains two elements: + A ``RequestType`` contains two elements: 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. diff --git a/docs/source/simulation_components/system/data_manipulation_bot.rst b/docs/source/simulation_components/system/data_manipulation_bot.rst index c9f8977a..810da3a0 100644 --- a/docs/source/simulation_components/system/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/data_manipulation_bot.rst @@ -16,15 +16,17 @@ 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. +on a database server by abusing an application's trusted database connectivity. Usage ----- - Create an instance and call ``configure`` to set: + - Target database server IP - Database password (if needed) - SQL statement payload + - Call ``run`` to connect and execute the statement. The bot handles connecting, executing the statement, and disconnecting. diff --git a/docs/source/simulation_components/system/database_client_server.rst b/docs/source/simulation_components/system/database_client_server.rst index 53687f60..0cbbddb1 100644 --- a/docs/source/simulation_components/system/database_client_server.rst +++ b/docs/source/simulation_components/system/database_client_server.rst @@ -17,8 +17,6 @@ Key capabilities - Initialises a SQLite 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. -- Executes SQL queries against the SQLite database. -- Returns query results and status codes back to clients. - Leverages the Service base class for install/uninstall, status tracking, etc. Usage @@ -33,7 +31,6 @@ Implementation - Uses SQLite for persistent storage. - Creates the database file within the node's file system. - Manages client connections in a dictionary by session ID. -- Processes SQL queries via the SQLite cursor and connection. - Returns results and status codes in a standard dictionary format. - Extends Service class for integration with ``SoftwareManager``. @@ -46,17 +43,14 @@ Key features ^^^^^^^^^^^^ - Connects to the ``DatabaseService`` via the ``SoftwareManager``. +- Handles connecting and disconnecting. - Executes SQL queries and retrieves result sets. -- Handles connecting, querying, and disconnecting. -- Provides a simple ``query`` method for running SQL. - Usage ^^^^^ - Initialise with server IP address and optional password. - Connect to the ``DatabaseService`` with ``connect``. -- Execute SQL queries via ``query``. - Retrieve results in a dictionary. - Disconnect when finished. @@ -71,6 +65,5 @@ Implementation - Leverages ``SoftwareManager`` for sending payloads over the network. - Connect and disconnect methods manage sessions. -- Provides easy interface for applications to query database. - Payloads serialised as dictionaries for transmission. - Extends base Application class. diff --git a/docs/source/state_system.rst b/docs/source/state_system.rst index de4cd093..860c9827 100644 --- a/docs/source/state_system.rst +++ b/docs/source/state_system.rst @@ -5,9 +5,9 @@ Simulation State ============== -``SimComponent`` in the simulation have a method called ``describe_state`` which returns a dictionary of the state of the component. This is used to report pertinent data that could impact 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`` reports not only it's own attributes in the state but also that 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 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 childrens' own ``describe_state`` methods. -The game layer calls ``describe_state`` on the trunk ``SimComponent`` (the top-level parent) and then pass the state to the agents once per simulation step. For this reason, all ``SimComponent`` 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`` objetcs 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: diff --git a/src/primaite/exceptions.py b/src/primaite/exceptions.py index 6aa140ba..ad9e6e5b 100644 --- a/src/primaite/exceptions.py +++ b/src/primaite/exceptions.py @@ -1,6 +1,6 @@ # © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK class PrimaiteError(Exception): - """The root PrimAITe Error.""" + """The root PrimAITE Error.""" pass From 76b3a5ab6fd89eeb9ea0169a0a27f825b90c9fb3 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 24 Nov 2023 15:43:52 +0000 Subject: [PATCH 030/107] #2068: Updated version --- src/primaite/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/VERSION b/src/primaite/VERSION index a6f4248b..dcc86c22 100644 --- a/src/primaite/VERSION +++ b/src/primaite/VERSION @@ -1 +1 @@ -3.0.0a1 +3.0.0b2dev From 2ce27080a603e31e6b1de802f88fa761aafcf155 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Fri, 24 Nov 2023 15:48:13 +0000 Subject: [PATCH 031/107] #2068: Remove reference to ARCD GATE --- docs/source/about.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/about.rst b/docs/source/about.rst index 32b54eee..e8befbaf 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -18,7 +18,6 @@ PrimAITE provides the following features: * Highly configurable network hosts, including definition of software, file system, and network interfaces, * Realistic network traffic simulation, including address and sending packets via internet protocols like TCP, UDP, ICMP, etc. * Routers with traffic routing and firewall capabilities -* Interfaces with ARCD GATE to allow training of agents * Simulation of customisable deterministic agents * Support for multiple agents, each having their own customisable observation space, action space, and reward function definition. From e62ca22cb7d45fe7fa8b03d582b3c3f8fc66f676 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 15:53:07 +0000 Subject: [PATCH 032/107] Fix data manipulation bot tests --- .../red_services/data_manipulation_bot.py | 20 +++++++++---------- .../test_data_manipulation_bot.py | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 17b89386..6db9e1aa 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -128,16 +128,16 @@ class DataManipulationBot(DatabaseClient): # perform the attack if not self.connected: self.connect() - if self.connected: - self.query(self.payload) - self.sys_log.info(f"{self.name} payload delivered: {self.payload}") - attack_successful = True - if attack_successful: - self.sys_log.info(f"{self.name}: Data manipulation successful") - self.attack_stage = DataManipulationAttackStage.COMPLETE - else: - self.sys_log.info(f"{self.name}: Data manipulation failed") - self.attack_stage = DataManipulationAttackStage.FAILED + if self.connected: + self.query(self.payload) + self.sys_log.info(f"{self.name} payload delivered: {self.payload}") + attack_successful = True + if attack_successful: + self.sys_log.info(f"{self.name}: Data manipulation successful") + self.attack_stage = DataManipulationAttackStage.COMPLETE + else: + self.sys_log.info(f"{self.name}: Data manipulation failed") + self.attack_stage = DataManipulationAttackStage.FAILED def run(self): """ diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py index 8a78beae..936f7c5c 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py @@ -4,6 +4,7 @@ from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.services.red_services.data_manipulation_bot import ( DataManipulationAttackStage, DataManipulationBot, @@ -64,6 +65,7 @@ def test_dm_bot_perform_data_manipulation_no_success(dm_bot): def test_dm_bot_perform_data_manipulation_success(dm_bot): dm_bot.attack_stage = DataManipulationAttackStage.PORT_SCAN + dm_bot.operating_state = ApplicationOperatingState.RUNNING dm_bot._perform_data_manipulation(p_of_success=1.0) From 08c1b3cfb99ceae8aefbecf2331b868393ff1f59 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 15:56:04 +0000 Subject: [PATCH 033/107] Fix code style issues --- src/primaite/game/science.py | 2 +- src/primaite/simulator/system/applications/application.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primaite/game/science.py b/src/primaite/game/science.py index f6215127..19a86237 100644 --- a/src/primaite/game/science.py +++ b/src/primaite/game/science.py @@ -1,7 +1,7 @@ from random import random -def simulate_trial(p_of_success: float): +def simulate_trial(p_of_success: float) -> bool: """ Simulates the outcome of a single trial in a Bernoulli process. diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 7f79ac2b..9a58c98a 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -66,7 +66,7 @@ class Application(IOSoftware): self.operating_state = ApplicationOperatingState.RUNNING def _application_loop(self): - """THe main application loop.""" + """The main application loop.""" pass def close(self) -> None: From afce6ca5159db196e50997207c3e4a637712e925 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 16:04:11 +0000 Subject: [PATCH 034/107] Update changelog for data manipulator bot & agent --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af5c14c..9ddd0398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ SessionManager. - `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 - Red Agent Services: - - Data Manipulator Bot - A red agent service which sends a payload to a target machine. (By default this payload is a SQL query that breaks a database) + - Data Manipulator Bot - A red agent service which sends a payload to a target machine. (By default this payload is a SQL query that breaks a database). The attack runs in stages with a random, configurable probability of succeeding. + - `DataManipulationAgent` runs the Data Manipulator Bot according to a configured start step, frequency and variance. - DNS Services: `DNSClient` and `DNSServer` - FTP Services: `FTPClient` and `FTPServer` - HTTP Services: `WebBrowser` to simulate a web client and `WebServer` From cbdaa6c44418ba5d34c2221313054785defdf978 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 24 Nov 2023 16:32:04 +0000 Subject: [PATCH 035/107] Move data manipulation agent into individual file --- .../game/agent/data_manipulation_agent.py | 0 .../game/agent/data_manipulation_bot.py | 48 +++++++++++++++++++ src/primaite/game/agent/interface.py | 44 +---------------- src/primaite/game/session.py | 3 +- 4 files changed, 51 insertions(+), 44 deletions(-) create mode 100644 src/primaite/game/agent/data_manipulation_agent.py create mode 100644 src/primaite/game/agent/data_manipulation_bot.py diff --git a/src/primaite/game/agent/data_manipulation_agent.py b/src/primaite/game/agent/data_manipulation_agent.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/game/agent/data_manipulation_bot.py b/src/primaite/game/agent/data_manipulation_bot.py new file mode 100644 index 00000000..51221154 --- /dev/null +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -0,0 +1,48 @@ +import random +from typing import Dict, List, Tuple + +from gymnasium.core import ObsType + +from primaite.game.agent.interface import AbstractScriptedAgent +from primaite.simulator.system.services.red_services.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 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) + + def _set_next_execution_timestep(self, timestep: int) -> None: + """Set the next execution timestep with a configured random variance. + + :param timestep: The timestep to add variance to. + """ + random_timestep_increment = random.randint( + -self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance + ) + self.next_execution_timestep = timestep + random_timestep_increment + + def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: + """Randomly sample an action from the action space. + + :param obs: _description_ + :type obs: ObsType + :param reward: _description_, defaults to None + :type reward: float, optional + :return: _description_ + :rtype: Tuple[str, Dict] + """ + current_timestep = self.action_manager.session.step_counter + + if current_timestep < self.next_execution_timestep: + return "DONOTHING", {"dummy": 0} + + self._set_next_execution_timestep(current_timestep + self.agent_settings.start_settings.frequency) + + return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index b321b17c..6e783725 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -1,5 +1,4 @@ """Interface for agents.""" -import random from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, TYPE_CHECKING @@ -11,7 +10,7 @@ from primaite.game.agent.observations import ObservationManager from primaite.game.agent.rewards import RewardFunction if TYPE_CHECKING: - from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot + pass class AgentStartSettings(BaseModel): @@ -183,47 +182,6 @@ class ProxyAgent(AbstractAgent): self.most_recent_action = action -class DataManipulationAgent(AbstractScriptedAgent): - """Agent that uses a DataManipulationBot to perform an SQL injection attack.""" - - data_manipulation_bots: List["DataManipulationBot"] = [] - next_execution_timestep: int = 0 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self._set_next_execution_timestep(self.agent_settings.start_settings.start_step) - - def _set_next_execution_timestep(self, timestep: int) -> None: - """Set the next execution timestep with a configured random variance. - - :param timestep: The timestep to add variance to. - """ - random_timestep_increment = random.randint( - -self.agent_settings.start_settings.variance, self.agent_settings.start_settings.variance - ) - self.next_execution_timestep = timestep + random_timestep_increment - - def get_action(self, obs: ObsType, reward: float = None) -> Tuple[str, Dict]: - """Randomly sample an action from the action space. - - :param obs: _description_ - :type obs: ObsType - :param reward: _description_, defaults to None - :type reward: float, optional - :return: _description_ - :rtype: Tuple[str, Dict] - """ - current_timestep = self.action_manager.session.step_counter - - if current_timestep < self.next_execution_timestep: - return "DONOTHING", {"dummy": 0} - - self._set_next_execution_timestep(current_timestep + self.agent_settings.start_settings.frequency) - - return "NODE_APPLICATION_EXECUTE", {"node_id": 0, "application_id": 0} - - class AbstractGATEAgent(AbstractAgent): """Base class for actors controlled via external messages, such as RL policies.""" diff --git a/src/primaite/game/session.py b/src/primaite/game/session.py index f0dcdd61..095458b7 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/session.py @@ -11,7 +11,8 @@ from pydantic import BaseModel, ConfigDict from primaite import getLogger from primaite.game.agent.actions import ActionManager -from primaite.game.agent.interface import AbstractAgent, AgentSettings, DataManipulationAgent, ProxyAgent, RandomAgent +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.io import SessionIO, SessionIOSettings From cd49f1eb85c49c43af1c9521df8e0af85705f113 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 25 Nov 2023 13:19:32 +0000 Subject: [PATCH 036/107] #2064: Apply PR suggestions --- .../system/services/dns/dns_client.py | 1 + .../red_services/data_manipulation_bot.py | 2 +- .../system/test_ftp_client_server.py | 20 +++++++++---------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index a0965009..2c3716e9 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -62,6 +62,7 @@ class DNSClient(Service): return False self.dns_cache[domain_name] = ip_address + return True def check_domain_exists( self, diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index f6662762..8dc2eeab 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -46,7 +46,7 @@ class DataManipulationBot(DatabaseClient): self.sys_log.error(f"Failed to start the {self.name} as it requires both a target_ip_address and payload.") def attack(self): - """Run the datab manipulation attack.""" + """Run the data manipulation attack.""" if not self.connected: self.connect() if self.connected: diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index d8968b2d..b2cdbc06 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -15,10 +15,10 @@ def test_ftp_client_store_file_in_server(uc2_network): backup_server: Server = uc2_network.get_node_by_hostname("backup_server") ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] - ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = backup_server.software_manager.software["FTPServer"] assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server.operating_state == ServiceOperatingState.RUNNING + assert ftp_server_service.operating_state == ServiceOperatingState.RUNNING # create file on ftp client ftp_client.file_system.create_file(file_name="test_file.txt") @@ -31,7 +31,7 @@ def test_ftp_client_store_file_in_server(uc2_network): dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, ) - assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") + assert ftp_server_service.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") def test_ftp_client_retrieve_file_from_server(uc2_network): @@ -42,13 +42,13 @@ def test_ftp_client_retrieve_file_from_server(uc2_network): backup_server: Server = uc2_network.get_node_by_hostname("backup_server") ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] - ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = backup_server.software_manager.software["FTPServer"] assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server.operating_state == ServiceOperatingState.RUNNING + assert ftp_server_service.operating_state == ServiceOperatingState.RUNNING # create file on ftp server - ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + ftp_server_service.file_system.create_file(file_name="test_file.txt", folder_name="file_share") assert ftp_client.request_file( src_folder_name="file_share", @@ -68,13 +68,13 @@ def test_ftp_client_tries_to_connect_to_offline_server(uc2_network): backup_server: Server = uc2_network.get_node_by_hostname("backup_server") ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] - ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = backup_server.software_manager.software["FTPServer"] assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server.operating_state == ServiceOperatingState.RUNNING + assert ftp_server_service.operating_state == ServiceOperatingState.RUNNING # create file on ftp server - ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + ftp_server_service.file_system.create_file(file_name="test_file.txt", folder_name="file_share") backup_server.power_off() @@ -82,7 +82,7 @@ def test_ftp_client_tries_to_connect_to_offline_server(uc2_network): uc2_network.apply_timestep(timestep=i) assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server.operating_state == ServiceOperatingState.STOPPED + assert ftp_server_service.operating_state == ServiceOperatingState.STOPPED assert ( ftp_client.request_file( From ece9b14d6365c73b4278320c605e4f85113d613d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 26 Nov 2023 23:29:14 +0000 Subject: [PATCH 037/107] Resolve merge conflicts --- docs/source/primaite_session.rst | 215 +-- pyproject.toml | 1 + .../config/_package_data/example_config.yaml | 983 +++++++------- .../example_config_2_rl_agents.yaml | 1164 +++++++++++++++++ src/primaite/game/agent/actions.py | 26 +- .../game/agent/data_manipulation_bot.py | 2 +- src/primaite/game/agent/observations.py | 110 +- src/primaite/game/agent/rewards.py | 40 +- src/primaite/game/{session.py => game.py} | 238 +--- src/primaite/game/policy/__init__.py | 3 - src/primaite/main.py | 8 +- .../training_example_ray_multi_agent.ipynb | 127 ++ .../training_example_ray_single_agent.ipynb | 122 ++ .../notebooks/training_example_sb3.ipynb | 102 ++ src/primaite/notebooks/uc2_demo.ipynb | 306 +++++ src/primaite/session/__init__.py | 0 src/primaite/session/environment.py | 162 +++ src/primaite/{game => session}/io.py | 0 src/primaite/session/policy/__init__.py | 4 + .../{game => session}/policy/policy.py | 4 +- src/primaite/session/policy/rllib.py | 106 ++ src/primaite/{game => session}/policy/sb3.py | 4 +- src/primaite/session/session.py | 113 ++ .../assets/configs/bad_primaite_session.yaml | 1003 +++++++------- .../configs/eval_only_primaite_session.yaml | 1002 +++++++------- tests/assets/configs/multi_agent_session.yaml | 1156 ++++++++++++++++ .../assets/configs/test_primaite_session.yaml | 999 +++++++------- .../configs/train_only_primaite_session.yaml | 1003 +++++++------- tests/conftest.py | 3 +- .../test_rllib_multi_agent_environment.py | 45 + .../test_rllib_single_agent_environment.py | 40 + .../environments/test_sb3_environment.py | 27 + .../test_primaite_session.py | 24 +- 33 files changed, 6074 insertions(+), 3068 deletions(-) create mode 100644 src/primaite/config/_package_data/example_config_2_rl_agents.yaml rename src/primaite/game/{session.py => game.py} (71%) delete mode 100644 src/primaite/game/policy/__init__.py create mode 100644 src/primaite/notebooks/training_example_ray_multi_agent.ipynb create mode 100644 src/primaite/notebooks/training_example_ray_single_agent.ipynb create mode 100644 src/primaite/notebooks/training_example_sb3.ipynb create mode 100644 src/primaite/notebooks/uc2_demo.ipynb create mode 100644 src/primaite/session/__init__.py create mode 100644 src/primaite/session/environment.py rename src/primaite/{game => session}/io.py (100%) create mode 100644 src/primaite/session/policy/__init__.py rename src/primaite/{game => session}/policy/policy.py (93%) create mode 100644 src/primaite/session/policy/rllib.py rename src/primaite/{game => session}/policy/sb3.py (96%) create mode 100644 src/primaite/session/session.py create mode 100644 tests/assets/configs/multi_agent_session.yaml create mode 100644 tests/e2e_integration_tests/environments/test_rllib_multi_agent_environment.py create mode 100644 tests/e2e_integration_tests/environments/test_rllib_single_agent_environment.py create mode 100644 tests/e2e_integration_tests/environments/test_sb3_environment.py diff --git a/docs/source/primaite_session.rst b/docs/source/primaite_session.rst index 472a361f..f3ef0399 100644 --- a/docs/source/primaite_session.rst +++ b/docs/source/primaite_session.rst @@ -7,207 +7,28 @@ Run a PrimAITE Session ====================== +``PrimaiteSession`` allows the user to train or evaluate an RL agent on the primaite simulation with just a config file, +no code required. It manages the lifecycle of a training or evaluation session, including the setup of the environment, +policy, simulator, agents, and IO. + +If you want finer control over the RL policy, you can interface with the :py:module::`primaite.session.environment` +module directly without running a session. + + + Run --- -A PrimAITE session can be ran either with the ``primaite session`` command from the cli +A PrimAITE session can be started either with the ``primaite session`` command from the cli (See :func:`primaite.cli.session`), or by calling :func:`primaite.main.run` from a Python terminal or Jupyter Notebook. -Both the ``primaite session`` and :func:`primaite.main.run` take a training config and a lay down config as parameters. -.. note:: - 🚧 *UNDER CONSTRUCTION* 🚧 +There are two parameters that can be specified: + - ``--config``: The path to the config file to use. If not specified, the default config file is used. + - ``--agent-load-file``: The path to the pre-trained agent to load. If not specified, a new agent is created. -.. - .. code-block:: bash - :caption: Unix CLI +Outputs +------- - cd ~/primaite/2.0.0 - source ./.venv/bin/activate - primaite session --tc ./config/my_training_config.yaml --ldc ./config/my_lay_down_config.yaml - - .. code-block:: powershell - :caption: Powershell CLI - - cd ~\primaite\2.0.0 - .\.venv\Scripts\activate - primaite session --tc .\config\my_training_config.yaml --ldc .\config\my_lay_down_config.yaml - - - .. code-block:: python - :caption: Python - - from primaite.main import run - - training_config = - lay_down_config = - run(training_config, lay_down_config) - - When a session is ran, a session output sub-directory is created in the users app sessions directory (``~/primaite/2.0.0/sessions``). - The sub-directory is formatted as such: ``~/primaite/2.0.0/sessions//_/`` - - For example, when running a session at 17:30:00 on 31st January 2023, the session will output to: - ``~/primaite/2.0.0/sessions/2023-01-31/2023-01-31_17-30-00/``. - - ``primaite session`` can be ran in the terminal/command prompt without arguments. It will use the default configs in the directory ``primaite/config/example_config``. - - To run a PrimAITE session using legacy training or laydown config files, add the ``--legacy-tc`` and/or ``legacy-ldc`` options. - - - - .. code-block:: bash - :caption: Unix CLI - - cd ~/primaite/2.0.0 - source ./.venv/bin/activate - primaite session --tc ./config/my_legacy_training_config.yaml --legacy-tc --ldc ./config/my_legacy_lay_down_config.yaml --legacy-ldc - - .. code-block:: powershell - :caption: Powershell CLI - - cd ~\primaite\2.0.0 - .\.venv\Scripts\activate - primaite session --tc .\config\my_legacy_training_config.yaml --legacy-tc --ldc .\config\my_legacy_lay_down_config.yaml --legacy-ldc - - - .. code-block:: python - :caption: Python - - from primaite.main import run - - training_config = - lay_down_config = - run(training_config, lay_down_config, legacy_training_config=True, legacy_lay_down_config=True) - - - - - Outputs - ------- - - PrimAITE produces four types of outputs: - - * Session Metadata - * Results - * Diagrams - * Saved agents (training checkpoints and a final trained agent) - - - **Session Metadata** - - PrimAITE creates a ``session_metadata.json`` file that contains the following metadata: - - * **uuid** - The UUID assigned to the session upon instantiation. - * **start_datetime** - The date & time the session started in iso format. - * **end_datetime** - The date & time the session ended in iso format. - * **learning** - * **total_episodes** - The total number of training episodes completed. - * **total_time_steps** - The total number of training time steps completed. - * **evaluation** - * **total_episodes** - The total number of evaluation episodes completed. - * **total_time_steps** - The total number of evaluation time steps completed. - * **env** - * **training_config** - * **All training config items** - * **lay_down_config** - * **All lay down config items** - - - **Results** - - PrimAITE automatically creates two sets of results from each learning and evaluation session: - - * Average reward per episode - a csv file listing the average reward for each episode of the session. This provides, for example, an indication of the change over a training session of the reward value - * All transactions - a csv file listing the following values for every step of every episode: - - * Timestamp - * Episode number - * Step number - * Reward value - * Action taken (as presented by the blue agent on this step). Individual elements of the action space are presented in the format AS_X - * Initial observation space (what the blue agent observed when it decided its action) - - **Diagrams** - - * For each session, PrimAITE automatically creates a visualisation of the system / network lay down configuration. - * For each learning and evaluation task within the session, PrimAITE automatically plots the average reward per episode using PlotLY and saves it to the learning or evaluation subdirectory in the session directory. - - **Saved agents** - - For each training session, assuming the agent being trained implements the *save()* function and this function is called by the code, PrimAITE automatically saves the agent state. - - **Example Session Directory Structure** - - .. code-block:: text - - ~/ - └── primaite/ - └── 2.0.0/ - └── sessions/ - └── 2023-07-18/ - └── 2023-07-18_11-06-04/ - ├── evaluation/ - │ ├── all_transactions_2023-07-18_11-06-04.csv - │ ├── average_reward_per_episode_2023-07-18_11-06-04.csv - │ └── average_reward_per_episode_2023-07-18_11-06-04.png - ├── learning/ - │ ├── all_transactions_2023-07-18_11-06-04.csv - │ ├── average_reward_per_episode_2023-07-18_11-06-04.csv - │ ├── average_reward_per_episode_2023-07-18_11-06-04.png - │ ├── checkpoints/ - │ │ └── sb3ppo_10.zip - │ ├── SB3_PPO.zip - │ └── tensorboard_logs/ - │ ├── PPO_1/ - │ │ └── events.out.tfevents.1689674765.METD-9PMRFB3.42960.0 - │ ├── PPO_2/ - │ │ └── events.out.tfevents.1689674766.METD-9PMRFB3.42960.1 - │ ├── PPO_3/ - │ │ └── events.out.tfevents.1689674766.METD-9PMRFB3.42960.2 - │ ├── PPO_4/ - │ │ └── events.out.tfevents.1689674767.METD-9PMRFB3.42960.3 - │ ├── PPO_5/ - │ │ └── events.out.tfevents.1689674767.METD-9PMRFB3.42960.4 - │ ├── PPO_6/ - │ │ └── events.out.tfevents.1689674768.METD-9PMRFB3.42960.5 - │ ├── PPO_7/ - │ │ └── events.out.tfevents.1689674768.METD-9PMRFB3.42960.6 - │ ├── PPO_8/ - │ │ └── events.out.tfevents.1689674769.METD-9PMRFB3.42960.7 - │ ├── PPO_9/ - │ │ └── events.out.tfevents.1689674770.METD-9PMRFB3.42960.8 - │ └── PPO_10/ - │ └── events.out.tfevents.1689674770.METD-9PMRFB3.42960.9 - ├── network_2023-07-18_11-06-04.png - └── session_metadata.json - - Loading a session - ----------------- - - A previous session can be loaded by providing the **directory** of the previous session to either the ``primaite session`` command from the cli - (See :func:`primaite.cli.session`), or by calling :func:`primaite.main.run` with session_path. - - .. tabs:: - - .. code-tab:: bash - :caption: Unix CLI - - cd ~/primaite/2.0.0 - source ./.venv/bin/activate - primaite session --load "path/to/session" - - .. code-tab:: bash - :caption: Powershell CLI - - cd ~\primaite\2.0.0 - .\.venv\Scripts\activate - primaite session --load "path\to\session" - - - .. code-tab:: python - :caption: Python - - from primaite.main import run - - run(session_path=) - - When PrimAITE runs a loaded session, PrimAITE will output in the provided session directory +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, +the saved agent checkpoints, and final model. diff --git a/pyproject.toml b/pyproject.toml index 92f78ec0..3e5b959a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "tensorflow==2.12.0", "typer[all]==0.9.0", "pydantic==2.1.1", + "ray[rllib] == 2.8.0, < 3" ] [tool.setuptools.dynamic] diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 6455272c..d9896b01 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -1,8 +1,8 @@ training_config: - rl_framework: SB3 + rl_framework: RLLIB_single_agent rl_algorithm: PPO seed: 333 - n_learn_episodes: 25 + n_learn_episodes: 1 n_eval_episodes: 5 max_steps_per_episode: 128 deterministic_eval: false @@ -15,7 +15,8 @@ io_settings: checkpoint_interval: 5 -game_config: +game: + max_episode_length: 256 ports: - ARP - DNS @@ -26,522 +27,504 @@ game_config: - TCP - UDP - agents: - - ref: client_1_green_user - team: GREEN - type: GreenWebBrowsingAgent - observation_space: - type: UC2GreenObservation - action_space: - action_list: - - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com +agents: + - ref: client_1_green_user + team: GREEN + type: GreenWebBrowsingAgent + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + # + # - type: NODE_LOGON + # - type: NODE_LOGOFF + # - type: NODE_APPLICATION_EXECUTE + # options: + # execution_definition: + # target_address: arcd.com - options: - nodes: - - node_ref: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 - reward_function: - reward_components: - - type: DUMMY + reward_function: + reward_components: + - type: DUMMY - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + agent_settings: + start_settings: + start_step: 5 + frequency: 4 + variance: 3 - - ref: client_1_data_manipulation_red_bot - team: RED - type: RedDatabaseCorruptingAgent + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent - observation_space: - type: UC2RedObservation - options: - nodes: + observation_space: + type: UC2RedObservation + options: + nodes: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 - node_ref: client_1 - observations: - - logon_status - - operating_status - applications: - - application_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: security_suite + nic_num: 2 + ics: null - action_space: - action_list: - - type: DONOTHING - # + # - type: NODE_LOGON + # - type: NODE_LOGOFF + # - type: NODE_APPLICATION_EXECUTE + # options: + # execution_definition: + # target_address: arcd.com + + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: + start_step: 5 + frequency: 4 + variance: 3 + + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent + + observation_space: + type: UC2RedObservation + options: + nodes: + - node_ref: client_1 + observations: + - logon_status + - operating_status + services: + - service_ref: data_manipulation_bot + observations: + operating_status + health_status + folders: {} + + action_space: + action_list: + - type: DONOTHING + # None: """Init method for ActionManager. - :param session: Reference to the session to which the agent belongs. - :type session: PrimaiteSession + :param game: Reference to the game to which the agent belongs. + :type game: PrimaiteGame :param actions: List of action types which should be made available to the agent. :type actions: List[str] :param node_uuids: List of node UUIDs that this agent can act on. @@ -633,8 +633,8 @@ 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.session: "PrimaiteSession" = session - self.sim: Simulation = self.session.simulation + self.game: "PrimaiteGame" = game + self.sim: Simulation = self.game.simulation self.node_uuids: List[str] = node_uuids self.application_uuids: List[List[str]] = application_uuids self.protocols: List[str] = protocols @@ -874,7 +874,7 @@ class ActionManager: return nics[nic_idx] @classmethod - def from_config(cls, session: "PrimaiteSession", cfg: Dict) -> "ActionManager": + def from_config(cls, game: "PrimaiteGame", cfg: Dict) -> "ActionManager": """ Construct an ActionManager from a config definition. @@ -893,20 +893,20 @@ class ActionManager: These options are used to calculate the shape of the action space, and to provide additional information to the ActionManager which is required to convert the agent's action choice into a CAOS request. - :param session: The Primaite Session to which the agent belongs. - :type session: PrimaiteSession + :param game: The Primaite Game to which the agent belongs. + :type game: PrimaiteGame :param cfg: The action space config. :type cfg: Dict :return: The constructed ActionManager. :rtype: ActionManager """ obj = cls( - session=session, + game=game, actions=cfg["action_list"], # node_uuids=cfg["options"]["node_uuids"], **cfg["options"], - protocols=session.options.protocols, - ports=session.options.ports, + protocols=game.options.protocols, + ports=game.options.ports, ip_address_list=None, 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 51221154..8237ce06 100644 --- a/src/primaite/game/agent/data_manipulation_bot.py +++ b/src/primaite/game/agent/data_manipulation_bot.py @@ -38,7 +38,7 @@ class DataManipulationAgent(AbstractScriptedAgent): :return: _description_ :rtype: Tuple[str, Dict] """ - current_timestep = self.action_manager.session.step_counter + current_timestep = self.action_manager.game.step_counter if current_timestep < self.next_execution_timestep: return "DONOTHING", {"dummy": 0} diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index a74771c0..14fb2fa7 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -11,7 +11,7 @@ from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_ST _LOGGER = getLogger(__name__) if TYPE_CHECKING: - from primaite.game.session import PrimaiteSession + from primaite.game.game import PrimaiteGame class AbstractObservation(ABC): @@ -37,10 +37,10 @@ class AbstractObservation(ABC): @classmethod @abstractmethod - def from_config(cls, config: Dict, session: "PrimaiteSession"): + def from_config(cls, config: Dict, game: "PrimaiteGame"): """Create this observation space component form a serialised format. - The `session` parameter is for a the PrimaiteSession object that spawns this component. During deserialisation, + The `game` parameter is for a the PrimaiteGame object that spawns this component. During deserialisation, a subclass of this class may need to translate from a 'reference' to a UUID. """ pass @@ -91,13 +91,13 @@ class FileObservation(AbstractObservation): return spaces.Dict({"health_status": spaces.Discrete(6)}) @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession", parent_where: List[str] = None) -> "FileObservation": + 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 session: _description_ - :type session: PrimaiteSession + :param game: _description_ + :type game: PrimaiteGame :param parent_where: _description_, defaults to None :type parent_where: _type_, optional :return: _description_ @@ -149,20 +149,20 @@ class ServiceObservation(AbstractObservation): @classmethod def from_config( - cls, config: Dict, session: "PrimaiteSession", parent_where: Optional[List[str]] = None + 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 session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :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", session.ref_map_services[config["service_ref"]].uuid]) + return cls(where=parent_where + ["services", game.ref_map_services[config["service_ref"]].uuid]) class LinkObservation(AbstractObservation): @@ -219,17 +219,17 @@ class LinkObservation(AbstractObservation): return spaces.Dict({"PROTOCOLS": spaces.Dict({"ALL": spaces.Discrete(11)})}) @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "LinkObservation": + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "LinkObservation": """Create link observation from a config. :param config: Dictionary containing the configuration for this link observation. :type config: Dict - :param session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame :return: Constructed link observation :rtype: LinkObservation """ - return cls(where=["network", "links", session.ref_map_links[config["link_ref"]]]) + return cls(where=["network", "links", game.ref_map_links[config["link_ref"]]]) class FolderObservation(AbstractObservation): @@ -310,15 +310,15 @@ class FolderObservation(AbstractObservation): @classmethod def from_config( - cls, config: Dict, session: "PrimaiteSession", parent_where: Optional[List[str]], num_files_per_folder: int = 2 + 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 session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :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'] @@ -332,7 +332,7 @@ class FolderObservation(AbstractObservation): where = parent_where + ["folders", config["folder_name"]] file_configs = config["files"] - files = [FileObservation.from_config(config=f, session=session, parent_where=where) for f in file_configs] + 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) @@ -376,15 +376,13 @@ class NicObservation(AbstractObservation): return spaces.Dict({"nic_status": spaces.Discrete(3)}) @classmethod - def from_config( - cls, config: Dict, session: "PrimaiteSession", parent_where: Optional[List[str]] - ) -> "NicObservation": + 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 session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :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]] @@ -515,7 +513,7 @@ class NodeObservation(AbstractObservation): def from_config( cls, config: Dict, - session: "PrimaiteSession", + game: "PrimaiteGame", parent_where: Optional[List[str]] = None, num_services_per_node: int = 2, num_folders_per_node: int = 2, @@ -526,8 +524,8 @@ class NodeObservation(AbstractObservation): :param config: Dictionary containing the configuration for this node observation. :type config: Dict - :param session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :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]] @@ -543,24 +541,24 @@ class NodeObservation(AbstractObservation): :return: Constructed node observation :rtype: NodeObservation """ - node_uuid = session.ref_map_nodes[config["node_ref"]] + node_uuid = game.ref_map_nodes[config["node_ref"]] if parent_where is None: where = ["network", "nodes", node_uuid] else: where = parent_where + ["nodes", node_uuid] svc_configs = config.get("services", {}) - services = [ServiceObservation.from_config(config=c, session=session, parent_where=where) for c in svc_configs] + 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, session=session, parent_where=where, num_files_per_folder=num_files_per_folder + config=c, game=game, parent_where=where, num_files_per_folder=num_files_per_folder ) for c in folder_configs ] - nic_uuids = session.simulation.network.nodes[node_uuid].nics.keys() + nic_uuids = game.simulation.network.nodes[node_uuid].nics.keys() nic_configs = [{"nic_uuid": n for n in nic_uuids}] if nic_uuids else [] - nics = [NicObservation.from_config(config=c, session=session, parent_where=where) for c in nic_configs] + nics = [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, @@ -694,13 +692,13 @@ class AclObservation(AbstractObservation): ) @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "AclObservation": + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "AclObservation": """Generate ACL observation from a config. :param config: Dictionary containing the configuration for this ACL observation. :type config: Dict - :param session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame :return: Observation object :rtype: AclObservation """ @@ -709,15 +707,15 @@ class AclObservation(AbstractObservation): for ip_idx, ip_map_config in enumerate(config["ip_address_order"]): node_ref = ip_map_config["node_ref"] nic_num = ip_map_config["nic_num"] - node_obj = session.simulation.network.nodes[session.ref_map_nodes[node_ref]] + node_obj = game.simulation.network.nodes[game.ref_map_nodes[node_ref]] nic_obj = node_obj.ethernet_port[nic_num] node_ip_to_idx[nic_obj.ip_address] = ip_idx + 2 - router_uuid = session.ref_map_nodes[config["router_node_ref"]] + router_uuid = game.ref_map_nodes[config["router_node_ref"]] return cls( node_ip_to_id=node_ip_to_idx, - ports=session.options.ports, - protocols=session.options.protocols, + ports=game.options.ports, + protocols=game.options.protocols, where=["network", "nodes", router_uuid, "acl", "acl"], num_rules=max_acl_rules, ) @@ -740,7 +738,7 @@ class NullObservation(AbstractObservation): return spaces.Discrete(1) @classmethod - def from_config(cls, config: Dict, session: Optional["PrimaiteSession"] = None) -> "NullObservation": + def from_config(cls, config: Dict, game: Optional["PrimaiteGame"] = None) -> "NullObservation": """ Create null observation from a config. @@ -836,14 +834,14 @@ class UC2BlueObservation(AbstractObservation): ) @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "UC2BlueObservation": + 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 session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame :return: Constructed UC2 blue observation :rtype: UC2BlueObservation """ @@ -855,7 +853,7 @@ class UC2BlueObservation(AbstractObservation): nodes = [ NodeObservation.from_config( config=n, - session=session, + 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, @@ -865,13 +863,13 @@ class UC2BlueObservation(AbstractObservation): ] link_configs = config["links"] - links = [LinkObservation.from_config(config=link, session=session) for link in link_configs] + links = [LinkObservation.from_config(config=link, game=game) for link in link_configs] acl_config = config["acl"] - acl = AclObservation.from_config(config=acl_config, session=session) + acl = AclObservation.from_config(config=acl_config, game=game) ics_config = config["ics"] - ics = ICSObservation.from_config(config=ics_config, session=session) + ics = ICSObservation.from_config(config=ics_config, game=game) new = cls(nodes=nodes, links=links, acl=acl, ics=ics, where=["network"]) return new @@ -907,17 +905,17 @@ class UC2RedObservation(AbstractObservation): ) @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "UC2RedObservation": + 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 session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :param game: Reference to the PrimaiteGame object that spawned this observation. + :type game: PrimaiteGame """ node_configs = config["nodes"] - nodes = [NodeObservation.from_config(config=cfg, session=session) for cfg in node_configs] + nodes = [NodeObservation.from_config(config=cfg, game=game) for cfg in node_configs] return cls(nodes=nodes, where=["network"]) @@ -966,7 +964,7 @@ class ObservationManager: return self.obs.space @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "ObservationManager": + 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. @@ -974,14 +972,14 @@ class ObservationManager: UC2BlueObservation, UC2RedObservation, UC2GreenObservation) The other key is 'options' which are passed to the constructor of the selected observation class. :type config: Dict - :param session: Reference to the PrimaiteSession object that spawned this observation. - :type session: PrimaiteSession + :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", {}), session=session)) + return cls(UC2BlueObservation.from_config(config.get("options", {}), game=game)) elif config["type"] == "UC2RedObservation": - return cls(UC2RedObservation.from_config(config.get("options", {}), session=session)) + return cls(UC2RedObservation.from_config(config.get("options", {}), game=game)) elif config["type"] == "UC2GreenObservation": - return cls(UC2GreenObservation.from_config(config.get("options", {}), session=session)) + return cls(UC2GreenObservation.from_config(config.get("options", {}), game=game)) else: raise ValueError("Observation space type invalid") diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index da1331b0..8a1c2da4 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -34,7 +34,7 @@ from primaite.game.agent.utils import access_from_nested_dict, NOT_PRESENT_IN_ST _LOGGER = getLogger(__name__) if TYPE_CHECKING: - from primaite.game.session import PrimaiteSession + from primaite.game.game import PrimaiteGame class AbstractReward: @@ -47,13 +47,13 @@ class AbstractReward: @classmethod @abstractmethod - def from_config(cls, config: dict, session: "PrimaiteSession") -> "AbstractReward": + def from_config(cls, config: dict, game: "PrimaiteGame") -> "AbstractReward": """Create a reward function component from a config dictionary. :param config: dict of options for the reward component's constructor :type config: dict - :param session: Reference to the PrimAITE Session object - :type session: PrimaiteSession + :param game: Reference to the PrimAITE Game object + :type game: PrimaiteGame :return: The reward component. :rtype: AbstractReward """ @@ -68,13 +68,13 @@ class DummyReward(AbstractReward): return 0.0 @classmethod - def from_config(cls, config: dict, session: "PrimaiteSession") -> "DummyReward": + def from_config(cls, config: dict, game: "PrimaiteGame") -> "DummyReward": """Create a reward function component from a config dictionary. :param config: dict of options for the reward component's constructor. Should be empty. :type config: dict - :param session: Reference to the PrimAITE Session object - :type session: PrimaiteSession + :param game: Reference to the PrimAITE Game object + :type game: PrimaiteGame """ return cls() @@ -119,13 +119,13 @@ class DatabaseFileIntegrity(AbstractReward): return 0 @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "DatabaseFileIntegrity": + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "DatabaseFileIntegrity": """Create a reward function component from a config dictionary. :param config: dict of options for the reward component's constructor :type config: Dict - :param session: Reference to the PrimAITE Session object - :type session: PrimaiteSession + :param game: Reference to the PrimAITE Game object + :type game: PrimaiteGame :return: The reward component. :rtype: DatabaseFileIntegrity """ @@ -147,7 +147,7 @@ class DatabaseFileIntegrity(AbstractReward): f"{cls.__name__} could not be initialised from config because file_name parameter was not specified" ) return DummyReward() # TODO: better error handling - node_uuid = session.ref_map_nodes[node_ref] + node_uuid = game.ref_map_nodes[node_ref] if not node_uuid: _LOGGER.error( ( @@ -193,13 +193,13 @@ class WebServer404Penalty(AbstractReward): return 0.0 @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "WebServer404Penalty": + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "WebServer404Penalty": """Create a reward function component from a config dictionary. :param config: dict of options for the reward component's constructor :type config: Dict - :param session: Reference to the PrimAITE Session object - :type session: PrimaiteSession + :param game: Reference to the PrimAITE Game object + :type game: PrimaiteGame :return: The reward component. :rtype: WebServer404Penalty """ @@ -212,8 +212,8 @@ class WebServer404Penalty(AbstractReward): ) _LOGGER.warn(msg) return DummyReward() # TODO: should we error out with incorrect inputs? Probably! - node_uuid = session.ref_map_nodes[node_ref] - service_uuid = session.ref_map_services[service_ref].uuid + node_uuid = game.ref_map_nodes[node_ref] + service_uuid = game.ref_map_services[service_ref].uuid if not (node_uuid and service_uuid): msg = ( f"{cls.__name__} could not be initialised because node {node_ref} and service {service_ref} were not" @@ -265,13 +265,13 @@ class RewardFunction: return self.current_reward @classmethod - def from_config(cls, config: Dict, session: "PrimaiteSession") -> "RewardFunction": + def from_config(cls, config: Dict, game: "PrimaiteGame") -> "RewardFunction": """Create a reward function from a config dictionary. :param config: dict of options for the reward manager's constructor :type config: Dict - :param session: Reference to the PrimAITE Session object - :type session: PrimaiteSession + :param game: Reference to the PrimAITE Game object + :type game: PrimaiteGame :return: The reward manager. :rtype: RewardFunction """ @@ -281,6 +281,6 @@ class RewardFunction: rew_type = rew_component_cfg["type"] weight = rew_component_cfg.get("weight", 1.0) rew_class = cls.__rew_class_identifiers[rew_type] - rew_instance = rew_class.from_config(config=rew_component_cfg.get("options", {}), session=session) + rew_instance = rew_class.from_config(config=rew_component_cfg.get("options", {}), game=game) new.regsiter_component(component=rew_instance, weight=weight) return new diff --git a/src/primaite/game/session.py b/src/primaite/game/game.py similarity index 71% rename from src/primaite/game/session.py rename to src/primaite/game/game.py index 095458b7..ae60bbc1 100644 --- a/src/primaite/game/session.py +++ b/src/primaite/game/game.py @@ -1,12 +1,8 @@ -"""PrimAITE session - the main entry point to training agents on PrimAITE.""" +"""PrimAITE game - Encapsulates the simulation and agents.""" from copy import deepcopy -from enum import Enum from ipaddress import IPv4Address -from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, SupportsFloat, Tuple +from typing import Dict, List -import gymnasium -from gymnasium.core import ActType, ObsType from pydantic import BaseModel, ConfigDict from primaite import getLogger @@ -15,8 +11,6 @@ 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.io import SessionIO, SessionIOSettings -from primaite.game.policy.policy import PolicyABC from primaite.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import ACLAction, Router @@ -40,65 +34,7 @@ from primaite.simulator.system.services.web_server.web_server import WebServer _LOGGER = getLogger(__name__) -class PrimaiteGymEnv(gymnasium.Env): - """ - Thin wrapper env to provide agents with a gymnasium API. - - This is always a single agent environment since gymnasium is a single agent API. Therefore, we can make some - assumptions about the agent list always having a list of length 1. - """ - - def __init__(self, session: "PrimaiteSession", agents: List[ProxyAgent]): - """Initialise the environment.""" - super().__init__() - self.session: "PrimaiteSession" = session - self.agent: ProxyAgent = agents[0] - - 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 - self.agent.store_action(action) - # apply_agent_actions accesses the action we just stored - self.session.apply_agent_actions() - self.session.advance_timestep() - state = self.session.get_sim_state() - self.session.update_agents(state) - - next_obs = self._get_obs() - reward = self.agent.reward_function.current_reward - terminated = False - truncated = self.session.calculate_truncated() - info = {} - - return next_obs, reward, terminated, truncated, info - - def reset(self, seed: Optional[int] = None) -> Tuple[ObsType, Dict[str, Any]]: - """Reset the environment.""" - self.session.reset() - state = self.session.get_sim_state() - self.session.update_agents(state) - next_obs = self._get_obs() - info = {} - return next_obs, info - - @property - def action_space(self) -> gymnasium.Space: - """Return the action space of the environment.""" - return self.agent.action_manager.space - - @property - def observation_space(self) -> gymnasium.Space: - """Return the observation space of the environment.""" - return gymnasium.spaces.flatten_space(self.agent.observation_manager.space) - - def _get_obs(self) -> ObsType: - """Return the current observation.""" - unflat_space = self.agent.observation_manager.space - unflat_obs = self.agent.observation_manager.current_observation - return gymnasium.spaces.flatten(unflat_space, unflat_obs) - - -class PrimaiteSessionOptions(BaseModel): +class PrimaiteGameOptions(BaseModel): """ Global options which are applicable to all of the agents in the game. @@ -107,40 +43,20 @@ class PrimaiteSessionOptions(BaseModel): model_config = ConfigDict(extra="forbid") + max_episode_length: int = 256 ports: List[str] protocols: List[str] -class TrainingOptions(BaseModel): - """Options for training the RL agent.""" +class PrimaiteGame: + """ + Primaite game encapsulates the simulation and agents which interact with it. - model_config = ConfigDict(extra="forbid") - - rl_framework: Literal["SB3", "RLLIB"] - rl_algorithm: Literal["PPO", "A2C"] - n_learn_episodes: int - n_eval_episodes: Optional[int] = None - max_steps_per_episode: int - # checkpoint_freq: Optional[int] = None - deterministic_eval: bool - seed: Optional[int] - n_agents: int - agent_references: List[str] - - -class SessionMode(Enum): - """Helper to keep track of the current session mode.""" - - TRAIN = "train" - EVAL = "eval" - MANUAL = "manual" - - -class PrimaiteSession: - """The main entrypoint for PrimAITE sessions, this manages a simulation, agents, and environments.""" + Provides main logic loop for the game. However, it does not provide policy training, or a gymnasium environment. + """ def __init__(self): - """Initialise a PrimaiteSession object.""" + """Initialise a PrimaiteGame object.""" self.simulation: Simulation = Simulation() """Simulation object with which the agents will interact.""" @@ -159,15 +75,9 @@ class PrimaiteSession: self.episode_counter: int = 0 """Current episode number.""" - self.options: PrimaiteSessionOptions + self.options: PrimaiteGameOptions """Special options that apply for the entire game.""" - self.training_options: TrainingOptions - """Options specific to agent training.""" - - self.policy: PolicyABC - """The reinforcement learning policy.""" - self.ref_map_nodes: Dict[str, Node] = {} """Mapping from unique node reference name to node object. Used when parsing config files.""" @@ -180,40 +90,6 @@ class PrimaiteSession: self.ref_map_links: Dict[str, Link] = {} """Mapping from human-readable link reference to link object. Used when parsing config files.""" - self.env: PrimaiteGymEnv - """The environment that the agent can consume. Could be PrimaiteEnv.""" - - self.mode: SessionMode = SessionMode.MANUAL - """Current session mode.""" - - self.io_manager = SessionIO() - """IO manager for the session.""" - - def start_session(self) -> None: - """Commence the training session.""" - self.mode = SessionMode.TRAIN - n_learn_episodes = self.training_options.n_learn_episodes - n_eval_episodes = self.training_options.n_eval_episodes - max_steps_per_episode = self.training_options.max_steps_per_episode - - deterministic_eval = self.training_options.deterministic_eval - self.policy.learn( - n_episodes=n_learn_episodes, - timesteps_per_episode=max_steps_per_episode, - ) - self.save_models() - - self.mode = SessionMode.EVAL - if n_eval_episodes > 0: - self.policy.eval(n_episodes=n_eval_episodes, deterministic=deterministic_eval) - - self.mode = SessionMode.MANUAL - - def save_models(self) -> None: - """Save the RL models.""" - save_path = self.io_manager.generate_model_save_path("temp_model_name") - self.policy.save(save_path) - def step(self): """ Perform one step of the simulation/agent loop. @@ -232,7 +108,7 @@ class PrimaiteSession: single-agent gym, make sure to update the ProxyAgent's action with the action before calling ``self.apply_agent_actions()``. """ - _LOGGER.debug(f"Stepping primaite session. Step counter: {self.step_counter}") + _LOGGER.debug(f"Stepping. Step counter: {self.step_counter}") # Get the current state of the simulation sim_state = self.get_sim_state() @@ -274,29 +150,29 @@ class PrimaiteSession: def calculate_truncated(self) -> bool: """Calculate whether the episode is truncated.""" current_step = self.step_counter - max_steps = self.training_options.max_steps_per_episode + max_steps = self.options.max_episode_length if current_step >= max_steps: return True return False def reset(self) -> None: - """Reset the session, this will reset the simulation.""" + """Reset the game, this will reset the simulation.""" self.episode_counter += 1 self.step_counter = 0 - _LOGGER.debug(f"Restting primaite session, episode = {self.episode_counter}") + _LOGGER.debug(f"Resetting primaite game, episode = {self.episode_counter}") self.simulation = deepcopy(self._simulation_initial_state) def close(self) -> None: - """Close the session, this will stop the env and close the simulation.""" + """Close the game, this will close the simulation.""" return NotImplemented @classmethod - def from_config(cls, cfg: dict, agent_load_path: Optional[str] = None) -> "PrimaiteSession": - """Create a PrimaiteSession object from a config dictionary. + def from_config(cls, cfg: Dict) -> "PrimaiteGame": + """Create a PrimaiteGame object from a config dictionary. The config dictionary should have the following top-level keys: 1. training_config: options for training the RL agent. - 2. game_config: options for the game itself. Used by PrimaiteSession. + 2. game_config: options for the game itself. Used by PrimaiteGame. 3. simulation: defines the network topology and the initial state of the simulation. The specification for each of the three major areas is described in a separate documentation page. @@ -304,26 +180,19 @@ class PrimaiteSession: :param cfg: The config dictionary. :type cfg: dict - :return: A PrimaiteSession object. - :rtype: PrimaiteSession + :return: A PrimaiteGame object. + :rtype: PrimaiteGame """ - sess = cls() - sess.options = PrimaiteSessionOptions( - ports=cfg["game_config"]["ports"], - protocols=cfg["game_config"]["protocols"], - ) - sess.training_options = TrainingOptions(**cfg["training_config"]) + game = cls() + game.options = PrimaiteGameOptions(**cfg["game"]) - # READ IO SETTINGS (this sets the global session path as well) # TODO: GLOBAL SIDE EFFECTS... - io_settings = cfg.get("io_settings", {}) - sess.io_manager.settings = SessionIOSettings(**io_settings) - - sim = sess.simulation + # 1. create simulation + sim = game.simulation net = sim.network - sess.ref_map_nodes: Dict[str, Node] = {} - sess.ref_map_services: Dict[str, Service] = {} - sess.ref_map_links: Dict[str, Link] = {} + game.ref_map_nodes: Dict[str, Node] = {} + game.ref_map_services: Dict[str, Service] = {} + game.ref_map_links: Dict[str, Link] = {} nodes_cfg = cfg["simulation"]["network"]["nodes"] links_cfg = cfg["simulation"]["network"]["links"] @@ -400,7 +269,7 @@ class PrimaiteSession: print(f"installing {service_type} on node {new_node.hostname}") new_node.software_manager.install(service_types_mapping[service_type]) new_service = new_node.software_manager.software[service_type] - sess.ref_map_services[service_ref] = new_service + game.ref_map_services[service_ref] = new_service else: print(f"service type not found {service_type}") # service-dependent options @@ -434,7 +303,7 @@ class PrimaiteSession: if application_type in application_types_mapping: new_node.software_manager.install(application_types_mapping[application_type]) new_application = new_node.software_manager.software[application_type] - sess.ref_map_applications[application_ref] = new_application + game.ref_map_applications[application_ref] = new_application else: print(f"application type not found {application_type}") @@ -442,7 +311,7 @@ class PrimaiteSession: if "options" in application_cfg: opt = application_cfg["options"] new_application.configure( - server_ip_address=opt.get("server_ip"), + server_ip_address=IPv4Address(opt.get("server_ip")), payload=opt.get("payload"), 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")), @@ -453,7 +322,7 @@ class PrimaiteSession: net.add_node(new_node) new_node.power_on() - sess.ref_map_nodes[ + game.ref_map_nodes[ node_ref ] = ( new_node.uuid @@ -461,8 +330,8 @@ class PrimaiteSession: # 2. create links between nodes for link_cfg in links_cfg: - node_a = net.nodes[sess.ref_map_nodes[link_cfg["endpoint_a_ref"]]] - node_b = net.nodes[sess.ref_map_nodes[link_cfg["endpoint_b_ref"]]] + node_a = net.nodes[game.ref_map_nodes[link_cfg["endpoint_a_ref"]]] + node_b = net.nodes[game.ref_map_nodes[link_cfg["endpoint_b_ref"]]] if isinstance(node_a, Switch): endpoint_a = node_a.switch_ports[link_cfg["endpoint_a_port"]] else: @@ -472,13 +341,10 @@ class PrimaiteSession: else: endpoint_b = node_b.ethernet_port[link_cfg["endpoint_b_port"]] new_link = net.connect(endpoint_a=endpoint_a, endpoint_b=endpoint_b) - sess.ref_map_links[link_cfg["ref"]] = new_link.uuid - # endpoint_a.enable() - # endpoint_b.enable() + game.ref_map_links[link_cfg["ref"]] = new_link.uuid # 3. create agents - game_cfg = cfg["game_config"] - agents_cfg = game_cfg["agents"] + agents_cfg = cfg["agents"] for agent_cfg in agents_cfg: agent_ref = agent_cfg["ref"] # noqa: F841 @@ -488,7 +354,7 @@ class PrimaiteSession: reward_function_cfg = agent_cfg["reward_function"] # CREATE OBSERVATION SPACE - obs_space = ObservationManager.from_config(observation_space_cfg, sess) + obs_space = ObservationManager.from_config(observation_space_cfg, game) # CREATE ACTION SPACE action_space_cfg["options"]["node_uuids"] = [] @@ -497,7 +363,7 @@ class PrimaiteSession: # if a list of nodes is defined, convert them from node references to node UUIDs for action_node_option in action_space_cfg.get("options", {}).pop("nodes", {}): if "node_ref" in action_node_option: - node_uuid = sess.ref_map_nodes[action_node_option["node_ref"]] + node_uuid = game.ref_map_nodes[action_node_option["node_ref"]] action_space_cfg["options"]["node_uuids"].append(node_uuid) if "applications" in action_node_option: @@ -505,7 +371,7 @@ class PrimaiteSession: for application_option in action_node_option["applications"]: # TODO: fix inconsistency with node uuids and application uuids. The node object get added to # node_uuid, whereas here the application gets added by uuid. - application_uuid = sess.ref_map_applications[application_option["application_ref"]].uuid + application_uuid = game.ref_map_applications[application_option["application_ref"]].uuid node_application_uuids.append(application_uuid) action_space_cfg["options"]["application_uuids"].append(node_application_uuids) @@ -522,12 +388,12 @@ class PrimaiteSession: if "options" in action_config: if "target_router_ref" in action_config["options"]: _target = action_config["options"]["target_router_ref"] - action_config["options"]["target_router_uuid"] = sess.ref_map_nodes[_target] + action_config["options"]["target_router_uuid"] = game.ref_map_nodes[_target] - action_space = ActionManager.from_config(sess, action_space_cfg) + action_space = ActionManager.from_config(game, action_space_cfg) # CREATE REWARD FUNCTION - rew_function = RewardFunction.from_config(reward_function_cfg, session=sess) + rew_function = RewardFunction.from_config(reward_function_cfg, game=game) agent_settings = AgentSettings.from_config(agent_cfg.get("agent_settings")) @@ -541,7 +407,7 @@ class PrimaiteSession: reward_function=rew_function, agent_settings=agent_settings, ) - sess.agents.append(new_agent) + game.agents.append(new_agent) elif agent_type == "ProxyAgent": new_agent = ProxyAgent( agent_name=agent_cfg["ref"], @@ -549,8 +415,8 @@ class PrimaiteSession: observation_space=obs_space, reward_function=rew_function, ) - sess.agents.append(new_agent) - sess.rl_agents.append(new_agent) + game.agents.append(new_agent) + game.rl_agents.append(new_agent) elif agent_type == "RedDatabaseCorruptingAgent": new_agent = DataManipulationAgent( agent_name=agent_cfg["ref"], @@ -559,18 +425,10 @@ class PrimaiteSession: reward_function=rew_function, agent_settings=agent_settings, ) - sess.agents.append(new_agent) + game.agents.append(new_agent) else: print("agent type not found") - # CREATE ENVIRONMENT - sess.env = PrimaiteGymEnv(session=sess, agents=sess.rl_agents) + game._simulation_initial_state = deepcopy(game.simulation) # noqa - # CREATE POLICY - sess.policy = PolicyABC.from_config(sess.training_options, session=sess) - if agent_load_path: - sess.policy.load(Path(agent_load_path)) - - sess._simulation_initial_state = deepcopy(sess.simulation) # noqa - - return sess + return game diff --git a/src/primaite/game/policy/__init__.py b/src/primaite/game/policy/__init__.py deleted file mode 100644 index 29196112..00000000 --- a/src/primaite/game/policy/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from primaite.game.policy.sb3 import SB3Policy - -__all__ = ["SB3Policy"] diff --git a/src/primaite/main.py b/src/primaite/main.py index 1699fe51..b63227a7 100644 --- a/src/primaite/main.py +++ b/src/primaite/main.py @@ -5,8 +5,8 @@ from pathlib import Path from typing import Optional, Union from primaite import getLogger -from primaite.config.load import load -from primaite.game.session import PrimaiteSession +from primaite.config.load import example_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: - _LOGGER.error("Please provide a config file using the --config " "argument") + args.config = example_config_path() - run(session_path=args.config) + run(args.config) diff --git a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb new file mode 100644 index 00000000..d31d53cc --- /dev/null +++ b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb @@ -0,0 +1,127 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.game.game import PrimaiteGame\n", + "import yaml\n", + "from primaite.config.load import example_config_path\n", + "\n", + "from primaite.session.environment import PrimaiteRayEnv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(example_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + "\n", + "game = PrimaiteGame.from_config(cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# gym = PrimaiteRayEnv({\"game\":game})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ray\n", + "from ray import air, tune\n", + "from ray.rllib.algorithms.ppo import PPOConfig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ray.shutdown()\n", + "ray.init()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.session.environment import PrimaiteRayMARLEnv\n", + "\n", + "\n", + "env_config = {\"game\":game}\n", + "config = (\n", + " PPOConfig()\n", + " .environment(env=PrimaiteRayMARLEnv, env_config={\"game\":game})\n", + " .rollouts(num_rollout_workers=0)\n", + " .multi_agent(\n", + " policies={agent.agent_name for agent in game.rl_agents},\n", + " policy_mapping_fn=lambda agent_id, episode, worker, **kw: agent_id,\n", + " )\n", + " .training(train_batch_size=128)\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tune.Tuner(\n", + " \"PPO\",\n", + " run_config=air.RunConfig(\n", + " stop={\"training_iteration\": 128},\n", + " checkpoint_config=air.CheckpointConfig(\n", + " checkpoint_frequency=10,\n", + " ),\n", + " ),\n", + " param_space=config\n", + ").fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/notebooks/training_example_ray_single_agent.ipynb b/src/primaite/notebooks/training_example_ray_single_agent.ipynb new file mode 100644 index 00000000..8ee16d41 --- /dev/null +++ b/src/primaite/notebooks/training_example_ray_single_agent.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.game.game import PrimaiteGame\n", + "import yaml\n", + "from primaite.config.load import example_config_path\n", + "\n", + "from primaite.session.environment import PrimaiteRayEnv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(example_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + "\n", + "game = PrimaiteGame.from_config(cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gym = PrimaiteRayEnv({\"game\":game})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ray\n", + "from ray.rllib.algorithms import ppo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ray.shutdown()\n", + "ray.init()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env_config = {\"game\":game}\n", + "config = {\n", + " \"env\" : PrimaiteRayEnv,\n", + " \"env_config\" : env_config,\n", + " \"disable_env_checking\": True,\n", + " \"num_rollout_workers\": 0,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "algo = ppo.PPO(config=config)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(5):\n", + " result = algo.train()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "algo.save(\"temp/deleteme\")" + ] + } + ], + "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/notebooks/training_example_sb3.ipynb b/src/primaite/notebooks/training_example_sb3.ipynb new file mode 100644 index 00000000..e5085c5e --- /dev/null +++ b/src/primaite/notebooks/training_example_sb3.ipynb @@ -0,0 +1,102 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.game.game import PrimaiteGame\n", + "from primaite.session.environment import PrimaiteGymEnv\n", + "import yaml" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.config.load import example_config_path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(example_config_path(), 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + "\n", + "game = PrimaiteGame.from_config(cfg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gym = PrimaiteGymEnv(game=game)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from stable_baselines3 import PPO" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = PPO('MlpPolicy', gym)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.learn(total_timesteps=1000)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model.save(\"deleteme\")" + ] + } + ], + "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/notebooks/uc2_demo.ipynb b/src/primaite/notebooks/uc2_demo.ipynb new file mode 100644 index 00000000..3950ef10 --- /dev/null +++ b/src/primaite/notebooks/uc2_demo.ipynb @@ -0,0 +1,306 @@ +{ + "cells": [ + { + "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" + ] + } + ], + "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" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "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" + ] + } + ], + "source": [ + "\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" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "db_client.run()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db_service.backup_database()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db_client.query(\"SELECT\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "db_manipulation_bot.run()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "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" + ] + } + ], + "source": [ + "my_a.increase_x(3.2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/__init__.py b/src/primaite/session/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py new file mode 100644 index 00000000..db24db60 --- /dev/null +++ b/src/primaite/session/environment.py @@ -0,0 +1,162 @@ +from typing import Any, Dict, Final, Optional, SupportsFloat, Tuple + +import gymnasium +from gymnasium.core import ActType, ObsType +from ray.rllib.env.multi_agent_env import MultiAgentEnv + +from primaite.game.agent.interface import ProxyAgent +from primaite.game.game import PrimaiteGame + + +class PrimaiteGymEnv(gymnasium.Env): + """ + Thin wrapper env to provide agents with a gymnasium API. + + This is always a single agent environment since gymnasium is a single agent API. Therefore, we can make some + assumptions about the agent list always having a list of length 1. + """ + + def __init__(self, game: PrimaiteGame): + """Initialise the environment.""" + super().__init__() + self.game: "PrimaiteGame" = game + self.agent: ProxyAgent = self.game.rl_agents[0] + + 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 + self.agent.store_action(action) + # apply_agent_actions accesses the action we just stored + 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() + reward = self.agent.reward_function.current_reward + terminated = False + truncated = self.game.calculate_truncated() + info = {} + + return next_obs, reward, terminated, truncated, info + + def reset(self, seed: Optional[int] = None) -> Tuple[ObsType, Dict[str, Any]]: + """Reset the environment.""" + self.game.reset() + state = self.game.get_sim_state() + self.game.update_agents(state) + next_obs = self._get_obs() + info = {} + return next_obs, info + + @property + def action_space(self) -> gymnasium.Space: + """Return the action space of the environment.""" + return self.agent.action_manager.space + + @property + def observation_space(self) -> gymnasium.Space: + """Return the observation space of the environment.""" + return gymnasium.spaces.flatten_space(self.agent.observation_manager.space) + + def _get_obs(self) -> ObsType: + """Return the current observation.""" + unflat_space = self.agent.observation_manager.space + unflat_obs = self.agent.observation_manager.current_observation + return gymnasium.spaces.flatten(unflat_space, unflat_obs) + + +class PrimaiteRayEnv(gymnasium.Env): + """Ray wrapper that accepts a single `env_config` parameter in init function for compatibility with Ray.""" + + def __init__(self, env_config: Dict[str, PrimaiteGame]) -> 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] + """ + self.env = PrimaiteGymEnv(game=env_config["game"]) + self.action_space = self.env.action_space + self.observation_space = self.env.observation_space + + def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: + """Reset the environment.""" + return self.env.reset(seed=seed) + + def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict]: + """Perform a step in the environment.""" + return self.env.step(action) + + +class PrimaiteRayMARLEnv(MultiAgentEnv): + """Ray Environment that inherits from MultiAgentEnv to allow training MARL systems.""" + + def __init__(self, env_config: Optional[Dict] = None) -> 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] + """ + self.game: PrimaiteGame = env_config["game"] + """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.terminateds = set() + self.truncateds = set() + self.observation_space = gymnasium.spaces.Dict( + {name: agent.observation_manager.space for name, agent in self.agents.items()} + ) + self.action_space = gymnasium.spaces.Dict( + {name: agent.action_manager.space for name, agent in self.agents.items()} + ) + super().__init__() + + def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]: + """Reset the environment.""" + self.game.reset() + state = self.game.get_sim_state() + self.game.update_agents(state) + next_obs = self._get_obs() + info = {} + return next_obs, info + + def step( + self, actions: Dict[str, ActType] + ) -> Tuple[Dict[str, ObsType], Dict[str, SupportsFloat], Dict[str, bool], Dict[str, bool], Dict]: + """Perform a step in the environment. Adherent to Ray MultiAgentEnv step API. + + :param actions: Dict of actions. The key is agent identifier and the value is a gymnasium action instance. + :type actions: Dict[str, ActType] + :return: Observations, rewards, terminateds, truncateds, and info. Each one is a dictionary keyed by agent + identifier. + :rtype: Tuple[Dict[str,ObsType], Dict[str, SupportsFloat], Dict[str,bool], Dict[str,bool], Dict] + """ + # 1. Perform actions + for agent_name, action in actions.items(): + self.agents[agent_name].store_action(action) + self.game.apply_agent_actions() + + # 2. Advance timestep + self.game.advance_timestep() + + # 3. Get next observations + state = self.game.get_sim_state() + self.game.update_agents(state) + next_obs = self._get_obs() + + # 4. Get rewards + 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 = {} + terminateds["__all__"] = len(self.terminateds) == len(self.agents) + truncateds["__all__"] = self.game.calculate_truncated() + return next_obs, rewards, terminateds, truncateds, infos + + def _get_obs(self) -> Dict[str, ObsType]: + """Return the current observation.""" + return {name: agent.observation_manager.current_observation for name, agent in self.agents.items()} diff --git a/src/primaite/game/io.py b/src/primaite/session/io.py similarity index 100% rename from src/primaite/game/io.py rename to src/primaite/session/io.py diff --git a/src/primaite/session/policy/__init__.py b/src/primaite/session/policy/__init__.py new file mode 100644 index 00000000..811c7a54 --- /dev/null +++ b/src/primaite/session/policy/__init__.py @@ -0,0 +1,4 @@ +from primaite.session.policy.rllib import RaySingleAgentPolicy +from primaite.session.policy.sb3 import SB3Policy + +__all__ = ["SB3Policy", "RaySingleAgentPolicy"] diff --git a/src/primaite/game/policy/policy.py b/src/primaite/session/policy/policy.py similarity index 93% rename from src/primaite/game/policy/policy.py rename to src/primaite/session/policy/policy.py index 249c3b52..984466d1 100644 --- a/src/primaite/game/policy/policy.py +++ b/src/primaite/session/policy/policy.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Any, Dict, Type, TYPE_CHECKING if TYPE_CHECKING: - from primaite.game.session import PrimaiteSession, TrainingOptions + from primaite.session.session import PrimaiteSession, TrainingOptions class PolicyABC(ABC): @@ -80,5 +80,3 @@ class PolicyABC(ABC): PolicyType = cls._registry[config.rl_framework] return PolicyType.from_config(config=config, session=session) - - # saving checkpoints logic will be handled here, it will invoke 'save' method which is implemented by the subclass diff --git a/src/primaite/session/policy/rllib.py b/src/primaite/session/policy/rllib.py new file mode 100644 index 00000000..be181797 --- /dev/null +++ b/src/primaite/session/policy/rllib.py @@ -0,0 +1,106 @@ +from pathlib import Path +from typing import Literal, Optional, TYPE_CHECKING + +from primaite.session.environment import PrimaiteRayEnv, PrimaiteRayMARLEnv +from primaite.session.policy.policy import PolicyABC + +if TYPE_CHECKING: + from primaite.session.session import PrimaiteSession, TrainingOptions + +import ray +from ray import air, tune +from ray.rllib.algorithms import ppo +from ray.rllib.algorithms.ppo import PPOConfig + + +class RaySingleAgentPolicy(PolicyABC, identifier="RLLIB_single_agent"): + """Single agent RL policy using Ray RLLib.""" + + def __init__(self, session: "PrimaiteSession", algorithm: Literal["PPO", "A2C"], seed: Optional[int] = None): + super().__init__(session=session) + + config = { + "env": PrimaiteRayEnv, + "env_config": {"game": session.game}, + "disable_env_checking": True, + "num_rollout_workers": 0, + } + + ray.shutdown() + ray.init() + + self._algo = ppo.PPO(config=config) + + def learn(self, n_episodes: int, timesteps_per_episode: int) -> None: + """Train the agent.""" + for ep in range(n_episodes): + self._algo.train() + + def eval(self, n_episodes: int, deterministic: bool) -> None: + """Evaluate the agent.""" + for ep in range(n_episodes): + obs, info = self.session.env.reset() + for step in range(self.session.game.options.max_episode_length): + action = self._algo.compute_single_action(observation=obs, explore=False) + obs, rew, term, trunc, info = self.session.env.step(action) + + def save(self, save_path: Path) -> None: + """Save the policy to a file.""" + self._algo.save(save_path) + + def load(self, model_path: Path) -> None: + """Load policy parameters from a file.""" + raise NotImplementedError + + @classmethod + def from_config(cls, config: "TrainingOptions", session: "PrimaiteSession") -> "RaySingleAgentPolicy": + """Create a policy from a config.""" + return cls(session=session, algorithm=config.rl_algorithm, seed=config.seed) + + +class RayMultiAgentPolicy(PolicyABC, identifier="RLLIB_multi_agent"): + """Mutli agent RL policy using Ray RLLib.""" + + def __init__(self, session: "PrimaiteSession", algorithm: Literal["PPO"], seed: Optional[int] = None): + """Initialise multi agent policy wrapper.""" + super().__init__(session=session) + + self.config = ( + PPOConfig() + .environment(env=PrimaiteRayMARLEnv, env_config={"game": session.game}) + .rollouts(num_rollout_workers=0) + .multi_agent( + policies={agent.agent_name for agent in session.game.rl_agents}, + policy_mapping_fn=lambda agent_id, episode, worker, **kw: agent_id, + ) + .training(train_batch_size=128) + ) + + def learn(self, n_episodes: int, timesteps_per_episode: int) -> None: + """Train the agent.""" + checkpoint_freq = self.session.io_manager.settings.checkpoint_interval + tune.Tuner( + "PPO", + run_config=air.RunConfig( + stop={"training_iteration": n_episodes * timesteps_per_episode}, + checkpoint_config=air.CheckpointConfig(checkpoint_frequency=checkpoint_freq), + ), + param_space=self.config, + ).fit() + + def load(self, model_path: Path) -> None: + """Load policy parameters from a file.""" + return NotImplemented + + def eval(self, n_episodes: int, deterministic: bool) -> None: + """Evaluate trained policy.""" + return NotImplemented + + def save(self, save_path: Path) -> None: + """Save policy parameters to a file.""" + return NotImplemented + + @classmethod + def from_config(cls, config: "TrainingOptions", session: "PrimaiteSession") -> "RayMultiAgentPolicy": + """Create policy from config.""" + return cls(session=session, algorithm=config.rl_algorithm, seed=config.seed) diff --git a/src/primaite/game/policy/sb3.py b/src/primaite/session/policy/sb3.py similarity index 96% rename from src/primaite/game/policy/sb3.py rename to src/primaite/session/policy/sb3.py index a4870054..051e2770 100644 --- a/src/primaite/game/policy/sb3.py +++ b/src/primaite/session/policy/sb3.py @@ -8,10 +8,10 @@ from stable_baselines3.common.callbacks import CheckpointCallback from stable_baselines3.common.evaluation import evaluate_policy from stable_baselines3.ppo import MlpPolicy as PPO_MLP -from primaite.game.policy.policy import PolicyABC +from primaite.session.policy.policy import PolicyABC if TYPE_CHECKING: - from primaite.game.session import PrimaiteSession, TrainingOptions + from primaite.session.session import PrimaiteSession, TrainingOptions class SB3Policy(PolicyABC, identifier="SB3"): diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py new file mode 100644 index 00000000..80b63ba7 --- /dev/null +++ b/src/primaite/session/session.py @@ -0,0 +1,113 @@ +from enum import Enum +from pathlib import Path +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 + +# from primaite.game.game import PrimaiteGame +from primaite.session.policy.policy import PolicyABC + + +class TrainingOptions(BaseModel): + """Options for training the RL agent.""" + + model_config = ConfigDict(extra="forbid") + + rl_framework: Literal["SB3", "RLLIB_single_agent", "RLLIB_multi_agent"] + rl_algorithm: Literal["PPO", "A2C"] + n_learn_episodes: int + n_eval_episodes: Optional[int] = None + max_steps_per_episode: int + # checkpoint_freq: Optional[int] = None + deterministic_eval: bool + seed: Optional[int] + n_agents: int + agent_references: List[str] + + +class SessionMode(Enum): + """Helper to keep track of the current session mode.""" + + TRAIN = "train" + EVAL = "eval" + MANUAL = "manual" + + +class PrimaiteSession: + """The main entrypoint for PrimAITE sessions, this manages a simulation, policy training, and environments.""" + + def __init__(self, game: PrimaiteGame): + """Initialise PrimaiteSession object.""" + self.training_options: TrainingOptions + """Options specific to agent training.""" + + self.mode: SessionMode = SessionMode.MANUAL + """Current session mode.""" + + self.env: Union[PrimaiteGymEnv, PrimaiteRayEnv, PrimaiteRayMARLEnv] + """The environment that the RL algorithm can consume.""" + + self.policy: PolicyABC + """The reinforcement learning policy.""" + + self.io_manager = SessionIO() + """IO manager for the session.""" + + self.game: PrimaiteGame = game + """Primaite Game object for managing main simulation loop and agents.""" + + def start_session(self) -> None: + """Commence the training/eval session.""" + self.mode = SessionMode.TRAIN + n_learn_episodes = self.training_options.n_learn_episodes + n_eval_episodes = self.training_options.n_eval_episodes + max_steps_per_episode = self.training_options.max_steps_per_episode + + deterministic_eval = self.training_options.deterministic_eval + self.policy.learn( + n_episodes=n_learn_episodes, + timesteps_per_episode=max_steps_per_episode, + ) + self.save_models() + + self.mode = SessionMode.EVAL + if n_eval_episodes > 0: + self.policy.eval(n_episodes=n_eval_episodes, deterministic=deterministic_eval) + + self.mode = SessionMode.MANUAL + + def save_models(self) -> None: + """Save the RL models.""" + save_path = self.io_manager.generate_model_save_path("temp_model_name") + self.policy.save(save_path) + + @classmethod + def from_config(cls, cfg: Dict, agent_load_path: Optional[str] = None) -> "PrimaiteSession": + """Create a PrimaiteSession object from a config dictionary.""" + game = PrimaiteGame.from_config(cfg) + + sess = cls(game=game) + + sess.training_options = TrainingOptions(**cfg["training_config"]) + + # READ IO SETTINGS (this sets the global session path as well) # TODO: GLOBAL SIDE EFFECTS... + io_settings = cfg.get("io_settings", {}) + sess.io_manager.settings = SessionIOSettings(**io_settings) + + # CREATE ENVIRONMENT + if sess.training_options.rl_framework == "RLLIB_single_agent": + sess.env = PrimaiteRayEnv(env_config={"game": game}) + elif sess.training_options.rl_framework == "RLLIB_multi_agent": + sess.env = PrimaiteRayMARLEnv(env_config={"game": game}) + elif sess.training_options.rl_framework == "SB3": + sess.env = PrimaiteGymEnv(game=game) + + sess.policy = PolicyABC.from_config(sess.training_options, session=sess) + if agent_load_path: + sess.policy.load(Path(agent_load_path)) + + return sess diff --git a/tests/assets/configs/bad_primaite_session.yaml b/tests/assets/configs/bad_primaite_session.yaml index 4d8e4669..9070f246 100644 --- a/tests/assets/configs/bad_primaite_session.yaml +++ b/tests/assets/configs/bad_primaite_session.yaml @@ -2,20 +2,12 @@ training_config: rl_framework: SB3 rl_algorithm: PPO se3ed: 333 # Purposeful typo to check that error is raised with bad configuration. - n_learn_episodes: 25 + n_learn_steps: 2560 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 -game_config: + +game: ports: - ARP - DNS @@ -26,522 +18,499 @@ game_config: - TCP - UDP - agents: - - ref: client_1_green_user - team: GREEN - type: GreenWebBrowsingAgent - observation_space: - type: UC2GreenObservation - action_space: - action_list: - - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com +agents: + - ref: client_1_green_user + team: GREEN + type: GreenWebBrowsingAgent + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 - options: - nodes: - - node_ref: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + reward_function: + reward_components: + - type: DUMMY - 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 - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent - - ref: client_1_data_manipulation_red_bot - team: RED - type: RedDatabaseCorruptingAgent + observation_space: + type: UC2RedObservation + options: + nodes: {} - observation_space: - type: UC2RedObservation - options: - nodes: + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + - type: NODE_FILE_DELETE + - type: NODE_FILE_CORRUPT + - type: NODE_OS_SCAN + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 - node_ref: client_1 - observations: - - logon_status - - operating_status - applications: - - application_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: security_suite + nic_num: 2 + ics: null - action_space: - action_list: - - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com +agents: + - ref: client_1_green_user + team: GREEN + type: GreenWebBrowsingAgent + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + # + # - type: NODE_LOGON + # - type: NODE_LOGOFF + # - type: NODE_APPLICATION_EXECUTE + # options: + # execution_definition: + # target_address: arcd.com - options: - nodes: - - node_ref: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 - reward_function: - reward_components: - - type: DUMMY + reward_function: + reward_components: + - type: DUMMY - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 - - ref: client_1_data_manipulation_red_bot - team: RED - type: RedDatabaseCorruptingAgent + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent - observation_space: - type: UC2RedObservation - options: - nodes: + observation_space: + type: UC2RedObservation + options: + nodes: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + - type: NODE_FILE_DELETE + - type: NODE_FILE_CORRUPT + - type: NODE_OS_SCAN + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + reward_function: + reward_components: + - type: DUMMY + + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 - node_ref: client_1 - observations: - - logon_status - - operating_status - applications: - - application_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: security_suite + nic_num: 2 + ics: null - action_space: - action_list: - - type: DONOTHING - # + # - type: NODE_LOGON + # - type: NODE_LOGOFF + # - type: NODE_APPLICATION_EXECUTE + # options: + # execution_definition: + # target_address: arcd.com + + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent + + observation_space: + type: UC2RedObservation + options: + nodes: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + - type: NODE_FILE_DELETE + - type: NODE_FILE_CORRUPT + - type: NODE_OS_SCAN + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 + + - ref: defender1 + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 + - node_ref: client_1 + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: 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_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_ref: router_1 + - type: NETWORK_ACL_REMOVERULE + options: + target_router_ref: 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: 2 + service_id: 1 + # stop webapp service + 2: + action: NODE_SERVICE_STOP + options: + node_id: 2 + service_id: 1 + # start webapp service + 3: + action: "NODE_SERVICE_START" + options: + node_id: 2 + service_id: 1 + 4: + action: "NODE_SERVICE_PAUSE" + options: + node_id: 2 + service_id: 1 + 5: + action: "NODE_SERVICE_RESUME" + options: + node_id: 2 + service_id: 1 + 6: + action: "NODE_SERVICE_RESTART" + options: + node_id: 2 + service_id: 1 + 7: + action: "NODE_SERVICE_DISABLE" + options: + node_id: 2 + service_id: 1 + 8: + action: "NODE_SERVICE_ENABLE" + options: + node_id: 2 + service_id: 1 + 9: + action: "NODE_FILE_SCAN" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 10: + action: "NODE_FILE_CHECKHASH" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 11: + action: "NODE_FILE_DELETE" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 12: + action: "NODE_FILE_REPAIR" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 13: + action: "NODE_FILE_RESTORE" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 14: + action: "NODE_FOLDER_SCAN" + options: + node_id: 3 + folder_id: 1 + 15: + action: "NODE_FOLDER_CHECKHASH" + options: + node_id: 3 + folder_id: 1 + 16: + action: "NODE_FOLDER_REPAIR" + options: + node_id: 3 + folder_id: 1 + 17: + action: "NODE_FOLDER_RESTORE" + options: + node_id: 3 + folder_id: 1 + 18: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 19: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 20: + action: "NODE_STARTUP" + options: + node_id: 6 + 21: + action: "NODE_RESET" + options: + node_id: 6 + 22: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 7 + dest_ip_id: 1 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + 23: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 8 + dest_ip_id: 1 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + 24: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 7 + dest_ip_id: 3 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 25: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 8 + dest_ip_id: 3 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 26: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 7 + dest_ip_id: 4 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 27: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 8 + dest_ip_id: 4 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 28: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 0 + 29: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 1 + 30: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 2 + 31: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 3 + 32: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 4 + 33: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 5 + 34: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 6 + 35: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 7 + 36: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 8 + 37: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 9 + 38: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 1 + nic_id: 1 + 39: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 1 + nic_id: 1 + 40: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 2 + nic_id: 1 + 41: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 2 + nic_id: 1 + 42: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 3 + nic_id: 1 + 43: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 3 + nic_id: 1 + 44: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 4 + nic_id: 1 + 45: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 4 + nic_id: 1 + 46: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 5 + nic_id: 1 + 47: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 5 + nic_id: 1 + 48: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 5 + nic_id: 2 + 49: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 5 + nic_id: 2 + 50: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 6 + nic_id: 1 + 51: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 6 + nic_id: 1 + 52: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 7 + nic_id: 1 + 53: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 7 + nic_id: 1 + + + options: + nodes: + - node_ref: router_1 + - node_ref: switch_1 + - node_ref: switch_2 + - node_ref: domain_controller + - node_ref: web_server + - node_ref: database_server + - node_ref: backup_server + - node_ref: security_suite + - node_ref: client_1 + - node_ref: 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 + + reward_function: + reward_components: + - type: DATABASE_FILE_INTEGRITY + weight: 0.5 + options: + node_ref: database_server + folder_name: database + file_name: database.db + + + - type: WEB_SERVER_404_PENALTY + weight: 0.5 + options: + node_ref: web_server + service_ref: web_server_web_service + + + agent_settings: + # ... + + - ref: defender2 + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 + - node_ref: client_1 + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: 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_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_ref: router_1 + - type: NETWORK_ACL_REMOVERULE + options: + target_router_ref: 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: 2 + service_id: 1 + # stop webapp service + 2: + action: NODE_SERVICE_STOP + options: + node_id: 2 + service_id: 1 + # start webapp service + 3: + action: "NODE_SERVICE_START" + options: + node_id: 2 + service_id: 1 + 4: + action: "NODE_SERVICE_PAUSE" + options: + node_id: 2 + service_id: 1 + 5: + action: "NODE_SERVICE_RESUME" + options: + node_id: 2 + service_id: 1 + 6: + action: "NODE_SERVICE_RESTART" + options: + node_id: 2 + service_id: 1 + 7: + action: "NODE_SERVICE_DISABLE" + options: + node_id: 2 + service_id: 1 + 8: + action: "NODE_SERVICE_ENABLE" + options: + node_id: 2 + service_id: 1 + 9: + action: "NODE_FILE_SCAN" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 10: + action: "NODE_FILE_CHECKHASH" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 11: + action: "NODE_FILE_DELETE" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 12: + action: "NODE_FILE_REPAIR" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 13: + action: "NODE_FILE_RESTORE" + options: + node_id: 3 + folder_id: 1 + file_id: 1 + 14: + action: "NODE_FOLDER_SCAN" + options: + node_id: 3 + folder_id: 1 + 15: + action: "NODE_FOLDER_CHECKHASH" + options: + node_id: 3 + folder_id: 1 + 16: + action: "NODE_FOLDER_REPAIR" + options: + node_id: 3 + folder_id: 1 + 17: + action: "NODE_FOLDER_RESTORE" + options: + node_id: 3 + folder_id: 1 + 18: + action: "NODE_OS_SCAN" + options: + node_id: 3 + 19: + action: "NODE_SHUTDOWN" + options: + node_id: 6 + 20: + action: "NODE_STARTUP" + options: + node_id: 6 + 21: + action: "NODE_RESET" + options: + node_id: 6 + 22: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 7 + dest_ip_id: 1 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + 23: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 8 + dest_ip_id: 1 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 1 + 24: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 7 + dest_ip_id: 3 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 25: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 8 + dest_ip_id: 3 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 26: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 7 + dest_ip_id: 4 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 27: + action: "NETWORK_ACL_ADDRULE" + options: + position: 1 + permission: 2 + source_ip_id: 8 + dest_ip_id: 4 + source_port_id: 1 + dest_port_id: 1 + protocol_id: 3 + 28: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 0 + 29: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 1 + 30: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 2 + 31: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 3 + 32: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 4 + 33: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 5 + 34: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 6 + 35: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 7 + 36: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 8 + 37: + action: "NETWORK_ACL_REMOVERULE" + options: + position: 9 + 38: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 1 + nic_id: 1 + 39: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 1 + nic_id: 1 + 40: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 2 + nic_id: 1 + 41: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 2 + nic_id: 1 + 42: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 3 + nic_id: 1 + 43: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 3 + nic_id: 1 + 44: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 4 + nic_id: 1 + 45: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 4 + nic_id: 1 + 46: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 5 + nic_id: 1 + 47: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 5 + nic_id: 1 + 48: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 5 + nic_id: 2 + 49: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 5 + nic_id: 2 + 50: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 6 + nic_id: 1 + 51: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 6 + nic_id: 1 + 52: + action: "NETWORK_NIC_DISABLE" + options: + node_id: 7 + nic_id: 1 + 53: + action: "NETWORK_NIC_ENABLE" + options: + node_id: 7 + nic_id: 1 + + + options: + nodes: + - node_ref: router_1 + - node_ref: switch_1 + - node_ref: switch_2 + - node_ref: domain_controller + - node_ref: web_server + - node_ref: database_server + - node_ref: backup_server + - node_ref: security_suite + - node_ref: client_1 + - node_ref: 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 + + reward_function: + reward_components: + - type: DATABASE_FILE_INTEGRITY + weight: 0.5 + options: + node_ref: database_server + folder_name: database + file_name: database.db + + + - type: WEB_SERVER_404_PENALTY + weight: 0.5 + options: + node_ref: web_server + service_ref: web_server_web_service + + + agent_settings: + # ... + + + + + +simulation: + network: + nodes: + + - ref: router_1 + type: router + hostname: router_1 + num_ports: 5 + ports: + 1: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + 2: + ip_address: 192.168.1.1 + subnet_mask: 255.255.255.0 + acl: + 0: + action: PERMIT + src_port: POSTGRES_SERVER + dst_port: POSTGRES_SERVER + 1: + action: PERMIT + src_port: DNS + dst_port: DNS + 22: + action: PERMIT + src_port: ARP + dst_port: ARP + 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: domain_controller + type: server + hostname: domain_controller + 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 + type: server + hostname: web_server + ip_address: 192.168.1.12 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.10 + dns_server: 192.168.1.10 + services: + - 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 + hostname: database_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 + + - ref: backup_server + type: server + hostname: backup_server + ip_address: 192.168.1.16 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + services: + - ref: backup_service + type: DatabaseBackup + + - ref: security_suite + type: server + hostname: security_suite + ip_address: 192.168.1.110 + subnet_mask: 255.255.255.0 + default_gateway: 192.168.1.1 + dns_server: 192.168.1.10 + nics: + 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 + 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 + applications: + - ref: data_manipulation_bot + type: DataManipulationBot + options: + port_scan_p_of_success: 0.1 + data_manipulation_p_of_success: 0.1 + payload: "DELETE" + server_ip: 192.168.1.14 + services: + - ref: client_1_dns_client + type: DNSClient + + - 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 + applications: + - ref: client_2_web_browser + type: WebBrowser + 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/assets/configs/test_primaite_session.yaml b/tests/assets/configs/test_primaite_session.yaml index 64be5488..d7e94cb6 100644 --- a/tests/assets/configs/test_primaite_session.yaml +++ b/tests/assets/configs/test_primaite_session.yaml @@ -15,7 +15,7 @@ io_settings: checkpoint_interval: 5 -game_config: +game: ports: - ARP - DNS @@ -26,522 +26,507 @@ game_config: - TCP - UDP - agents: - - ref: client_1_green_user - team: GREEN - type: GreenWebBrowsingAgent - observation_space: - type: UC2GreenObservation - action_space: - action_list: - - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com +agents: + - ref: client_1_green_user + team: GREEN + type: GreenWebBrowsingAgent + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + # + # - type: NODE_LOGON + # - type: NODE_LOGOFF + # - type: NODE_APPLICATION_EXECUTE + # options: + # execution_definition: + # target_address: arcd.com - options: - nodes: - - node_ref: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 - reward_function: - reward_components: - - type: DUMMY + reward_function: + reward_components: + - type: DUMMY - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 - - ref: client_1_data_manipulation_red_bot - team: RED - type: RedDatabaseCorruptingAgent + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent - observation_space: - type: UC2RedObservation - options: - nodes: + observation_space: + type: UC2RedObservation + options: + nodes: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + - type: NODE_FILE_DELETE + - type: NODE_FILE_CORRUPT + - type: NODE_OS_SCAN + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 - node_ref: client_1 - observations: - - logon_status - - operating_status - applications: - - application_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: security_suite + nic_num: 2 + ics: null - action_space: - action_list: - - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com +agents: + - ref: client_1_green_user + team: GREEN + type: GreenWebBrowsingAgent + observation_space: + type: UC2GreenObservation + action_space: + action_list: + - type: DONOTHING + # + # - type: NODE_LOGON + # - type: NODE_LOGOFF + # - type: NODE_APPLICATION_EXECUTE + # options: + # execution_definition: + # target_address: arcd.com - options: - nodes: - - node_ref: client_2 - max_folders_per_node: 1 - max_files_per_folder: 1 - max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + options: + nodes: + - node_ref: client_2 + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + max_nics_per_node: 2 + max_acl_rules: 10 - reward_function: - reward_components: - - type: DUMMY + reward_function: + reward_components: + - type: DUMMY - agent_settings: - start_settings: - start_step: 5 - frequency: 4 - variance: 3 + agent_settings: # options specific to this particular agent type, basically args of __init__(self) + start_settings: + start_step: 25 + frequency: 20 + variance: 5 - - ref: client_1_data_manipulation_red_bot - team: RED - type: RedDatabaseCorruptingAgent + - ref: client_1_data_manipulation_red_bot + team: RED + type: RedDatabaseCorruptingAgent - observation_space: - type: UC2RedObservation - options: - nodes: + observation_space: + type: UC2RedObservation + options: + nodes: {} + + action_space: + action_list: + - type: DONOTHING + - type: NODE_APPLICATION_EXECUTE + - type: NODE_FILE_DELETE + - type: NODE_FILE_CORRUPT + - type: NODE_OS_SCAN + options: + nodes: + - node_ref: client_1 + applications: + - application_ref: data_manipulation_bot + max_folders_per_node: 1 + max_files_per_folder: 1 + max_services_per_node: 1 + + reward_function: + reward_components: + - type: DUMMY + + 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_ref: domain_controller + services: + - service_ref: domain_controller_dns_server + - node_ref: web_server + services: + - service_ref: web_server_database_client + - node_ref: database_server + services: + - service_ref: database_service + 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 + 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_node_ref: router_1 + ip_address_order: + - node_ref: domain_controller + nic_num: 1 + - node_ref: web_server + nic_num: 1 + - node_ref: database_server + nic_num: 1 + - node_ref: backup_server + nic_num: 1 + - node_ref: security_suite + nic_num: 1 - node_ref: client_1 - observations: - - logon_status - - operating_status - applications: - - application_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} + nic_num: 1 + - node_ref: client_2 + nic_num: 1 + - node_ref: security_suite + nic_num: 2 + ics: null - action_space: - action_list: - - type: DONOTHING - # Date: Mon, 27 Nov 2023 11:38:03 +0000 Subject: [PATCH 038/107] #2064: documentation EVERYWHERE --- CHANGELOG.md | 1 + .../network/base_hardware.rst | 62 ++++++++++++++++++- .../system/data_manipulation_bot.rst | 7 ++- .../system/ftp_client_server.rst | 17 +++-- .../simulation_components/system/software.rst | 39 ++++++++++-- .../simulator/network/hardware/base.py | 2 + .../system/services/dns/dns_client.py | 4 +- .../system/test_application_on_node.py | 11 ++++ .../system/test_service_on_node.py | 11 ++++ 9 files changed, 141 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af5c14c..068c2332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ 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 ### Removed - Removed legacy simulation modules: `acl`, `common`, `environment`, `links`, `nodes`, `pol` diff --git a/docs/source/simulation_components/network/base_hardware.rst b/docs/source/simulation_components/network/base_hardware.rst index af4ec26c..ae922105 100644 --- a/docs/source/simulation_components/network/base_hardware.rst +++ b/docs/source/simulation_components/network/base_hardware.rst @@ -109,6 +109,67 @@ e.g. instant_start_node = Node(hostname="client", start_up_duration=0, shut_down_duration=0) instant_start_node.power_on() # node will still need to be powered on +.. _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 + ------------------ Network Interfaces ------------------ @@ -357,7 +418,6 @@ Creating the four nodes results in: 2023-08-08 15:50:08,359 INFO: Connected NIC 84:20:7c:ec:a5:c6/192.168.0.13 - --------------- Create Switches --------------- diff --git a/docs/source/simulation_components/system/data_manipulation_bot.rst b/docs/source/simulation_components/system/data_manipulation_bot.rst index 489f8ae5..cc120f70 100644 --- a/docs/source/simulation_components/system/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/data_manipulation_bot.rst @@ -35,9 +35,12 @@ Example .. code-block:: python client_1 = Computer( - hostname="client_1", ip_address="192.168.10.21", subnet_mask="255.255.255.0", default_gateway="192.168.10.1" + hostname="client_1", + 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 ) - client_1.power_on() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) data_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] diff --git a/docs/source/simulation_components/system/ftp_client_server.rst b/docs/source/simulation_components/system/ftp_client_server.rst index 306bc039..899af161 100644 --- a/docs/source/simulation_components/system/ftp_client_server.rst +++ b/docs/source/simulation_components/system/ftp_client_server.rst @@ -77,6 +77,7 @@ Dependencies from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.system.services.ftp.ftp_server import FTPServer 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -85,10 +86,18 @@ Example peer to peer network net = Network() - pc1 = Computer(hostname="pc1", ip_address="120.10.10.10", subnet_mask="255.255.255.0") - srv = Server(hostname="srv", ip_address="120.10.10.20", subnet_mask="255.255.255.0") - pc1.power_on() - srv.power_on() + pc1 = Computer( + hostname="pc1", + ip_address="120.10.10.10", + subnet_mask="255.255.255.0", + operating_state=NodeOperatingState.ON # initialise the computer in an ON state + ) + 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.ethernet_port[1], srv.ethernet_port[1]) Install the FTP Server diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index b2985393..1e5a0b6b 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -6,14 +6,45 @@ Software ======== +------------- +Base Software +------------- + +All 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` + +.. code-block:: python + + from primaite.simulator.network.hardware.base import Node + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState + from primaite.simulator.system.services.service import ServiceOperatingState + from primaite.simulator.system.services.web_server.web_server import WebServer + + node = Node(hostname="pc_a", start_up_duration=0, shut_down_duration=0) + + node.power_on() + assert node.operating_state is NodeOperatingState.ON + + node.software_manager.install(WebServer) + + web_server: WebServer = node.software_manager.software["WebServer"] + assert web_server.operating_state is ServiceOperatingState.RUNNING # service is immediately ran after install + + node.power_off() + assert node.operating_state is NodeOperatingState.OFF + assert web_server.operating_state is ServiceOperatingState.STOPPED # service stops when node is powered off + + node.power_on() + assert node.operating_state is NodeOperatingState.ON + assert web_server.operating_state is ServiceOperatingState.RUNNING # service turned back on when node is powered on - -Contents -######## +Services, Processes and Applications: +##################################### .. toctree:: - :maxdepth: 8 + :maxdepth: 2 database_client_server data_manipulation_bot diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ad101f1d..81272547 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1187,6 +1187,7 @@ class Node(SimComponent): self.start_up_countdown = self.start_up_duration if self.start_up_duration <= 0: + self._start_up_actions() self.operating_state = NodeOperatingState.ON self.sys_log.info("Turned on") for nic in self.nics.values(): @@ -1202,6 +1203,7 @@ class Node(SimComponent): self.shut_down_countdown = self.shut_down_duration if self.shut_down_duration <= 0: + self._shut_down_actions() self.operating_state = NodeOperatingState.OFF self.sys_log.info("Turned off") diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 2c3716e9..47196d15 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -1,5 +1,5 @@ from ipaddress import IPv4Address -from typing import Dict, Optional, Union +from typing import Dict, Optional from primaite import getLogger from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest @@ -51,7 +51,7 @@ class DNSClient(Service): """ pass - def add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address) -> Union[bool, None]: + def add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address) -> bool: """ Adds a domain name to the DNS Client cache. diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index 7ac7b492..cce586da 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -108,3 +108,14 @@ def test_server_turns_on_service(populated_node): assert computer.operating_state is NodeOperatingState.ON assert app.operating_state is ApplicationOperatingState.RUNNING + + computer.start_up_duration = 0 + computer.shut_down_duration = 0 + + computer.power_off() + assert computer.operating_state is NodeOperatingState.OFF + assert app.operating_state is ApplicationOperatingState.CLOSED + + computer.power_on() + assert computer.operating_state is NodeOperatingState.ON + assert app.operating_state is ApplicationOperatingState.RUNNING diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index b23df58b..9480c358 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -116,3 +116,14 @@ def test_server_turns_on_service(populated_node): assert server.operating_state is NodeOperatingState.ON assert service.operating_state is ServiceOperatingState.RUNNING + + server.start_up_duration = 0 + server.shut_down_duration = 0 + + server.power_off() + assert server.operating_state is NodeOperatingState.OFF + assert service.operating_state is ServiceOperatingState.STOPPED + + server.power_on() + assert server.operating_state is NodeOperatingState.ON + assert service.operating_state is ServiceOperatingState.RUNNING From 43fee236001a53a1a68c30f50e745192ee8d67e1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 27 Nov 2023 11:55:58 +0000 Subject: [PATCH 039/107] Fix incorrect order in session from config --- src/primaite/session/session.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index 80b63ba7..3919902a 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -88,16 +88,16 @@ class PrimaiteSession: @classmethod 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)) + game = PrimaiteGame.from_config(cfg) sess = cls(game=game) - + sess.io_manager = io_manager sess.training_options = TrainingOptions(**cfg["training_config"]) - # READ IO SETTINGS (this sets the global session path as well) # TODO: GLOBAL SIDE EFFECTS... - io_settings = cfg.get("io_settings", {}) - sess.io_manager.settings = SessionIOSettings(**io_settings) - # CREATE ENVIRONMENT if sess.training_options.rl_framework == "RLLIB_single_agent": sess.env = PrimaiteRayEnv(env_config={"game": game}) From 89cbc0835221f4172ae469aa9c7229b3ee8b4cb4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 27 Nov 2023 13:28:11 +0000 Subject: [PATCH 040/107] Apply suggestions from code review --- src/primaite/game/agent/actions.py | 54 +++++++++---------- .../game/agent/data_manipulation_agent.py | 0 src/primaite/game/agent/interface.py | 23 +++++--- .../red_services/data_manipulation_bot.py | 6 +-- .../test_data_manipulation_bot.py | 2 +- 5 files changed, 47 insertions(+), 38 deletions(-) delete mode 100644 src/primaite/game/agent/data_manipulation_agent.py diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 6c6cf7b2..ea992485 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -82,7 +82,7 @@ class NodeServiceAbstractAction(AbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes, "service_id": num_services} - self.verb: str + self.verb: str # define but don't initialise: defends against children classes not defining this def form_request(self, node_id: int, service_id: int) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -98,7 +98,7 @@ class NodeServiceScanAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "scan" + self.verb: str = "scan" class NodeServiceStopAction(NodeServiceAbstractAction): @@ -106,7 +106,7 @@ class NodeServiceStopAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "stop" + self.verb: str = "stop" class NodeServiceStartAction(NodeServiceAbstractAction): @@ -114,7 +114,7 @@ class NodeServiceStartAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "start" + self.verb: str = "start" class NodeServicePauseAction(NodeServiceAbstractAction): @@ -122,7 +122,7 @@ class NodeServicePauseAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "pause" + self.verb: str = "pause" class NodeServiceResumeAction(NodeServiceAbstractAction): @@ -130,7 +130,7 @@ class NodeServiceResumeAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "resume" + self.verb: str = "resume" class NodeServiceRestartAction(NodeServiceAbstractAction): @@ -138,7 +138,7 @@ class NodeServiceRestartAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "restart" + self.verb: str = "restart" class NodeServiceDisableAction(NodeServiceAbstractAction): @@ -146,7 +146,7 @@ class NodeServiceDisableAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "disable" + self.verb: str = "disable" class NodeServiceEnableAction(NodeServiceAbstractAction): @@ -154,7 +154,7 @@ class NodeServiceEnableAction(NodeServiceAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_services: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_services=num_services) - self.verb = "enable" + self.verb: str = "enable" class NodeApplicationAbstractAction(AbstractAction): @@ -169,7 +169,7 @@ class NodeApplicationAbstractAction(AbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes, "application_id": num_applications} - self.verb: str + self.verb: str # define but don't initialise: defends against children classes not defining this def form_request(self, node_id: int, application_id: int) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -185,7 +185,7 @@ class NodeApplicationExecuteAction(NodeApplicationAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_applications: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, num_applications=num_applications) - self.verb = "execute" + self.verb: str = "execute" class NodeFolderAbstractAction(AbstractAction): @@ -200,7 +200,7 @@ class NodeFolderAbstractAction(AbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, **kwargs) -> None: super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes, "folder_id": num_folders} - self.verb: str + self.verb: str # define but don't initialise: defends against children classes not defining this def form_request(self, node_id: int, folder_id: int) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -254,7 +254,7 @@ class NodeFileAbstractAction(AbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes, "folder_id": num_folders, "file_id": num_files} - self.verb: str + self.verb: str # define but don't initialise: defends against children classes not defining this def form_request(self, node_id: int, folder_id: int, file_id: int) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -271,7 +271,7 @@ class NodeFileScanAction(NodeFileAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb = "scan" + self.verb: str = "scan" class NodeFileCheckhashAction(NodeFileAbstractAction): @@ -279,7 +279,7 @@ class NodeFileCheckhashAction(NodeFileAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb = "checkhash" + self.verb: str = "checkhash" class NodeFileDeleteAction(NodeFileAbstractAction): @@ -287,7 +287,7 @@ class NodeFileDeleteAction(NodeFileAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb = "delete" + self.verb: str = "delete" class NodeFileRepairAction(NodeFileAbstractAction): @@ -295,7 +295,7 @@ class NodeFileRepairAction(NodeFileAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb = "repair" + self.verb: str = "repair" class NodeFileRestoreAction(NodeFileAbstractAction): @@ -303,7 +303,7 @@ class NodeFileRestoreAction(NodeFileAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb = "restore" + self.verb: str = "restore" class NodeFileCorruptAction(NodeFileAbstractAction): @@ -311,7 +311,7 @@ class NodeFileCorruptAction(NodeFileAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, num_folders: int, num_files: int, **kwargs) -> None: super().__init__(manager, num_nodes=num_nodes, num_folders=num_folders, num_files=num_files, **kwargs) - self.verb = "corrupt" + self.verb: str = "corrupt" class NodeAbstractAction(AbstractAction): @@ -325,7 +325,7 @@ class NodeAbstractAction(AbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes} - self.verb: str + self.verb: str # define but don't initialise: defends against children classes not defining this def form_request(self, node_id: int) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -338,7 +338,7 @@ class NodeOSScanAction(NodeAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes) - self.verb = "scan" + self.verb: str = "scan" class NodeShutdownAction(NodeAbstractAction): @@ -346,7 +346,7 @@ class NodeShutdownAction(NodeAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes) - self.verb = "shutdown" + self.verb: str = "shutdown" class NodeStartupAction(NodeAbstractAction): @@ -354,7 +354,7 @@ class NodeStartupAction(NodeAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes) - self.verb = "startup" + self.verb: str = "startup" class NodeResetAction(NodeAbstractAction): @@ -362,7 +362,7 @@ class NodeResetAction(NodeAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes) - self.verb = "reset" + self.verb: str = "reset" class NetworkACLAddRuleAction(AbstractAction): @@ -520,7 +520,7 @@ class NetworkNICAbstractAction(AbstractAction): """ super().__init__(manager=manager) self.shape: Dict[str, int] = {"node_id": num_nodes, "nic_id": max_nics_per_node} - self.verb: str + self.verb: str # define but don't initialise: defends against children classes not defining this def form_request(self, node_id: int, nic_id: int) -> List[str]: """Return the action formatted as a request which can be ingested by the PrimAITE simulation.""" @@ -543,7 +543,7 @@ class NetworkNICEnableAction(NetworkNICAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, max_nics_per_node=max_nics_per_node, **kwargs) - self.verb = "enable" + self.verb: str = "enable" class NetworkNICDisableAction(NetworkNICAbstractAction): @@ -551,7 +551,7 @@ class NetworkNICDisableAction(NetworkNICAbstractAction): def __init__(self, manager: "ActionManager", num_nodes: int, max_nics_per_node: int, **kwargs) -> None: super().__init__(manager=manager, num_nodes=num_nodes, max_nics_per_node=max_nics_per_node, **kwargs) - self.verb = "disable" + self.verb: str = "disable" class ActionManager: diff --git a/src/primaite/game/agent/data_manipulation_agent.py b/src/primaite/game/agent/data_manipulation_agent.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/primaite/game/agent/interface.py b/src/primaite/game/agent/interface.py index 6e783725..fbbe5473 100644 --- a/src/primaite/game/agent/interface.py +++ b/src/primaite/game/agent/interface.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, TYPE_CHECKING from gymnasium.core import ActType, ObsType -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from primaite.game.agent.actions import ActionManager from primaite.game.agent.observations import ObservationManager @@ -23,6 +23,21 @@ class AgentStartSettings(BaseModel): variance: int = 0 "The amount the frequency can randomly change to" + @model_validator(mode="after") + def check_variance_lt_frequency(self) -> "AgentStartSettings": + """ + Make sure variance is equal to or lower than frequency. + + This is because the calculation for the next execution time is now + (frequency +- variance). If variance were + greater than frequency, sometimes the bracketed term would be negative and the attack would never happen again. + """ + if self.variance > self.frequency: + raise ValueError( + f"Agent start settings error: variance must be lower than frequency " + f"{self.variance=}, {self.frequency=}" + ) + return self + class AgentSettings(BaseModel): """Settings for configuring the operation of an agent.""" @@ -180,9 +195,3 @@ class ProxyAgent(AbstractAgent): The environment is responsible for calling this method when it receives an action from the agent policy. """ self.most_recent_action = action - - -class AbstractGATEAgent(AbstractAgent): - """Base class for actors controlled via external messages, such as RL policies.""" - - ... diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 6db9e1aa..b0b34396 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -24,7 +24,7 @@ class DataManipulationAttackStage(IntEnum): "Represents the stage of performing a horizontal port scan on the target." ATTACKING = 3 "Stage of actively attacking the target." - COMPLETE = 4 + SUCCEEDED = 4 "Indicates the attack has been successfully completed." FAILED = 5 "Signifies that the attack has failed." @@ -134,7 +134,7 @@ class DataManipulationBot(DatabaseClient): attack_successful = True if attack_successful: self.sys_log.info(f"{self.name}: Data manipulation successful") - self.attack_stage = DataManipulationAttackStage.COMPLETE + self.attack_stage = DataManipulationAttackStage.SUCCEEDED else: self.sys_log.info(f"{self.name}: Data manipulation failed") self.attack_stage = DataManipulationAttackStage.FAILED @@ -163,7 +163,7 @@ class DataManipulationBot(DatabaseClient): self._perform_data_manipulation(p_of_success=self.data_manipulation_p_of_success) if self.repeat and self.attack_stage in ( - DataManipulationAttackStage.COMPLETE, + DataManipulationAttackStage.SUCCEEDED, DataManipulationAttackStage.FAILED, ): self.attack_stage = DataManipulationAttackStage.NOT_STARTED diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py index 936f7c5c..3b1e4aa4 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py @@ -69,5 +69,5 @@ 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.COMPLETE, DataManipulationAttackStage.FAILED) + assert dm_bot.attack_stage in (DataManipulationAttackStage.SUCCEEDED, DataManipulationAttackStage.FAILED) assert dm_bot.connected From 4d4a578555f2452c85faa72e2b40b85cd4489542 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Mon, 27 Nov 2023 13:47:59 +0000 Subject: [PATCH 041/107] #1859 - Integrated the runtime execution for web client. Added in the webclient application execution action. Now fixing http status code issues. --- .../config/_package_data/example_config.yaml | 28 +++++++++---------- src/primaite/game/game.py | 5 +++- src/primaite/session/environment.py | 2 +- src/primaite/simulator/network/networks.py | 2 ++ .../simulator/network/protocols/http.py | 4 +-- .../system/applications/database_client.py | 4 +-- .../system/applications/web_browser.py | 15 ++++++++-- .../system/services/web_server/web_server.py | 4 ++- .../system/test_web_client_server.py | 11 ++++---- 9 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index 3cea2f29..b68861e1 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -1,5 +1,5 @@ training_config: - rl_framework: RLLIB_single_agent + rl_framework: SB3 rl_algorithm: PPO seed: 333 n_learn_episodes: 1 @@ -36,22 +36,16 @@ agents: action_space: action_list: - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com - + - type: NODE_APPLICATION_EXECUTE options: nodes: - node_ref: client_2 + applications: + - application_ref: client_2_web_browser max_folders_per_node: 1 max_files_per_folder: 1 max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + max_applications_per_node: 1 reward_function: reward_components: @@ -549,19 +543,19 @@ simulation: ip_address: 192.168.10.1 subnet_mask: 255.255.255.0 acl: - 0: + 18: action: PERMIT src_port: POSTGRES_SERVER dst_port: POSTGRES_SERVER - 1: + 19: action: PERMIT src_port: DNS dst_port: DNS - 2: + 20: action: PERMIT src_port: FTP dst_port: FTP - 3: + 21: action: PERMIT src_port: HTTP dst_port: HTTP @@ -679,10 +673,14 @@ simulation: applications: - ref: client_2_web_browser type: WebBrowser + options: + target_url: http://arcd.com/users/ services: - ref: client_2_dns_client type: DNSClient + + links: - ref: router_1___switch_1 endpoint_a_ref: router_1 diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index ae60bbc1..48615ca6 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -316,6 +316,10 @@ class PrimaiteGame: port_scan_p_of_success=float(opt.get("port_scan_p_of_success", "0.1")), data_manipulation_p_of_success=float(opt.get("data_manipulation_p_of_success", "0.1")), ) + elif application_type == "WebBrowser": + if "options" in application_cfg: + opt = application_cfg["options"] + new_application.target_url = opt.get("target_url") if "nics" in node_cfg: for nic_num, nic_cfg in node_cfg["nics"].items(): new_node.connect_nic(NIC(ip_address=nic_cfg["ip_address"], subnet_mask=nic_cfg["subnet_mask"])) @@ -377,7 +381,6 @@ class PrimaiteGame: action_space_cfg["options"]["application_uuids"].append(node_application_uuids) else: action_space_cfg["options"]["application_uuids"].append([]) - # Each action space can potentially have a different list of nodes that it can apply to. Therefore, # we will pass node_uuids as a part of the action space config. # However, it's not possible to specify the node uuids directly in the config, as they are generated diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index db24db60..a5fdade9 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -37,7 +37,7 @@ class PrimaiteGymEnv(gymnasium.Env): terminated = False truncated = self.game.calculate_truncated() info = {} - + print(f"Episode: {self.game.episode_counter}, Step: {self.game.step_counter}, Reward: {reward}") return next_obs, reward, terminated, truncated, info def reset(self, seed: Optional[int] = None) -> Tuple[ObsType, Dict[str, Any]]: diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index ea767b54..446e5649 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -157,6 +157,8 @@ def arcd_uc2_network() -> Network: operating_state=NodeOperatingState.ON, ) client_2.power_on() + web_browser = client_2.software_manager["WebBrowser"] + web_browser.target_url = "http://arcd.com/users/" network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) # Domain Controller diff --git a/src/primaite/simulator/network/protocols/http.py b/src/primaite/simulator/network/protocols/http.py index 2dba2614..b88916a9 100644 --- a/src/primaite/simulator/network/protocols/http.py +++ b/src/primaite/simulator/network/protocols/http.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Enum, IntEnum from primaite.simulator.network.protocols.packet import DataPacket @@ -25,7 +25,7 @@ class HttpRequestMethod(Enum): """Apply partial modifications to a resource.""" -class HttpStatusCode(Enum): +class HttpStatusCode(IntEnum): """List of available HTTP Statuses.""" OK = 200 diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index b24b6062..37236e69 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -75,11 +75,11 @@ class DatabaseClient(Application): """ if is_reattempt: if self.connected: - self.sys_log.info(f"{self.name}: DatabaseClient connected to {server_ip_address} authorised") + self.sys_log.info(f"{self.name}: DatabaseClient connection to {server_ip_address} authorised") self.server_ip_address = server_ip_address return self.connected else: - self.sys_log.info(f"{self.name}: DatabaseClient connected to {server_ip_address} declined") + self.sys_log.info(f"{self.name}: DatabaseClient connection to {server_ip_address} declined") return False payload = {"type": "connect_request", "password": password} software_manager: SoftwareManager = self.software_manager diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ea9c3ac3..0a9c7fc3 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -2,6 +2,7 @@ from ipaddress import IPv4Address from typing import Dict, Optional from urllib.parse import urlparse +from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.http import HttpRequestMethod, HttpRequestPacket, HttpResponsePacket from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -16,6 +17,8 @@ class WebBrowser(Application): The application requests and loads web pages using its domain name and requesting IP addresses using DNS. """ + target_url: Optional[str] = None + domain_name_ip_address: Optional[IPv4Address] = None "The IP address of the domain name for the webpage." @@ -32,6 +35,14 @@ class WebBrowser(Application): super().__init__(**kwargs) self.run() + 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 + ) + + return rm + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of the WebBrowser. @@ -51,7 +62,7 @@ class WebBrowser(Application): self.domain_name_ip_address = None self.latest_response = None - def get_webpage(self, url: str) -> bool: + def get_webpage(self) -> bool: """ Retrieve the webpage. @@ -60,6 +71,7 @@ class WebBrowser(Application): :param: url: The address of the web page the browser requests :type: url: str """ + url = self.target_url # reset latest response self.latest_response = None @@ -71,7 +83,6 @@ class WebBrowser(Application): # get the IP address of the domain name via DNS dns_client: DNSClient = self.software_manager.software["DNSClient"] - domain_exists = dns_client.check_domain_exists(target_domain=parsed_url.hostname) # if domain does not exist, the request fails diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index cb1a4738..5dda82d5 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -29,8 +29,9 @@ class WebServer(Service): :rtype: Dict """ state = super().describe_state() + state["last_response_status_code"] = ( - self.last_response_status_code.value if self.last_response_status_code else None + self.last_response_status_code.value if isinstance(self.last_response_status_code, HttpStatusCode) else None ) return state @@ -84,6 +85,7 @@ class WebServer(Service): # return true if response is OK self.last_response_status_code = response.status_code + print(self.last_response_status_code) return response.status_code == HttpStatusCode.OK def _handle_get_request(self, payload: HttpRequestPacket) -> HttpResponsePacket: diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f4546cbf..991d6282 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -3,7 +3,6 @@ from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.http import HttpStatusCode from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser -from primaite.simulator.system.services.service import ServiceOperatingState def test_web_page_home_page(uc2_network): @@ -11,9 +10,10 @@ def test_web_page_home_page(uc2_network): client_1: Computer = uc2_network.get_node_by_hostname("client_1") web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] web_client.run() + web_client.target_url = "http://arcd.com/" assert web_client.operating_state == ApplicationOperatingState.RUNNING - assert web_client.get_webpage("http://arcd.com/") is True + assert web_client.get_webpage() is True # latest reponse should have status code 200 assert web_client.latest_response is not None @@ -27,7 +27,7 @@ def test_web_page_get_users_page_request_with_domain_name(uc2_network): web_client.run() assert web_client.operating_state == ApplicationOperatingState.RUNNING - assert web_client.get_webpage("http://arcd.com/users/") is True + assert web_client.get_webpage() is True # latest reponse should have status code 200 assert web_client.latest_response is not None @@ -41,11 +41,12 @@ def test_web_page_get_users_page_request_with_ip_address(uc2_network): web_client.run() web_server: Server = uc2_network.get_node_by_hostname("web_server") - web_server_ip = web_server.nics.get(next(iter(web_server.nics))).ip_address + web_server_ip = web_server.nics.get(next(iter(web_server.nics))).ip_address + web_client.target_url = f"http://{web_server_ip}/users/" assert web_client.operating_state == ApplicationOperatingState.RUNNING - assert web_client.get_webpage(f"http://{web_server_ip}/users/") is True + assert web_client.get_webpage() is True # latest reponse should have status code 200 assert web_client.latest_response is not None From 6fd37a609ac663da8795238ac41a094e993f85b5 Mon Sep 17 00:00:00 2001 From: Nick Todd Date: Mon, 27 Nov 2023 14:38:59 +0000 Subject: [PATCH 042/107] #2068: code review comments. --- docs/index.rst | 1 - docs/source/about.rst | 4 ++-- docs/source/custom_agent.rst | 14 -------------- 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 docs/source/custom_agent.rst diff --git a/docs/index.rst b/docs/index.rst index 2dfc8a65..9eae8adc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -107,7 +107,6 @@ Head over to the :ref:`getting-started` page to install and setup PrimAITE! source/primaite_session source/simulation source/game_layer - source/custom_agent source/config .. toctree:: diff --git a/docs/source/about.rst b/docs/source/about.rst index e8befbaf..3f905933 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -278,7 +278,7 @@ The game layer is built on top of the simulator and it consumes the simulation a 3. Any (Agent can take both node-based and ACL-based actions) The choice of action space used during a training session is determined in the config_[name].yaml file. **Node-Based** - The agent is able to influence the status of nodes by switching them off, resetting, or patching operating systems and services. In this instance, the action space is an Gymnasium spaces.Discrete type, as follows: + The agent is able to influence the status of nodes by switching them off, resetting, or patching operating systems and services. In this instance, the action space is a Gymnasium spaces.Discrete type, as follows: * Dictionary item {... ,1: [x1, x2, x3,x4] ...} The placeholders inside the list under the key '1' mean the following: * [0, num nodes] - Node ID (0 = nothing, node ID) @@ -286,7 +286,7 @@ The game layer is built on top of the simulator and it consumes the simulation a * [0, 3] - Action on property (0 = nothing, 1 = on / scan, 2 = off / repair, 3 = reset / patch / restore) * [0, num services] - Resolves to service ID (0 = nothing, resolves to service) **Access Control List** - The blue agent is able to influence the configuration of the Access Control List rule set (which implements a system-wide firewall). In this instance, the action space is an OpenAI spaces.Discrete type, as follows: + The blue agent is able to influence the configuration of the Access Control List rule set (which implements a system-wide firewall). In this instance, the action space is an Gymnasium spaces.Discrete type, as follows: * Dictionary item {... ,1: [x1, x2, x3, x4, x5, x6] ...} The placeholders inside the list under the key '1' mean the following: * [0, 2] - Action (0 = do nothing, 1 = create rule, 2 = delete rule) diff --git a/docs/source/custom_agent.rst b/docs/source/custom_agent.rst deleted file mode 100644 index 7a9d83c1..00000000 --- a/docs/source/custom_agent.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. only:: comment - - © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK - -Custom Agents -============= - - -Integrating a user defined blue agent -************************************* - -.. note:: - - TBA From ae5046b8fb94d1a8c787f870fa461489c5d98fef Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Mon, 27 Nov 2023 17:05:12 +0000 Subject: [PATCH 043/107] #1859 - As disccused --- src/primaite/game/agent/actions.py | 11 ++-- src/primaite/game/agent/observations.py | 2 +- src/primaite/game/agent/rewards.py | 3 +- src/primaite/game/game.py | 56 +++++++++++++------ .../simulator/network/hardware/base.py | 3 + .../system/applications/web_browser.py | 7 +++ .../system/services/web_server/web_server.py | 29 +++++++++- 7 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index ea992485..62e56c6e 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -634,7 +634,6 @@ class ActionManager: :type act_map: Optional[Dict[int, Dict]] """ self.game: "PrimaiteGame" = game - self.sim: Simulation = self.game.simulation self.node_uuids: List[str] = node_uuids self.application_uuids: List[List[str]] = application_uuids self.protocols: List[str] = protocols @@ -646,7 +645,7 @@ class ActionManager: else: self.ip_address_list = [] for node_uuid in self.node_uuids: - node_obj = self.sim.network.nodes[node_uuid] + node_obj = self.game.simulation.network.nodes[node_uuid] nics = node_obj.nics for nic_uuid, nic_obj in nics.items(): self.ip_address_list.append(nic_obj.ip_address) @@ -770,7 +769,7 @@ class ActionManager: :rtype: Optional[str] """ node_uuid = self.get_node_uuid_by_idx(node_idx) - node = self.sim.network.nodes[node_uuid] + node = self.game.simulation.network.nodes[node_uuid] folder_uuids = list(node.file_system.folders.keys()) return folder_uuids[folder_idx] if len(folder_uuids) > folder_idx else None @@ -788,7 +787,7 @@ class ActionManager: :rtype: Optional[str] """ node_uuid = self.get_node_uuid_by_idx(node_idx) - node = self.sim.network.nodes[node_uuid] + node = self.game.simulation.network.nodes[node_uuid] folder_uuids = list(node.file_system.folders.keys()) if len(folder_uuids) <= folder_idx: return None @@ -807,7 +806,7 @@ class ActionManager: :rtype: Optional[str] """ node_uuid = self.get_node_uuid_by_idx(node_idx) - node = self.sim.network.nodes[node_uuid] + node = self.game.simulation.network.nodes[node_uuid] service_uuids = list(node.services.keys()) return service_uuids[service_idx] if len(service_uuids) > service_idx else None @@ -867,7 +866,7 @@ class ActionManager: :rtype: str """ node_uuid = self.get_node_uuid_by_idx(node_idx) - node_obj = self.sim.network.nodes[node_uuid] + node_obj = self.game.simulation.network.nodes[node_uuid] nics = list(node_obj.nics.keys()) if len(nics) <= nic_idx: return None diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index 14fb2fa7..823d65d7 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -162,7 +162,7 @@ class ServiceObservation(AbstractObservation): :return: Constructed service observation :rtype: ServiceObservation """ - return cls(where=parent_where + ["services", game.ref_map_services[config["service_ref"]].uuid]) + return cls(where=parent_where + ["services", game.ref_map_services[config["service_ref"]]]) class LinkObservation(AbstractObservation): diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 8a1c2da4..7cca9116 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -25,6 +25,7 @@ the structure: service_ref: web_server_database_client ``` """ +import json from abc import abstractmethod from typing import Dict, List, Tuple, Type, TYPE_CHECKING @@ -213,7 +214,7 @@ class WebServer404Penalty(AbstractReward): _LOGGER.warn(msg) return DummyReward() # TODO: should we error out with incorrect inputs? Probably! node_uuid = game.ref_map_nodes[node_ref] - service_uuid = game.ref_map_services[service_ref].uuid + service_uuid = game.ref_map_services[service_ref] if not (node_uuid and service_uuid): msg = ( f"{cls.__name__} could not be initialised because node {node_ref} and service {service_ref} were not" diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 48615ca6..147ed499 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -59,8 +59,9 @@ class PrimaiteGame: """Initialise a PrimaiteGame object.""" self.simulation: Simulation = Simulation() """Simulation object with which the agents will interact.""" + print(f"Hello, welcome to PrimaiteGame. This is the ID of the ORIGINAL simulation {id(self.simulation)}") - self._simulation_initial_state = deepcopy(self.simulation) + self._simulation_initial_state = None """The Simulation original state (deepcopy of the original Simulation).""" self.agents: List[AbstractAgent] = [] @@ -78,16 +79,16 @@ class PrimaiteGame: self.options: PrimaiteGameOptions """Special options that apply for the entire game.""" - self.ref_map_nodes: Dict[str, Node] = {} + self.ref_map_nodes: Dict[str, str] = {} """Mapping from unique node reference name to node object. Used when parsing config files.""" - self.ref_map_services: Dict[str, Service] = {} + self.ref_map_services: Dict[str, str] = {} """Mapping from human-readable service reference to service object. Used for parsing config files.""" - self.ref_map_applications: Dict[str, Application] = {} + self.ref_map_applications: Dict[str, str] = {} """Mapping from human-readable application reference to application object. Used for parsing config files.""" - self.ref_map_links: Dict[str, Link] = {} + self.ref_map_links: Dict[str, str] = {} """Mapping from human-readable link reference to link object. Used when parsing config files.""" def step(self): @@ -161,6 +162,33 @@ class PrimaiteGame: self.step_counter = 0 _LOGGER.debug(f"Resetting primaite game, episode = {self.episode_counter}") self.simulation = deepcopy(self._simulation_initial_state) + self._reset_components_for_episode() + print("Reset") + + def _reset_components_for_episode(self): + print("Performing full reset for episode") + for node in self.simulation.network.nodes.values(): + print(f"Resetting Node: {node.hostname}") + node.reset_component_for_episode(self.episode_counter) + + # reset Node NIC + + # Reset Node Services + + # Reset Node Applications + print(f"Resetting Software...") + for application in node.software_manager.software.values(): + print(f"Resetting {application.name}") + if isinstance(application, WebBrowser): + application.do_this() + + # Reset Node FileSystem + # Reset Node FileSystemFolder's + # Reset Node FileSystemFile's + + # Reset Router + + # Reset Links def close(self) -> None: """Close the game, this will close the simulation.""" @@ -190,10 +218,6 @@ class PrimaiteGame: sim = game.simulation net = sim.network - game.ref_map_nodes: Dict[str, Node] = {} - game.ref_map_services: Dict[str, Service] = {} - game.ref_map_links: Dict[str, Link] = {} - nodes_cfg = cfg["simulation"]["network"]["nodes"] links_cfg = cfg["simulation"]["network"]["links"] for node_cfg in nodes_cfg: @@ -269,7 +293,7 @@ class PrimaiteGame: print(f"installing {service_type} on node {new_node.hostname}") new_node.software_manager.install(service_types_mapping[service_type]) new_service = new_node.software_manager.software[service_type] - game.ref_map_services[service_ref] = new_service + game.ref_map_services[service_ref] = new_service.uuid else: print(f"service type not found {service_type}") # service-dependent options @@ -303,7 +327,7 @@ class PrimaiteGame: if application_type in application_types_mapping: new_node.software_manager.install(application_types_mapping[application_type]) new_application = new_node.software_manager.software[application_type] - game.ref_map_applications[application_ref] = new_application + game.ref_map_applications[application_ref] = new_application.uuid else: print(f"application type not found {application_type}") @@ -326,11 +350,7 @@ class PrimaiteGame: net.add_node(new_node) new_node.power_on() - game.ref_map_nodes[ - node_ref - ] = ( - new_node.uuid - ) # TODO: fix inconsistency with service and link. Node gets added by uuid, but service by object + game.ref_map_nodes[node_ref] = new_node.uuid # 2. create links between nodes for link_cfg in links_cfg: @@ -375,7 +395,7 @@ class PrimaiteGame: for application_option in action_node_option["applications"]: # TODO: fix inconsistency with node uuids and application uuids. The node object get added to # node_uuid, whereas here the application gets added by uuid. - application_uuid = game.ref_map_applications[application_option["application_ref"]].uuid + application_uuid = game.ref_map_applications[application_option["application_ref"]] node_application_uuids.append(application_uuid) action_space_cfg["options"]["application_uuids"].append(node_application_uuids) @@ -433,5 +453,7 @@ class PrimaiteGame: print("agent type not found") game._simulation_initial_state = deepcopy(game.simulation) # noqa + web_server = game.simulation.network.get_node_by_hostname("web_server").software_manager.software["WebServer"] + print(f"And this is the ID of the original WebServer {id(web_server)}") return game diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 29d3a05c..0717f813 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1005,6 +1005,9 @@ class Node(SimComponent): return rm + def reset_component_for_episode(self, episode: int): + self._init_request_manager() + def _install_system_software(self): """Install System Software - software that is usually provided with the OS.""" pass diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 0a9c7fc3..ef9ac0e7 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -43,6 +43,13 @@ class WebBrowser(Application): return rm + def do_this(self): + self._init_request_manager() + print(f"Resetting WebBrowser for episode") + + def reset_component_for_episode(self, episode: int): + pass + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of the WebBrowser. 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 5dda82d5..86a4e4f1 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -17,7 +17,16 @@ from primaite.simulator.system.services.service import Service class WebServer(Service): """Class used to represent a Web Server Service in simulation.""" - last_response_status_code: Optional[HttpStatusCode] = None + _last_response_status_code: Optional[HttpStatusCode] = None + + @property + def last_response_status_code(self) -> HttpStatusCode: + return self._last_response_status_code + + @last_response_status_code.setter + def last_response_status_code(self, val: Any): + print(f"val: {val}, type: {type(val)}") + self._last_response_status_code = val def describe_state(self) -> Dict: """ @@ -29,10 +38,17 @@ class WebServer(Service): :rtype: Dict """ state = super().describe_state() - state["last_response_status_code"] = ( self.last_response_status_code.value if isinstance(self.last_response_status_code, HttpStatusCode) else None ) + + print( + f"" + f"Printing state from Webserver describe func: " + f"val={state['last_response_status_code']}, " + f"type={type(state['last_response_status_code'])}, " + f"Service obj ID={id(self)}" + ) return state def __init__(self, **kwargs): @@ -85,7 +101,14 @@ class WebServer(Service): # return true if response is OK self.last_response_status_code = response.status_code - print(self.last_response_status_code) + + print( + f"" + f"Printing state from Webserver http request func: " + f"val={self.last_response_status_code}, " + f"type={type(self.last_response_status_code)}, " + f"Service obj ID={id(self)}" + ) return response.status_code == HttpStatusCode.OK def _handle_get_request(self, payload: HttpRequestPacket) -> HttpResponsePacket: From 58e9033a4c8729290e56d9d2b601ab291521b65c Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Mon, 27 Nov 2023 23:01:56 +0000 Subject: [PATCH 044/107] #1859 - First pass at an implementation of the full reset method. Will now start testing... --- src/primaite/game/agent/actions.py | 1 - src/primaite/game/agent/rewards.py | 1 - src/primaite/game/game.py | 42 +--- src/primaite/simulator/core.py | 20 +- src/primaite/simulator/domain/account.py | 13 ++ src/primaite/simulator/file_system/file.py | 12 ++ .../simulator/file_system/file_system.py | 30 +++ .../file_system/file_system_item_abc.py | 5 + src/primaite/simulator/file_system/folder.py | 38 ++++ src/primaite/simulator/network/container.py | 14 ++ .../simulator/network/hardware/base.py | 195 ++++++++---------- .../network/hardware/nodes/router.py | 31 +++ src/primaite/simulator/sim_container.py | 10 +- .../system/applications/application.py | 15 +- .../system/applications/database_client.py | 7 + .../system/applications/web_browser.py | 23 +-- .../simulator/system/core/packet_capture.py | 9 +- .../simulator/system/core/session_manager.py | 5 + src/primaite/simulator/system/core/sys_log.py | 7 +- .../simulator/system/processes/process.py | 6 + .../services/database/database_service.py | 17 ++ .../system/services/dns/dns_client.py | 20 +- .../system/services/dns/dns_server.py | 14 +- .../simulator/system/services/service.py | 15 +- .../system/services/web_server/web_server.py | 21 +- src/primaite/simulator/system/software.py | 29 ++- 26 files changed, 360 insertions(+), 240 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index 62e56c6e..c70d4d66 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -15,7 +15,6 @@ from typing import Dict, List, Optional, Tuple, TYPE_CHECKING from gymnasium import spaces from primaite import getLogger -from primaite.simulator.sim_container import Simulation _LOGGER = getLogger(__name__) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 7cca9116..3466114c 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -25,7 +25,6 @@ the structure: service_ref: web_server_database_client ``` """ -import json from abc import abstractmethod from typing import Dict, List, Tuple, Type, TYPE_CHECKING diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 147ed499..38e9d5fc 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -1,5 +1,4 @@ """PrimAITE game - Encapsulates the simulation and agents.""" -from copy import deepcopy from ipaddress import IPv4Address from typing import Dict, List @@ -11,7 +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.simulator.network.hardware.base import Link, NIC, Node, NodeOperatingState +from primaite.simulator.network.hardware.base import NIC, NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.server import Server @@ -19,7 +18,6 @@ from primaite.simulator.network.hardware.nodes.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.application import Application from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService @@ -28,7 +26,6 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot -from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.web_server.web_server import WebServer _LOGGER = getLogger(__name__) @@ -59,10 +56,6 @@ class PrimaiteGame: """Initialise a PrimaiteGame object.""" self.simulation: Simulation = Simulation() """Simulation object with which the agents will interact.""" - print(f"Hello, welcome to PrimaiteGame. This is the ID of the ORIGINAL simulation {id(self.simulation)}") - - self._simulation_initial_state = None - """The Simulation original state (deepcopy of the original Simulation).""" self.agents: List[AbstractAgent] = [] """List of agents.""" @@ -161,34 +154,7 @@ class PrimaiteGame: self.episode_counter += 1 self.step_counter = 0 _LOGGER.debug(f"Resetting primaite game, episode = {self.episode_counter}") - self.simulation = deepcopy(self._simulation_initial_state) - self._reset_components_for_episode() - print("Reset") - - def _reset_components_for_episode(self): - print("Performing full reset for episode") - for node in self.simulation.network.nodes.values(): - print(f"Resetting Node: {node.hostname}") - node.reset_component_for_episode(self.episode_counter) - - # reset Node NIC - - # Reset Node Services - - # Reset Node Applications - print(f"Resetting Software...") - for application in node.software_manager.software.values(): - print(f"Resetting {application.name}") - if isinstance(application, WebBrowser): - application.do_this() - - # Reset Node FileSystem - # Reset Node FileSystemFolder's - # Reset Node FileSystemFile's - - # Reset Router - - # Reset Links + self.simulation.reset_component_for_episode(episode=self.episode_counter) def close(self) -> None: """Close the game, this will close the simulation.""" @@ -452,8 +418,6 @@ class PrimaiteGame: else: print("agent type not found") - game._simulation_initial_state = deepcopy(game.simulation) # noqa - web_server = game.simulation.network.get_node_by_hostname("web_server").software_manager.software["WebServer"] - print(f"And this is the ID of the original WebServer {id(web_server)}") + game.simulation.set_original_state() return game diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 9ead877e..18a470cd 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -153,6 +153,8 @@ class SimComponent(BaseModel): uuid: str """The component UUID.""" + _original_state: Dict = {} + def __init__(self, **kwargs): if not kwargs.get("uuid"): kwargs["uuid"] = str(uuid4()) @@ -160,6 +162,16 @@ 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) + def _init_request_manager(self) -> RequestManager: """ Initialise the request manager for this component. @@ -227,14 +239,6 @@ class SimComponent(BaseModel): """ pass - def reset_component_for_episode(self, episode: int): - """ - Reset this component to its original state for a new episode. - - Override this method with anything that needs to happen within the component for it to be reset. - """ - pass - @property def parent(self) -> "SimComponent": """Reference to the parent object which manages this object. diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index d235c00e..1402a474 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -42,6 +42,19 @@ 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 d9b02e8e..8f0abb3c 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -73,6 +73,18 @@ 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.""" + 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.""" + 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 41f02270..dc6f01a3 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -35,6 +35,36 @@ class FileSystem(SimComponent): if not self.folders: self.create_folder("root") + def set_original_state(self): + """Sets the original state.""" + for folder in self.folders.values(): + folder.set_original_state() + super().set_original_state() + # Capture a list of all 'original' file uuids + self._original_state["original_folder_uuids"] = list(self.folders.keys()) + + def reset_component_for_episode(self, episode: int): + """Reset the original state of the SimComponent.""" + # Move any 'original' folder that have been deleted back to folders + original_folder_uuids = self._original_state.pop("original_folder_uuids") + for uuid in original_folder_uuids: + if uuid in self.deleted_folders: + self.folders[uuid] = self.deleted_folders.pop(uuid) + + # 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: + 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/file_system_item_abc.py b/src/primaite/simulator/file_system/file_system_item_abc.py index fbe5f4b3..86cd1ee7 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -85,6 +85,11 @@ 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"} + 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 f0d55ef8..8e577097 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -51,6 +51,44 @@ class Folder(FileSystemItemABC): self.sys_log.info(f"Created file /{self.name} (id: {self.uuid})") + def set_original_state(self): + """Sets the original state.""" + 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.""" + # Move any 'original' file that have been deleted back to files + original_file_uuids = self._original_state.pop("original_file_uuids") + for uuid in original_file_uuids: + if uuid in self.deleted_files: + self.files[uuid] = self.deleted_files.pop(uuid) + + # 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: + 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/container.py b/src/primaite/simulator/network/container.py index 9fbafc29..cab983c7 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -43,6 +43,20 @@ 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(): + node.reset_component_for_episode(episode) + for link in self.links.values(): + link.reset_component_for_episode(episode) + def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() self._node_request_manager = RequestManager() diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 0717f813..2863dd22 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -121,6 +121,20 @@ 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) + if episode and self.pcap: + self.pcap.current_episode = episode + self.pcap.setup_logger() + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -308,6 +322,14 @@ 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. @@ -454,6 +476,14 @@ 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. @@ -536,15 +566,6 @@ class Link(SimComponent): return True return False - def reset_component_for_episode(self, episode: int): - """ - Link reset function. - - Reset: - - returns the link current_load to 0. - """ - self.current_load = 0 - def __str__(self) -> str: return f"{self.endpoint_a}<-->{self.endpoint_b}" @@ -584,6 +605,10 @@ class ARPCache: ) print(table) + def clear(self): + """Clears the arp cache.""" + self.arp.clear() + def add_arp_cache_entry(self, ip_address: IPv4Address, mac_address: str, nic: NIC, override: bool = False): """ Add an ARP entry to the cache. @@ -756,6 +781,10 @@ class ICMP: self.arp: ARPCache = arp_cache self.request_replies = {} + def clear(self): + """Clears the ICMP request replies tracker.""" + self.request_replies.clear() + def process_icmp(self, frame: Frame, from_nic: NIC, is_reattempt: bool = False): """ Process an ICMP packet, including handling echo requests and replies. @@ -972,6 +1001,55 @@ 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() + + 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.""" + # Reset ARP Cache + self.arp.clear() + + # Reset ICMP + self.icmp.clear() + + # Reset Session Manager + self.session_manager.clear() + + for software in self.software_manager.software.values(): + software.reset_component_for_episode(episode) + + # Reset all Nics + for nic in self.nics.values(): + nic.reset_component_for_episode(episode) + + if episode and self.sys_log: + self.sys_log.current_episode = episode + self.sys_log.setup_logger() + + super().reset_component_for_episode(episode) 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 @@ -1005,9 +1083,6 @@ class Node(SimComponent): return rm - def reset_component_for_episode(self, episode: int): - self._init_request_manager() - def _install_system_software(self): """Install System Software - software that is usually provided with the OS.""" pass @@ -1425,99 +1500,3 @@ class Node(SimComponent): if isinstance(item, Service): return item.uuid in self.services return None - - -class Switch(Node): - """A class representing a Layer 2 network switch.""" - - num_ports: int = 24 - "The number of ports on the switch." - switch_ports: Dict[int, SwitchPort] = {} - "The SwitchPorts on the switch." - mac_address_table: Dict[str, SwitchPort] = {} - "A MAC address table mapping destination MAC addresses to corresponding SwitchPorts." - - def __init__(self, **kwargs): - super().__init__(**kwargs) - if not self.switch_ports: - self.switch_ports = {i: SwitchPort() for i in range(1, self.num_ports + 1)} - for port_num, port in self.switch_ports.items(): - port._connected_node = self - port.parent = self - port.port_num = port_num - - def show(self): - """Prints a table of the SwitchPorts on the Switch.""" - table = PrettyTable(["Port", "MAC Address", "Speed", "Status"]) - - for port_num, port in self.switch_ports.items(): - table.add_row([port_num, port.mac_address, port.speed, "Enabled" if port.enabled else "Disabled"]) - print(table) - - def describe_state(self) -> Dict: - """ - Produce a dictionary describing the current state of this object. - - Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - - :return: Current state of this object and child objects. - :rtype: Dict - """ - return { - "uuid": self.uuid, - "num_ports": self.num_ports, # redundant? - "ports": {port_num: port.describe_state() for port_num, port in self.switch_ports.items()}, - "mac_address_table": {mac: port for mac, port in self.mac_address_table.items()}, - } - - def _add_mac_table_entry(self, mac_address: str, switch_port: SwitchPort): - mac_table_port = self.mac_address_table.get(mac_address) - if not mac_table_port: - self.mac_address_table[mac_address] = switch_port - self.sys_log.info(f"Added MAC table entry: Port {switch_port.port_num} -> {mac_address}") - else: - if mac_table_port != switch_port: - self.mac_address_table.pop(mac_address) - self.sys_log.info(f"Removed MAC table entry: Port {mac_table_port.port_num} -> {mac_address}") - self._add_mac_table_entry(mac_address, switch_port) - - def forward_frame(self, frame: Frame, incoming_port: SwitchPort): - """ - Forward a frame to the appropriate port based on the destination MAC address. - - :param frame: The Frame to be forwarded. - :param incoming_port: The port number from which the frame was received. - """ - src_mac = frame.ethernet.src_mac_addr - dst_mac = frame.ethernet.dst_mac_addr - self._add_mac_table_entry(src_mac, incoming_port) - - outgoing_port = self.mac_address_table.get(dst_mac) - if outgoing_port or dst_mac != "ff:ff:ff:ff:ff:ff": - outgoing_port.send_frame(frame) - else: - # If the destination MAC is not in the table, flood to all ports except incoming - for port in self.switch_ports.values(): - if port != incoming_port: - port.send_frame(frame) - - def disconnect_link_from_port(self, link: Link, port_number: int): - """ - Disconnect a given link from the specified port number on the switch. - - :param link: The Link object to be disconnected. - :param port_number: The port number on the switch from where the link should be disconnected. - :raise NetworkError: When an invalid port number is provided or the link does not match the connection. - """ - port = self.switch_ports.get(port_number) - if port is None: - msg = f"Invalid port number {port_number} on the switch" - _LOGGER.error(msg) - raise NetworkError(msg) - - if port._connected_link != link: - msg = f"The link does not match the connection at port number {port_number}" - _LOGGER.error(msg) - raise NetworkError(msg) - - port.disconnect_link() diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index c2a38aba..8e03cfa3 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -52,6 +52,11 @@ 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. @@ -93,6 +98,18 @@ 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) + + 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) def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() @@ -638,6 +655,20 @@ 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() + vals_to_include = {"num_ports", "route_table"} + 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.""" + self.arp.clear() + self.acl.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("acl", RequestType(func=self.acl._request_manager)) diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 8e820ec8..c529ed04 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -9,7 +9,7 @@ class Simulation(SimComponent): """Top-level simulation object which holds a reference to all other parts of the simulation.""" network: Network - domain: DomainController + # domain: DomainController def __init__(self, **kwargs): """Initialise the Simulation.""" @@ -21,6 +21,14 @@ 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) + def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() # pass through network requests to the network objects diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 9a58c98a..c69f745d 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -38,6 +38,12 @@ class Application(IOSoftware): self.health_state_visible = SoftwareHealthState.UNUSED self.health_state_actual = SoftwareHealthState.UNUSED + 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: """ @@ -82,15 +88,6 @@ class Application(IOSoftware): self.sys_log.info(f"Installing Application {self.name}") self.operating_state = ApplicationOperatingState.INSTALLING - def reset_component_for_episode(self, episode: int): - """ - Resets the Application component for a new episode. - - This method ensures the Application is ready for a new episode, including resetting any - stateful properties or statistics, and clearing any message queues. - """ - pass - def receive(self, payload: Any, session_id: str, **kwargs) -> bool: """ Receives a payload from the SessionManager. diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 37236e69..12dfc0ac 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -31,6 +31,13 @@ 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.""" + super().set_original_state() + vals_to_include = {"server_ip_address", "server_password", "connected"} + self._original_state.update(self.model_dump(include=vals_to_include)) def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index ef9ac0e7..32dd9cd2 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -33,8 +33,15 @@ 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.""" + 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 _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request( @@ -43,13 +50,6 @@ class WebBrowser(Application): return rm - def do_this(self): - self._init_request_manager() - print(f"Resetting WebBrowser for episode") - - def reset_component_for_episode(self, episode: int): - pass - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of the WebBrowser. @@ -60,14 +60,7 @@ class WebBrowser(Application): state["last_response_status_code"] = self.latest_response.status_code if self.latest_response else None def reset_component_for_episode(self, episode: int): - """ - Resets the Application component for a new episode. - - This method ensures the Application is ready for a new episode, including resetting any - stateful properties or statistics, and clearing any message queues. - """ - self.domain_name_ip_address = None - self.latest_response = None + """Reset the original state of the SimComponent.""" def get_webpage(self) -> bool: """ diff --git a/src/primaite/simulator/system/core/packet_capture.py b/src/primaite/simulator/system/core/packet_capture.py index c2faeb10..1539e024 100644 --- a/src/primaite/simulator/system/core/packet_capture.py +++ b/src/primaite/simulator/system/core/packet_capture.py @@ -34,9 +34,12 @@ class PacketCapture: "The IP address associated with the PCAP logs." self.switch_port_number = switch_port_number "The SwitchPort number." - self._setup_logger() - def _setup_logger(self): + self.current_episode: int = 1 + + self.setup_logger() + + def setup_logger(self): """Set up the logger configuration.""" log_path = self._get_log_path() @@ -75,7 +78,7 @@ class PacketCapture: def _get_log_path(self) -> Path: """Get the path for the log file.""" - root = SIM_OUTPUT.path / self.hostname + root = SIM_OUTPUT.path / f"episode_{self.current_episode}" / self.hostname root.mkdir(exist_ok=True, parents=True) return root / f"{self._logger_name}.log" diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 360b5e73..8658f155 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -93,6 +93,11 @@ class SessionManager: """ pass + def clear(self): + """Clears the sessions.""" + self.sessions_by_key.clear() + self.sessions_by_uuid.clear() + @staticmethod def _get_session_key( frame: Frame, inbound_frame: bool = True diff --git a/src/primaite/simulator/system/core/sys_log.py b/src/primaite/simulator/system/core/sys_log.py index 7ac6df85..41ce8fee 100644 --- a/src/primaite/simulator/system/core/sys_log.py +++ b/src/primaite/simulator/system/core/sys_log.py @@ -31,9 +31,10 @@ class SysLog: :param hostname: The hostname associated with the system logs being recorded. """ self.hostname = hostname - self._setup_logger() + self.current_episode: int = 1 + self.setup_logger() - def _setup_logger(self): + def setup_logger(self): """ Configures the logger for this SysLog instance. @@ -80,7 +81,7 @@ class SysLog: :return: Path object representing the location of the log file. """ - root = SIM_OUTPUT.path / self.hostname + root = SIM_OUTPUT.path / f"episode_{self.current_episode}" / self.hostname root.mkdir(exist_ok=True, parents=True) return root / f"{self.hostname}_sys.log" diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index c4e94845..ad9af335 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -24,6 +24,12 @@ 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 d7277e1e..616cbedd 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -38,6 +38,23 @@ class DatabaseService(Service): self._db_file: File self._create_db_file() + def set_original_state(self): + """Sets the original state.""" + super().set_original_state() + vals_to_include = { + "password", + "connections", + "backup_server", + "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.""" + self.connections.clear() + 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 266ac4f6..c6c3e09a 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -29,6 +29,17 @@ class DNSClient(Service): super().__init__(**kwargs) self.start() + def set_original_state(self): + """Sets the original state.""" + 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() + super().reset_component_for_episode(episode) + def describe_state(self) -> Dict: """ Describes the current state of the software. @@ -42,15 +53,6 @@ class DNSClient(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 add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address): """ Adds a domain name to the DNS Client cache. diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 90a350c8..bbeaa62c 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -28,6 +28,11 @@ 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() + super().reset_component_for_episode(episode) + def describe_state(self) -> Dict: """ Describes the current state of the software. @@ -62,15 +67,6 @@ class DNSServer(Service): """ self.dns_table[domain_name] = domain_ip_address - 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: Any, diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index e2b04c15..d519da8e 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -46,6 +46,12 @@ class Service(IOSoftware): self.health_state_visible = SoftwareHealthState.UNUSED self.health_state_actual = SoftwareHealthState.UNUSED + 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())) @@ -73,15 +79,6 @@ class Service(IOSoftware): state["health_state_visible"] = self.health_state_visible 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 stop(self) -> None: """Stop the service.""" if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]: 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 86a4e4f1..754aa22f 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -19,8 +19,14 @@ 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.""" + self._last_response_status_code = None + super().reset_component_for_episode(episode) + @property def last_response_status_code(self) -> HttpStatusCode: + """The latest http response code.""" return self._last_response_status_code @last_response_status_code.setter @@ -41,14 +47,6 @@ class WebServer(Service): state["last_response_status_code"] = ( self.last_response_status_code.value if isinstance(self.last_response_status_code, HttpStatusCode) else None ) - - print( - f"" - f"Printing state from Webserver describe func: " - f"val={state['last_response_status_code']}, " - f"type={type(state['last_response_status_code'])}, " - f"Service obj ID={id(self)}" - ) return state def __init__(self, **kwargs): @@ -102,13 +100,6 @@ class WebServer(Service): # return true if response is OK self.last_response_status_code = response.status_code - print( - f"" - f"Printing state from Webserver http request func: " - f"val={self.last_response_status_code}, " - f"type={type(self.last_response_status_code)}, " - f"Service obj ID={id(self)}" - ) return response.status_code == HttpStatusCode.OK def _handle_get_request(self, payload: HttpRequestPacket) -> HttpResponsePacket: diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index f2627557..413da959 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -89,6 +89,19 @@ class Software(SimComponent): folder: Optional[Folder] = None "The folder on the file system the Software uses." + 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( @@ -131,16 +144,6 @@ class Software(SimComponent): ) return state - def reset_component_for_episode(self, episode: int): - """ - Resets the software component for a new episode. - - This method should ensure the software is ready for a new episode, including resetting any - stateful properties or statistics, and clearing any message queues. The specifics of what constitutes a - "reset" should be implemented in subclasses. - """ - pass - def set_health_state(self, health_state: SoftwareHealthState) -> None: """ Assign a new health state to this software. @@ -203,6 +206,12 @@ class IOSoftware(Software): port: Port "The port to which the software is connected." + 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: """ From 39dfbb741f53fbd43c25e43cd7c5dcbad153c29e Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Tue, 28 Nov 2023 00:21:41 +0000 Subject: [PATCH 045/107] #1859 - Made some fixes to resets. Still an issue with the Router reset. --- src/primaite/simulator/network/hardware/base.py | 1 + .../simulator/network/hardware/nodes/router.py | 4 ++++ .../simulator/system/services/dns/dns_server.py | 11 +++++++++++ .../system/services/web_server/web_server.py | 3 +-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 2863dd22..09e2b12f 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -134,6 +134,7 @@ class NIC(SimComponent): if episode and self.pcap: self.pcap.current_episode = episode self.pcap.setup_logger() + self.enable() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 8e03cfa3..1bf2ea2f 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -667,6 +667,10 @@ class Router(Node): """Reset the original state of the SimComponent.""" self.arp.clear() self.acl.reset_component_for_episode(episode) + for i, nic in self.ethernet_ports.items(): + nic.reset_component_for_episode(episode) + self.enable_port(i) + super().reset_component_for_episode(episode) def _init_request_manager(self) -> RequestManager: diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index bbeaa62c..3b1f3bf6 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -28,10 +28,21 @@ class DNSServer(Service): super().__init__(**kwargs) self.start() + def set_original_state(self): + """Sets the original state.""" + 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.""" + print("dns reset") + print("DNSServer original state", self._original_state) 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) + self.show() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index 754aa22f..56f47195 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -31,7 +31,6 @@ class WebServer(Service): @last_response_status_code.setter def last_response_status_code(self, val: Any): - print(f"val: {val}, type: {type(val)}") self._last_response_status_code = val def describe_state(self) -> Dict: @@ -47,6 +46,7 @@ class WebServer(Service): state["last_response_status_code"] = ( self.last_response_status_code.value if isinstance(self.last_response_status_code, HttpStatusCode) else None ) + print(state) return state def __init__(self, **kwargs): @@ -99,7 +99,6 @@ class WebServer(Service): # return true if response is OK self.last_response_status_code = response.status_code - return response.status_code == HttpStatusCode.OK def _handle_get_request(self, payload: HttpRequestPacket) -> HttpResponsePacket: From 37663c941d4ba4345216548e6f47fc0d58abf987 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Tue, 28 Nov 2023 00:51:48 +0000 Subject: [PATCH 046/107] #1859 - Added route table reset, still not working --- .../network/hardware/nodes/router.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 1bf2ea2f..667cf2bf 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -354,6 +354,11 @@ class RouteEntry(SimComponent): kwargs[key] = IPv4Address(kwargs[key]) super().__init__(**kwargs) + 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. @@ -385,6 +390,18 @@ class RouteTable(SimComponent): routes: List[RouteEntry] = [] sys_log: SysLog + def set_original_state(self): + """Sets the original state.""" + """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() + 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. @@ -660,13 +677,15 @@ class Router(Node): def set_original_state(self): """Sets the original state.""" self.acl.set_original_state() - vals_to_include = {"num_ports", "route_table"} + self.route_table.set_original_state() + vals_to_include = {"num_ports"} 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.""" self.arp.clear() self.acl.reset_component_for_episode(episode) + self.route_table.reset_component_for_episode(episode) for i, nic in self.ethernet_ports.items(): nic.reset_component_for_episode(episode) self.enable_port(i) @@ -765,6 +784,7 @@ class Router(Node): dst_ip_address=dst_ip_address, dst_port=dst_port, ) + if not permitted: at_port = self._get_port_of_nic(from_nic) self.sys_log.info(f"Frame blocked at port {at_port} by rule {rule}") From 517f99b04b9e8c2792ad70768a5e3bfa65f9e88a Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Tue, 28 Nov 2023 09:45:45 +0000 Subject: [PATCH 047/107] #1859 - Added the call to file system reset --- src/primaite/simulator/network/hardware/base.py | 7 +++++++ src/primaite/simulator/network/hardware/nodes/router.py | 1 + 2 files changed, 8 insertions(+) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 09e2b12f..cb159b8b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1009,6 +1009,8 @@ class Node(SimComponent): 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() @@ -1039,13 +1041,18 @@ class Node(SimComponent): # Reset Session Manager self.session_manager.clear() + # Reset software for software in self.software_manager.software.values(): software.reset_component_for_episode(episode) + # Reset File System + self.file_system.reset_component_for_episode(episode) + # Reset all Nics for nic in self.nics.values(): nic.reset_component_for_episode(episode) + # if episode and self.sys_log: self.sys_log.current_episode = episode self.sys_log.setup_logger() diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 667cf2bf..34b92a07 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -818,6 +818,7 @@ 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): """ From b0399195bbddfce87d6b9c032462c2944a920232 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 27 Nov 2023 22:20:44 +0000 Subject: [PATCH 048/107] Fix software manager usage in uc2 network func --- src/primaite/simulator/network/networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 446e5649..b7bd2e95 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -157,7 +157,7 @@ def arcd_uc2_network() -> Network: operating_state=NodeOperatingState.ON, ) client_2.power_on() - web_browser = client_2.software_manager["WebBrowser"] + web_browser = client_2.software_manager.software["WebBrowser"] web_browser.target_url = "http://arcd.com/users/" network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) From 3df3e113d1320b1eed9d7f76a3591a80d7e68c02 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 27 Nov 2023 22:24:30 +0000 Subject: [PATCH 049/107] Change data manipulation test to use the right func --- .../e2e_integration_tests/test_uc2_data_manipulation_scenario.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index fe7bab5f..81bbfc96 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -23,7 +23,6 @@ def test_data_manipulation(uc2_network): # Now we run the DataManipulationBot db_manipulation_bot.run() - db_manipulation_bot.attack() # Now check that the DB client on the web_server cannot query the users table on the database assert not db_client.query("SELECT") From 2de1d02c48805efaed3dfd26d21d56fcc12a7263 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 27 Nov 2023 22:55:00 +0000 Subject: [PATCH 050/107] Fix app install logic --- src/primaite/simulator/system/applications/application.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 4fe7a5e1..898e5917 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -108,9 +108,6 @@ class Application(IOSoftware): def install(self) -> None: """Install Application.""" - if self._can_perform_action(): - return - super().install() if self.operating_state == ApplicationOperatingState.CLOSED: self.sys_log.info(f"Installing Application {self.name}") From 9a4855e7bd448aeebcaccbf6980cea4c7d60c933 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Tue, 28 Nov 2023 11:58:09 +0000 Subject: [PATCH 051/107] #1859 - Added the call to file system reset --- src/primaite/simulator/file_system/file.py | 2 ++ .../simulator/file_system/file_system.py | 9 +++++++-- src/primaite/simulator/file_system/folder.py | 2 ++ .../simulator/network/hardware/base.py | 11 +++++++++- .../network/hardware/nodes/router.py | 3 ++- .../system/applications/database_client.py | 6 ++++++ .../system/applications/web_browser.py | 6 ++++++ .../services/database/database_service.py | 2 ++ .../system/services/dns/dns_client.py | 11 ++-------- .../system/services/dns/dns_server.py | 5 ++--- .../system/services/ftp/ftp_client.py | 12 +++++++++++ .../system/services/ftp/ftp_server.py | 13 ++++++++++++ .../red_services/data_manipulation_bot.py | 20 +++++++++++++++++++ .../system/services/web_server/web_server.py | 20 +++++++++---------- 14 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index 8f0abb3c..f0984f89 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -77,12 +77,14 @@ class File(FileSystemItemABC): def set_original_state(self): """Sets the original state.""" + print(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.""" + print(f"Resetting File ({self.path}) state on node {self.sys_log.hostname}") super().reset_component_for_episode(episode) @property diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index dc6f01a3..a6876786 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -37,15 +37,20 @@ class FileSystem(SimComponent): def set_original_state(self): """Sets the original state.""" + print(f"Setting FileSystem original state on node {self.sys_log.hostname}") for folder in self.folders.values(): folder.set_original_state() - super().set_original_state() # Capture a list of all 'original' file uuids - self._original_state["original_folder_uuids"] = list(self.folders.keys()) + 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.""" + print(f"Resetting FileSystem state on node {self.sys_log.hostname}") # Move any 'original' folder that have been deleted back to folders + print(self._original_state) original_folder_uuids = self._original_state.pop("original_folder_uuids") for uuid in original_folder_uuids: if uuid in self.deleted_folders: diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 8e577097..24dbdd79 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -53,6 +53,7 @@ class Folder(FileSystemItemABC): def set_original_state(self): """Sets the original state.""" + print(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() @@ -69,6 +70,7 @@ class Folder(FileSystemItemABC): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" + print(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.pop("original_file_uuids") for uuid in original_file_uuids: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index c6ee373e..b72fde54 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -993,6 +993,7 @@ class Node(SimComponent): def set_original_state(self): """Sets the original state.""" + print(f"Setting node original state for {self.hostname}") for software in self.software_manager.software.values(): software.set_original_state() @@ -1019,6 +1020,7 @@ class Node(SimComponent): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" + print(f"Resetting node state for {self.hostname}") # Reset ARP Cache self.arp.clear() @@ -1031,6 +1033,10 @@ class Node(SimComponent): # Reset software for software in self.software_manager.software.values(): software.reset_component_for_episode(episode) + if isinstance(software, Service): + software.start() + elif isinstance(software, Application): + software.run() # Reset File System self.file_system.reset_component_for_episode(episode) @@ -1039,13 +1045,16 @@ class Node(SimComponent): for nic in self.nics.values(): nic.reset_component_for_episode(episode) - # if episode and self.sys_log: self.sys_log.current_episode = episode self.sys_log.setup_logger() super().reset_component_for_episode(episode) + self.power_on() + for nic in self.nics.values(): + nic.enable() + 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. diff --git a/src/primaite/simulator/network/hardware/nodes/router.py b/src/primaite/simulator/network/hardware/nodes/router.py index 34b92a07..0017215a 100644 --- a/src/primaite/simulator/network/hardware/nodes/router.py +++ b/src/primaite/simulator/network/hardware/nodes/router.py @@ -678,8 +678,9 @@ class Router(Node): """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 = self.model_dump(include=vals_to_include) + 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/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 37f85b28..92f7e76d 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -35,10 +35,16 @@ class DatabaseClient(Application): def set_original_state(self): """Sets the original state.""" + print(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"} 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.""" + print(f"Resetting DataBaseClient state on node {self.software_manager.node.hostname}") + super().reset_component_for_episode(episode) + def describe_state(self) -> Dict: """ Describes the current state of the ACLRule. diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index bf304d7b..88560240 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -43,10 +43,16 @@ class WebBrowser(Application): def set_original_state(self): """Sets the original state.""" + print(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.""" + print(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( diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 45e469fb..925d1df0 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -40,6 +40,7 @@ class DatabaseService(Service): def set_original_state(self): """Sets the original state.""" + print(f"Setting DatabaseService original state on node {self.software_manager.node.hostname}") super().set_original_state() vals_to_include = { "password", @@ -52,6 +53,7 @@ class DatabaseService(Service): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" + print("Resetting DatabaseService original state on node {self.software_manager.node.hostname}") self.connections.clear() super().reset_component_for_episode(episode) diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 3d425bfa..147387ae 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -31,12 +31,14 @@ class DNSClient(Service): def set_original_state(self): """Sets the original state.""" + print(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.""" + print(f"Resetting DNSClient state on node {self.software_manager.node.hostname}") self.dns_cache.clear() super().reset_component_for_episode(episode) @@ -53,15 +55,6 @@ class DNSClient(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 add_domain_to_cache(self, domain_name: str, ip_address: IPv4Address) -> bool: """ Adds a domain name to the DNS Client cache. diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 30278ab1..7842a07e 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -30,19 +30,18 @@ class DNSServer(Service): def set_original_state(self): """Sets the original state.""" + print(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.""" - print("dns reset") - print("DNSServer original state", self._original_state) + print(f"Resetting DNSServer state on node {self.software_manager.node.hostname}") 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) - self.show() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 649b9b50..011b597f 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -28,6 +28,18 @@ class FTPClient(FTPServiceABC): super().__init__(**kwargs) self.start() + def set_original_state(self): + """Sets the original state.""" + print(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.""" + print(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 cd128339..811a8939 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -29,6 +29,19 @@ class FTPServer(FTPServiceABC): super().__init__(**kwargs) self.start() + def set_original_state(self): + """Sets the original state.""" + print(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.""" + print(f"Resetting FTPServer state on node {self.software_manager.node.hostname}") + self.connections.clear() + 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/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index b0b34396..75cdee85 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -47,6 +47,26 @@ class DataManipulationBot(DatabaseClient): super().__init__(**kwargs) self.name = "DataManipulationBot" + def set_original_state(self): + """Sets the original state.""" + print(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.""" + print(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/services/web_server/web_server.py b/src/primaite/simulator/system/services/web_server/web_server.py index becbf9f9..f34bba37 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -17,22 +17,20 @@ from primaite.simulator.system.services.service import Service class WebServer(Service): """Class used to represent a Web Server Service in simulation.""" - _last_response_status_code: Optional[HttpStatusCode] = None + last_response_status_code: Optional[HttpStatusCode] = None + + def set_original_state(self): + """Sets the original state.""" + print(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.""" - self._last_response_status_code = None + print(f"Resetting WebServer state on node {self.software_manager.node.hostname}") super().reset_component_for_episode(episode) - @property - def last_response_status_code(self) -> HttpStatusCode: - """The latest http response code.""" - return self._last_response_status_code - - @last_response_status_code.setter - def last_response_status_code(self, val: Any): - self._last_response_status_code = val - def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. From 94f8a45a4dcaa162cb73ba9d04bfd6718e57b8ec Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Tue, 28 Nov 2023 15:29:13 +0000 Subject: [PATCH 052/107] #1859 - Re-ordered the node reset function --- .../simulator/network/hardware/base.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index b72fde54..825df37d 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1021,6 +1021,8 @@ class Node(SimComponent): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" print(f"Resetting node state for {self.hostname}") + super().reset_component_for_episode(episode) + # Reset ARP Cache self.arp.clear() @@ -1030,17 +1032,11 @@ class Node(SimComponent): # Reset Session Manager self.session_manager.clear() - # Reset software - for software in self.software_manager.software.values(): - software.reset_component_for_episode(episode) - if isinstance(software, Service): - software.start() - elif isinstance(software, Application): - software.run() - # Reset File System self.file_system.reset_component_for_episode(episode) + self.power_on() + # Reset all Nics for nic in self.nics.values(): nic.reset_component_for_episode(episode) @@ -1049,9 +1045,14 @@ class Node(SimComponent): self.sys_log.current_episode = episode self.sys_log.setup_logger() - super().reset_component_for_episode(episode) + # Reset software + for software in self.software_manager.software.values(): + software.reset_component_for_episode(episode) + if isinstance(software, Service): + software.start() + elif isinstance(software, Application): + software.run() - self.power_on() for nic in self.nics.values(): nic.enable() From 19d534395be056a5896c6e45c82fb05cac256fea Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Wed, 29 Nov 2023 01:28:40 +0000 Subject: [PATCH 053/107] #2084: beginning the introduction of code coverage + adding tests to try to meet the 80% code coverage target --- .azure/azure-ci-build-pipeline.yaml | 4 +- .../simulator/file_system/file_system.py | 7 +- src/primaite/simulator/file_system/folder.py | 7 +- .../system/services/ftp/ftp_client.py | 7 +- .../system/services/ftp/ftp_server.py | 10 +- tests/conftest.py | 34 ++++- .../environments/test_sb3_environment.py | 2 + .../test_primaite_session.py | 1 + .../network/test_link_connection.py | 6 + .../system/test_dns_client_server.py | 50 +++++-- .../system/test_ftp_client_server.py | 70 +++++---- .../system/test_web_client_server.py | 135 ++++++++++-------- .../test_web_client_server_and_database.py | 106 ++++++++++++++ .../_file_system/test_file_system.py | 32 +++++ .../_system/_applications/test_web_browser.py | 51 +++++-- .../{test_dns.py => test_dns_client.py} | 61 +------- .../_system/_services/test_dns_server.py | 64 +++++++++ .../_system/_services/test_ftp_client.py | 50 +++++++ .../{test_ftp.py => test_ftp_server.py} | 58 ++++---- 19 files changed, 533 insertions(+), 222 deletions(-) create mode 100644 tests/integration_tests/system/test_web_client_server_and_database.py rename tests/unit_tests/_primaite/_simulator/_system/_services/{test_dns.py => test_dns_client.py} (65%) create mode 100644 tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py create mode 100644 tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py rename tests/unit_tests/_primaite/_simulator/_system/_services/{test_ftp.py => test_ftp_server.py} (63%) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 9070270a..49d76937 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -86,5 +86,5 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -n auto - displayName: 'Run tests' + pytest -n auto --cov=src --cov-report=html:coverage_report --cov-fail-under=80 + displayName: 'Run tests and code coverage' diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index dc6f01a3..d61b62d4 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -49,7 +49,10 @@ class FileSystem(SimComponent): original_folder_uuids = self._original_state.pop("original_folder_uuids") for uuid in original_folder_uuids: if uuid in self.deleted_folders: - self.folders[uuid] = self.deleted_folders.pop(uuid) + folder = self.deleted_folders[uuid] + self.deleted_folders.pop(uuid) + self.folders[uuid] = folder + self._folders_by_name[folder.name] = folder # Clear any other deleted folders that aren't original (have been created by agent) self.deleted_folders.clear() @@ -58,7 +61,9 @@ class FileSystem(SimComponent): 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) + self._folders_by_name.pop(folder.name) # Now reset all remaining folders for folder in self.folders.values(): diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 8e577097..a4907299 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -73,7 +73,10 @@ class Folder(FileSystemItemABC): original_file_uuids = self._original_state.pop("original_file_uuids") for uuid in original_file_uuids: if uuid in self.deleted_files: - self.files[uuid] = self.deleted_files.pop(uuid) + file = self.deleted_files[uuid] + self.deleted_files.pop(uuid) + self.files[uuid] = file + self._files_by_name[file.name] = file # Clear any other deleted files that aren't original (have been created by agent) self.deleted_files.clear() @@ -82,7 +85,9 @@ class Folder(FileSystemItemABC): 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) + self._files_by_name.pop(file.name) # Now reset all remaining files for file in self.files.values(): diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 649b9b50..b73eec7e 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -7,7 +7,6 @@ from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -from primaite.simulator.system.services.service import ServiceOperatingState class FTPClient(FTPServiceABC): @@ -38,8 +37,7 @@ class FTPClient(FTPServiceABC): :type: session_id: Optional[str] """ # if client service is down, return error - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error("FTP Client is not running") + if not self._can_perform_action(): payload.status_code = FTPStatusCode.ERROR return payload @@ -66,8 +64,7 @@ class FTPClient(FTPServiceABC): :type: is_reattempt: Optional[bool] """ # make sure the service is running before attempting - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") + if not self._can_perform_action(): return False # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index cd128339..c40aaa5a 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -5,7 +5,6 @@ from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPS from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC -from primaite.simulator.system.services.service import ServiceOperatingState class FTPServer(FTPServiceABC): @@ -42,8 +41,7 @@ class FTPServer(FTPServiceABC): payload.status_code = FTPStatusCode.ERROR # if server service is down, return error - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error("FTP Server not running") + if not self._can_perform_action(): return payload self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}") @@ -79,6 +77,9 @@ class FTPServer(FTPServiceABC): self.sys_log.error(f"{payload} is not an FTP packet") return False + if not super().receive(payload=payload, session_id=session_id, **kwargs): + return False + """ Ignore ftp payload if status code is defined. @@ -86,9 +87,6 @@ class FTPServer(FTPServiceABC): prevents an FTP request loop - FTP client and servers can exist on the same node. """ - if not super().receive(payload=payload, session_id=session_id, **kwargs): - return False - if payload.status_code is not None: return False diff --git a/tests/conftest.py b/tests/conftest.py index c0d05455..8a1f885c 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, Union +from typing import Any, Dict, Tuple, Union import pytest import yaml @@ -12,6 +12,9 @@ from primaite.session.session import PrimaiteSession # from primaite.environment.primaite_env import Primaite # from primaite.primaite_session import PrimaiteSession from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -29,7 +32,7 @@ from primaite import PRIMAITE_PATHS # PrimAITE v3 stuff from primaite.simulator.file_system.file_system import FileSystem -from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.base import Link, Node class TestService(Service): @@ -122,3 +125,30 @@ def temp_primaite_session(request, monkeypatch) -> TempPrimaiteSession: monkeypatch.setattr(PRIMAITE_PATHS, "user_sessions_path", temp_user_sessions_path()) config_path = request.param[0] return TempPrimaiteSession.from_config(config_path=config_path) + + +@pytest.fixture(scope="function") +def client_server() -> Tuple[Computer, Server]: + # Create Computer + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.0.1", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + + # Create Server + server = Server( + hostname="server", ip_address="192.168.0.2", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON + ) + + # Connect Computer and Server + computer_nic = computer.nics[next(iter(computer.nics))] + server_nic = server.nics[next(iter(server.nics))] + link = Link(endpoint_a=computer_nic, endpoint_b=server_nic) + + # Should be linked + assert link.is_up + + return computer, server diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index 3907ff50..c1c028a2 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -2,6 +2,7 @@ import tempfile from pathlib import Path +import pytest import yaml from stable_baselines3 import PPO @@ -10,6 +11,7 @@ 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: diff --git a/tests/e2e_integration_tests/test_primaite_session.py b/tests/e2e_integration_tests/test_primaite_session.py index 086e9af8..d0dce118 100644 --- a/tests/e2e_integration_tests/test_primaite_session.py +++ b/tests/e2e_integration_tests/test_primaite_session.py @@ -11,6 +11,7 @@ MISCONFIGURED_PATH = TEST_ASSETS_ROOT / "configs/bad_primaite_session.yaml" MULTI_AGENT_PATH = TEST_ASSETS_ROOT / "configs/multi_agent_session.yaml" +@pytest.mark.skip(reason="no way of currently testing this") class TestPrimaiteSession: @pytest.mark.parametrize("temp_primaite_session", [[CFG_PATH]], indirect=True) def test_creating_session(self, temp_primaite_session): diff --git a/tests/integration_tests/network/test_link_connection.py b/tests/integration_tests/network/test_link_connection.py index 0ddf54df..c6aeac24 100644 --- a/tests/integration_tests/network/test_link_connection.py +++ b/tests/integration_tests/network/test_link_connection.py @@ -16,3 +16,9 @@ def test_link_up(): assert nic_a.enabled assert nic_b.enabled assert link.is_up + + +def test_ping_between_computer_and_server(client_server): + computer, server = client_server + + assert computer.ping(target_ip_address=server.nics[next(iter(server.nics))].ip_address) diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 81a223ef..70657112 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -1,3 +1,8 @@ +from ipaddress import IPv4Address +from typing import Tuple + +import pytest + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server @@ -6,12 +11,31 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState -def test_dns_client_server(uc2_network): - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - domain_controller: Server = uc2_network.get_node_by_hostname("domain_controller") +@pytest.fixture(scope="function") +def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSServer, Server]: + computer, server = client_server - dns_client: DNSClient = client_1.software_manager.software["DNSClient"] - dns_server: DNSServer = domain_controller.software_manager.software["DNSServer"] + # Install DNS Client on computer + computer.software_manager.install(DNSClient) + dns_client: DNSClient = computer.software_manager.software["DNSClient"] + dns_client.start() + # set server as DNS Server + dns_client.dns_server = IPv4Address(server.nics.get(next(iter(server.nics))).ip_address) + + # Install DNS Server on server + server.software_manager.install(DNSServer) + dns_server: DNSServer = server.software_manager.software["DNSServer"] + dns_server.start() + # register arcd.com as a domain + dns_server.dns_register( + domain_name="arcd.com", domain_ip_address=IPv4Address(server.nics.get(next(iter(server.nics))).ip_address) + ) + + return dns_client, computer, dns_server, server + + +def test_dns_client_server(dns_client_and_dns_server): + dns_client, computer, dns_server, server = dns_client_and_dns_server assert dns_client.operating_state == ServiceOperatingState.RUNNING assert dns_server.operating_state == ServiceOperatingState.RUNNING @@ -29,12 +53,8 @@ def test_dns_client_server(uc2_network): assert len(dns_client.dns_cache) == 1 -def test_dns_client_requests_offline_dns_server(uc2_network): - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - domain_controller: Server = uc2_network.get_node_by_hostname("domain_controller") - - dns_client: DNSClient = client_1.software_manager.software["DNSClient"] - dns_server: DNSServer = domain_controller.software_manager.software["DNSServer"] +def test_dns_client_requests_offline_dns_server(dns_client_and_dns_server): + dns_client, computer, dns_server, server = dns_client_and_dns_server assert dns_client.operating_state == ServiceOperatingState.RUNNING assert dns_server.operating_state == ServiceOperatingState.RUNNING @@ -48,12 +68,12 @@ def test_dns_client_requests_offline_dns_server(uc2_network): assert len(dns_client.dns_cache) == 1 dns_client.dns_cache = {} - domain_controller.power_off() + server.power_off() - for i in range(domain_controller.shut_down_duration + 1): - uc2_network.apply_timestep(timestep=i) + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) - assert domain_controller.operating_state == NodeOperatingState.OFF + assert server.operating_state == NodeOperatingState.OFF assert dns_server.operating_state == ServiceOperatingState.STOPPED # this time it should not cache because dns server is not online diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index b2cdbc06..32ea7f2b 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -1,4 +1,7 @@ from ipaddress import IPv4Address +from typing import Tuple + +import pytest from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server @@ -7,18 +10,31 @@ from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.service import ServiceOperatingState -def test_ftp_client_store_file_in_server(uc2_network): +@pytest.fixture(scope="function") +def ftp_client_and_ftp_server(client_server) -> Tuple[FTPClient, Computer, FTPServer, Server]: + computer, server = client_server + + # Install FTP Client service on computer + computer.software_manager.install(FTPClient) + ftp_client: FTPClient = computer.software_manager.software["FTPClient"] + ftp_client.start() + + # Install FTP Server service on server + server.software_manager.install(FTPServer) + ftp_server: FTPServer = server.software_manager.software["FTPServer"] + ftp_server.start() + + return ftp_client, computer, ftp_server, server + + +def test_ftp_client_store_file_in_server(ftp_client_and_ftp_server): """ Test checks to see if the client is able to store files in the backup server. """ - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - backup_server: Server = uc2_network.get_node_by_hostname("backup_server") - - ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] - ftp_server_service: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server_service.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING # create file on ftp client ftp_client.file_system.create_file(file_name="test_file.txt") @@ -28,61 +44,53 @@ def test_ftp_client_store_file_in_server(uc2_network): src_file_name="test_file.txt", dest_folder_name="client_1_backup", dest_file_name="test_file.txt", - dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address, ) - assert ftp_server_service.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") + assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") -def test_ftp_client_retrieve_file_from_server(uc2_network): +def test_ftp_client_retrieve_file_from_server(ftp_client_and_ftp_server): """ Test checks to see if the client is able to retrieve files from the backup server. """ - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - backup_server: Server = uc2_network.get_node_by_hostname("backup_server") - - ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] - ftp_server_service: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server_service.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING # create file on ftp server - ftp_server_service.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") assert ftp_client.request_file( src_folder_name="file_share", src_file_name="test_file.txt", dest_folder_name="downloads", dest_file_name="test_file.txt", - dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address, ) # client should have retrieved the file assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") -def test_ftp_client_tries_to_connect_to_offline_server(uc2_network): +def test_ftp_client_tries_to_connect_to_offline_server(ftp_client_and_ftp_server): """Test checks to make sure that the client can't do anything when the server is offline.""" - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - backup_server: Server = uc2_network.get_node_by_hostname("backup_server") - - ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] - ftp_server_service: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server_service.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING # create file on ftp server - ftp_server_service.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") - backup_server.power_off() + server.power_off() - for i in range(backup_server.shut_down_duration + 1): - uc2_network.apply_timestep(timestep=i) + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) assert ftp_client.operating_state == ServiceOperatingState.RUNNING - assert ftp_server_service.operating_state == ServiceOperatingState.STOPPED + assert ftp_server.operating_state == ServiceOperatingState.STOPPED assert ( ftp_client.request_file( @@ -90,7 +98,7 @@ def test_ftp_client_tries_to_connect_to_offline_server(uc2_network): src_file_name="test_file.txt", dest_folder_name="downloads", dest_file_name="test_file.txt", - dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + dest_ip_address=server.nics.get(next(iter(server.nics))).ip_address, ) is False ) diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f2cc5b5d..41982805 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -1,103 +1,118 @@ +from typing import Tuple + +import pytest + from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.http import HttpStatusCode from primaite.simulator.system.applications.application import ApplicationOperatingState 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 -def test_web_page_home_page(uc2_network): - """Test to see if the browser is able to open the main page of the web server.""" - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() - web_client.target_url = "http://arcd.com/" - assert web_client.operating_state == ApplicationOperatingState.RUNNING +@pytest.fixture(scope="function") +def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebServer, Server]: + computer, server = client_server - assert web_client.get_webpage() is True + # Install Web Browser on computer + computer.software_manager.install(WebBrowser) + web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser.run() - # latest reponse should have status code 200 - assert web_client.latest_response is not None - assert web_client.latest_response.status_code == HttpStatusCode.OK + # Install DNS Client service on computer + computer.software_manager.install(DNSClient) + dns_client: DNSClient = computer.software_manager.software["DNSClient"] + # set dns server + dns_client.dns_server = server.nics[next(iter(server.nics))].ip_address + + # Install Web Server service on server + server.software_manager.install(WebServer) + web_server_service: WebServer = server.software_manager.software["WebServer"] + web_server_service.start() + + # Install DNS Server service on server + server.software_manager.install(DNSServer) + dns_server: DNSServer = server.software_manager.software["DNSServer"] + # register arcd.com to DNS + dns_server.dns_register(domain_name="arcd.com", domain_ip_address=server.nics[next(iter(server.nics))].ip_address) + + return web_browser, computer, web_server_service, server -def test_web_page_get_users_page_request_with_domain_name(uc2_network): +def test_web_page_get_users_page_request_with_domain_name(web_client_and_web_server): """Test to see if the client can handle requests with domain names""" - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() - assert web_client.operating_state == ApplicationOperatingState.RUNNING + web_browser_app, computer, web_server_service, server = web_client_and_web_server - assert web_client.get_webpage() is True + web_server_ip = server.nics.get(next(iter(server.nics))).ip_address + web_browser_app.target_url = f"http://arcd.com/" + assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING - # latest reponse should have status code 200 - assert web_client.latest_response is not None - assert web_client.latest_response.status_code == HttpStatusCode.OK + assert web_browser_app.get_webpage() is True + + # latest response should have status code 200 + assert web_browser_app.latest_response is not None + assert web_browser_app.latest_response.status_code == HttpStatusCode.OK -def test_web_page_get_users_page_request_with_ip_address(uc2_network): +def test_web_page_get_users_page_request_with_ip_address(web_client_and_web_server): """Test to see if the client can handle requests that use ip_address.""" - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() + web_browser_app, computer, web_server_service, server = web_client_and_web_server - web_server: Server = uc2_network.get_node_by_hostname("web_server") + web_server_ip = server.nics.get(next(iter(server.nics))).ip_address + web_browser_app.target_url = f"http://{web_server_ip}/" + assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING - web_server_ip = web_server.nics.get(next(iter(web_server.nics))).ip_address - web_client.target_url = f"http://{web_server_ip}/users/" - assert web_client.operating_state == ApplicationOperatingState.RUNNING - - assert web_client.get_webpage() is True + assert web_browser_app.get_webpage() is True # latest response should have status code 200 - assert web_client.latest_response is not None - assert web_client.latest_response.status_code == HttpStatusCode.OK + assert web_browser_app.latest_response is not None + assert web_browser_app.latest_response.status_code == HttpStatusCode.OK -def test_web_page_request_from_shut_down_server(uc2_network): +def test_web_page_request_from_shut_down_server(web_client_and_web_server): """Test to see that the web server does not respond when the server is off.""" - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() + web_browser_app, computer, web_server_service, server = web_client_and_web_server - web_server: Server = uc2_network.get_node_by_hostname("web_server") + web_server_ip = server.nics.get(next(iter(server.nics))).ip_address + web_browser_app.target_url = f"http://arcd.com/" + assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING - assert web_client.operating_state == ApplicationOperatingState.RUNNING - - assert web_client.get_webpage("http://arcd.com/users/") is True + assert web_browser_app.get_webpage() is True # latest response should have status code 200 - assert web_client.latest_response.status_code == HttpStatusCode.OK + assert web_browser_app.latest_response is not None + assert web_browser_app.latest_response.status_code == HttpStatusCode.OK - web_server.power_off() + server.power_off() - for i in range(web_server.shut_down_duration + 1): - uc2_network.apply_timestep(timestep=i) + server.power_off() + + for i in range(server.shut_down_duration + 1): + server.apply_timestep(timestep=i) # node should be off - assert web_server.operating_state is NodeOperatingState.OFF + assert server.operating_state is NodeOperatingState.OFF - assert web_client.get_webpage("http://arcd.com/users/") is False - assert web_client.latest_response.status_code == HttpStatusCode.NOT_FOUND + assert web_browser_app.get_webpage() is False + assert web_browser_app.latest_response.status_code == HttpStatusCode.NOT_FOUND -def test_web_page_request_from_closed_web_browser(uc2_network): - client_1: Computer = uc2_network.get_node_by_hostname("client_1") - web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] - web_client.run() +def test_web_page_request_from_closed_web_browser(web_client_and_web_server): + web_browser_app, computer, web_server_service, server = web_client_and_web_server - web_server: Server = uc2_network.get_node_by_hostname("web_server") - - assert web_client.operating_state == ApplicationOperatingState.RUNNING - - assert web_client.get_webpage("http://arcd.com/users/") is True + assert web_browser_app.operating_state == ApplicationOperatingState.RUNNING + web_browser_app.target_url = f"http://arcd.com/" + assert web_browser_app.get_webpage() is True # latest response should have status code 200 - assert web_client.latest_response.status_code == HttpStatusCode.OK + assert web_browser_app.latest_response.status_code == HttpStatusCode.OK - web_client.close() + web_browser_app.close() # node should be off - assert web_client.operating_state is ApplicationOperatingState.CLOSED + assert web_browser_app.operating_state is ApplicationOperatingState.CLOSED - assert web_client.get_webpage("http://arcd.com/users/") is False + assert web_browser_app.get_webpage() is False diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py new file mode 100644 index 00000000..d7b5603d --- /dev/null +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -0,0 +1,106 @@ +from ipaddress import IPv4Address +from typing import Tuple + +import pytest + +from primaite.simulator.network.hardware.base import Link +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.simulator.system.services.database.database_service import DatabaseService +from primaite.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 + + +@pytest.fixture(scope="function") +def web_client_web_server_database() -> Tuple[Computer, Server, Server]: + # Create Computer + computer: Computer = Computer( + hostname="test_computer", + ip_address="192.168.0.1", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + + # Create Web Server + web_server = Server( + hostname="web_server", + ip_address="192.168.0.2", + subnet_mask="255.255.255.0", + operating_state=NodeOperatingState.ON, + ) + + # Create Database Server + db_server = Server( + hostname="db_server", + ip_address="192.168.0.3", + subnet_mask="255.255.255.0", + operating_state=NodeOperatingState.ON, + ) + + # Get the NICs + computer_nic = computer.nics[next(iter(computer.nics))] + server_nic = web_server.nics[next(iter(web_server.nics))] + db_server_nic = db_server.nics[next(iter(db_server.nics))] + + # Connect Computer and Server + link_computer_server = Link(endpoint_a=computer_nic, endpoint_b=server_nic) + # Should be linked + assert link_computer_server.is_up + + # Connect database server and web server + link_server_db = Link(endpoint_a=server_nic, endpoint_b=db_server_nic) + # Should be linked + assert link_computer_server.is_up + assert link_server_db.is_up + + # Install DatabaseService on db server + db_server.software_manager.install(DatabaseService) + db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service.start() + + # Install Web Browser on computer + computer.software_manager.install(WebBrowser) + web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser.run() + + # Install DNS Client service on computer + computer.software_manager.install(DNSClient) + dns_client: DNSClient = computer.software_manager.software["DNSClient"] + # set dns server + dns_client.dns_server = web_server.nics[next(iter(web_server.nics))].ip_address + + # Install Web Server service on web server + web_server.software_manager.install(WebServer) + web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service.start() + + # Install DNS Server service on web server + web_server.software_manager.install(DNSServer) + dns_server: DNSServer = web_server.software_manager.software["DNSServer"] + # register arcd.com to DNS + dns_server.dns_register( + domain_name="arcd.com", domain_ip_address=web_server.nics[next(iter(web_server.nics))].ip_address + ) + + # Install DatabaseClient service on web server + web_server.software_manager.install(DatabaseClient) + db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client.server_ip_address = IPv4Address(db_server_nic.ip_address) # set IP address of Database Server + db_client.run() + assert db_client.connect() + + return computer, web_server, db_server + + +@pytest.mark.skip(reason="waiting for a way to set this up correctly") +def test_web_client_requests_users(web_client_web_server_database): + computer, web_server, db_server = web_client_web_server_database + + web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + + web_browser.get_webpage() 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..9366d173 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,6 +185,38 @@ 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") + file_system.set_original_state() + + # 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/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index b2724369..83426409 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -1,39 +1,66 @@ +from typing import Tuple + import pytest +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.protocols.http import HttpResponsePacket, HttpStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.web_browser import WebBrowser @pytest.fixture(scope="function") -def web_client() -> Computer: - node = Computer( - hostname="web_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" +def web_browser() -> WebBrowser: + computer = Computer( + hostname="web_client", + ip_address="192.168.1.11", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) - return node + # Web Browser should be pre-installed in computer + web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser.run() + assert web_browser.operating_state is ApplicationOperatingState.RUNNING + return web_browser -def test_create_web_client(web_client): - assert web_client is not None - web_browser: WebBrowser = web_client.software_manager.software["WebBrowser"] +def test_create_web_client(): + computer = Computer( + hostname="web_client", + ip_address="192.168.1.11", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + # Web Browser should be pre-installed in computer + web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] assert web_browser.name is "WebBrowser" assert web_browser.port is Port.HTTP assert web_browser.protocol is IPProtocol.TCP -def test_receive_invalid_payload(web_client): - web_browser: WebBrowser = web_client.software_manager.software["WebBrowser"] - +def test_receive_invalid_payload(web_browser): assert web_browser.receive(payload={}) is False -def test_receive_payload(web_client): +def test_receive_payload(web_browser): payload = HttpResponsePacket(status_code=HttpStatusCode.OK) - web_browser: WebBrowser = web_client.software_manager.software["WebBrowser"] assert web_browser.latest_response is None web_browser.receive(payload=payload) assert web_browser.latest_response is not None + + +def test_invalid_target_url(web_browser): + # none value target url + web_browser.target_url = None + assert web_browser.get_webpage() is False + + +def test_non_existent_target_url(web_browser): + web_browser.target_url = "http://192.168.255.255" + assert web_browser.get_webpage() is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py similarity index 65% rename from tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py rename to tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 2b4082d9..71517855 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -5,28 +5,13 @@ import pytest from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer -from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.dns.dns_client import DNSClient -from primaite.simulator.system.services.dns.dns_server import DNSServer from primaite.simulator.system.services.service import ServiceOperatingState -@pytest.fixture(scope="function") -def dns_server() -> Node: - node = Server( - hostname="dns_server", - ip_address="192.168.1.10", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - operating_state=NodeOperatingState.ON, - ) - node.software_manager.install(software_class=DNSServer) - return node - - @pytest.fixture(scope="function") def dns_client() -> Node: node = Computer( @@ -39,14 +24,6 @@ def dns_client() -> Node: return node -def test_create_dns_server(dns_server): - assert dns_server is not None - dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] - assert dns_server_service.name is "DNSServer" - assert dns_server_service.port is Port.DNS - assert dns_server_service.protocol is IPProtocol.TCP - - def test_create_dns_client(dns_client): assert dns_client is not None dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] @@ -93,18 +70,6 @@ def test_dns_client_check_domain_exists_when_not_running(dns_client): assert dns_client_service.check_domain_exists("test.com") is False -def test_dns_server_domain_name_registration(dns_server): - """Test to check if the domain name registration works.""" - dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] - - # register the web server in the domain controller - dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) - - # return none for an unknown domain - assert dns_server_service.dns_lookup("fake-domain.com") is None - assert dns_server_service.dns_lookup("real-domain.com") is not None - - def test_dns_client_check_domain_in_cache(dns_client): """Test to make sure that the check_domain_in_cache returns the correct values.""" dns_client.operating_state = NodeOperatingState.ON @@ -118,26 +83,6 @@ def test_dns_client_check_domain_in_cache(dns_client): assert dns_client_service.check_domain_exists("real-domain.com") is True -def test_dns_server_receive(dns_server): - """Test to make sure that the DNS Server correctly responds to a DNS Client request.""" - dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] - - # register the web server in the domain controller - dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) - - assert ( - dns_server_service.receive(payload=DNSPacket(dns_request=DNSRequest(domain_name_request="fake-domain.com"))) - is False - ) - - assert ( - dns_server_service.receive(payload=DNSPacket(dns_request=DNSRequest(domain_name_request="real-domain.com"))) - is True - ) - - dns_server_service.show() - - def test_dns_client_receive(dns_client): """Test to make sure the DNS Client knows how to deal with request responses.""" dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] @@ -151,3 +96,9 @@ def test_dns_client_receive(dns_client): # domain name should be saved to cache assert dns_client_service.dns_cache["real-domain.com"] == IPv4Address("192.168.1.12") + + +def test_dns_client_receive_non_dns_payload(dns_client): + dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + + assert dns_client_service.receive(payload=None) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py new file mode 100644 index 00000000..5b65dfc2 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -0,0 +1,64 @@ +from ipaddress import IPv4Address + +import pytest + +from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.protocols.dns import DNSPacket, DNSRequest +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.services.dns.dns_server import DNSServer + + +@pytest.fixture(scope="function") +def dns_server() -> Node: + node = Server( + hostname="dns_server", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + node.software_manager.install(software_class=DNSServer) + return node + + +def test_create_dns_server(dns_server): + assert dns_server is not None + dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] + assert dns_server_service.name is "DNSServer" + assert dns_server_service.port is Port.DNS + assert dns_server_service.protocol is IPProtocol.TCP + + +def test_dns_server_domain_name_registration(dns_server): + """Test to check if the domain name registration works.""" + dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] + + # register the web server in the domain controller + dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) + + # return none for an unknown domain + assert dns_server_service.dns_lookup("fake-domain.com") is None + assert dns_server_service.dns_lookup("real-domain.com") is not None + + +def test_dns_server_receive(dns_server): + """Test to make sure that the DNS Server correctly responds to a DNS Client request.""" + dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] + + # register the web server in the domain controller + dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) + + assert ( + dns_server_service.receive(payload=DNSPacket(dns_request=DNSRequest(domain_name_request="fake-domain.com"))) + is False + ) + + assert ( + dns_server_service.receive(payload=DNSPacket(dns_request=DNSRequest(domain_name_request="real-domain.com"))) + is True + ) + + dns_server_service.show() diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py new file mode 100644 index 00000000..c079ebc4 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -0,0 +1,50 @@ +import pytest + +from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.services.ftp.ftp_client import FTPClient + + +@pytest.fixture(scope="function") +def ftp_client() -> Node: + node = Computer( + hostname="ftp_client", + ip_address="192.168.1.11", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + return node + + +def test_create_ftp_client(ftp_client): + assert ftp_client is not None + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + assert ftp_client_service.name is "FTPClient" + assert ftp_client_service.port is Port.FTP + assert ftp_client_service.protocol is IPProtocol.TCP + + +def test_ftp_client_store_file(ftp_client): + """Test to make sure the FTP Client knows how to deal with request responses.""" + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") is None + + response: FTPPacket = FTPPacket( + ftp_command=FTPCommand.STOR, + ftp_command_args={ + "dest_folder_name": "downloads", + "dest_file_name": "file.txt", + "file_size": 24, + }, + packet_payload_size=24, + status_code=FTPStatusCode.OK, + ) + + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service.receive(response) + + assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py similarity index 63% rename from tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py rename to tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 9957b6f6..0c849106 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -1,16 +1,13 @@ -from ipaddress import IPv4Address - import pytest from primaite.simulator.network.hardware.base import Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState -from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port -from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState @pytest.fixture(scope="function") @@ -26,18 +23,6 @@ def ftp_server() -> Node: return node -@pytest.fixture(scope="function") -def ftp_client() -> Node: - node = Computer( - hostname="ftp_client", - ip_address="192.168.1.11", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - operating_state=NodeOperatingState.ON, - ) - return node - - def test_create_ftp_server(ftp_server): assert ftp_server is not None ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] @@ -46,14 +31,6 @@ def test_create_ftp_server(ftp_server): assert ftp_server_service.protocol is IPProtocol.TCP -def test_create_ftp_client(ftp_client): - assert ftp_client is not None - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] - assert ftp_client_service.name is "FTPClient" - assert ftp_client_service.port is Port.FTP - assert ftp_client_service.protocol is IPProtocol.TCP - - def test_ftp_server_store_file(ftp_server): """Test to make sure the FTP Server knows how to deal with request responses.""" assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") is None @@ -74,10 +51,28 @@ def test_ftp_server_store_file(ftp_server): assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") -def test_ftp_client_store_file(ftp_client): - """Test to make sure the FTP Client knows how to deal with request responses.""" - assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") is None +def test_ftp_server_should_send_error_if_port_arg_is_invalid(ftp_server): + """Should fail if the port command receives an invalid port.""" + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.PORT, + ftp_command_args=None, + packet_payload_size=24, + ) + ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + assert ftp_server_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR + + +def test_ftp_server_receives_non_ftp_packet(ftp_server): + """Receive should return false if the service receives a non ftp packet.""" + response: FTPPacket = None + + ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + assert ftp_server_service.receive(response) is False + + +def test_offline_ftp_server_receives_request(ftp_server): + """Receive should return false if the service is stopped.""" response: FTPPacket = FTPPacket( ftp_command=FTPCommand.STOR, ftp_command_args={ @@ -86,10 +81,9 @@ def test_ftp_client_store_file(ftp_client): "file_size": 24, }, packet_payload_size=24, - status_code=FTPStatusCode.OK, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] - ftp_client_service.receive(response) - - assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") + ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service.stop() + assert ftp_server_service.operating_state is ServiceOperatingState.STOPPED + assert ftp_server_service.receive(response) is False From bf73cc2eb7daf6b4e2a13b43eac6aff3980a9629 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Wed, 29 Nov 2023 13:45:34 +0000 Subject: [PATCH 054/107] #1859 - Re-ordered the node reset function again --- src/primaite/simulator/network/container.py | 16 ++++++++++++++++ src/primaite/simulator/network/hardware/base.py | 12 ------------ .../system/applications/database_client.py | 3 ++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 7ef55c3c..1ee384f3 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -12,6 +12,8 @@ from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.network.hardware.nodes.router import Router from primaite.simulator.network.hardware.nodes.server import Server from primaite.simulator.network.hardware.nodes.switch import Switch +from primaite.simulator.system.applications.application import Application +from primaite.simulator.system.services.service import Service _LOGGER = getLogger(__name__) @@ -56,6 +58,20 @@ class Network(SimComponent): node.reset_component_for_episode(episode) for link in self.links.values(): link.reset_component_for_episode(episode) + + for node in self.nodes.values(): + node.power_on() + + # Reset software + for software in node.software_manager.software.values(): + software.reset_component_for_episode(episode) + if isinstance(software, Service): + software.start() + elif isinstance(software, Application): + software.run() + + for nic in node.nics.values(): + nic.enable() def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 825df37d..9fb007ff 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1035,8 +1035,6 @@ class Node(SimComponent): # Reset File System self.file_system.reset_component_for_episode(episode) - self.power_on() - # Reset all Nics for nic in self.nics.values(): nic.reset_component_for_episode(episode) @@ -1045,16 +1043,6 @@ class Node(SimComponent): self.sys_log.current_episode = episode self.sys_log.setup_logger() - # Reset software - for software in self.software_manager.software.values(): - software.reset_component_for_episode(episode) - if isinstance(software, Service): - software.start() - elif isinstance(software, Application): - software.run() - - for nic in self.nics.values(): - nic.enable() 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 diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 92f7e76d..b1743fad 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -37,13 +37,14 @@ class DatabaseClient(Application): """Sets the original state.""" print(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"} + 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.""" print(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: """ From 05d62a956d6e03631f70f9789477d2eb2faf4349 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 29 Nov 2023 13:18:38 +0000 Subject: [PATCH 055/107] Fix software reset issues --- sandbox.py | 72 +++++++++++++++++++ src/primaite/simulator/file_system/file.py | 4 +- .../simulator/file_system/file_system.py | 5 +- src/primaite/simulator/file_system/folder.py | 4 +- src/primaite/simulator/network/container.py | 8 +-- .../simulator/network/hardware/base.py | 6 +- .../system/applications/database_client.py | 6 +- .../system/applications/web_browser.py | 7 +- .../services/database/database_service.py | 5 +- .../system/services/dns/dns_client.py | 3 +- .../system/services/dns/dns_server.py | 3 +- .../system/services/ftp/ftp_client.py | 7 +- .../system/services/ftp/ftp_server.py | 7 +- .../red_services/data_manipulation_bot.py | 7 +- .../system/services/web_server/web_server.py | 7 +- 15 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 sandbox.py diff --git a/sandbox.py b/sandbox.py new file mode 100644 index 00000000..b08f15b1 --- /dev/null +++ b/sandbox.py @@ -0,0 +1,72 @@ +from primaite.config.load import example_config_path, load +from primaite.session.session import PrimaiteSession +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.applications.web_browser import WebBrowser +from primaite.simulator.system.services.dns.dns_client import DNSClient + +cfg = load(example_config_path()) +session = PrimaiteSession.from_config(cfg) +network = session.game.simulation.network + +dc = network.get_node_by_hostname("domain_controller") +router = network.get_node_by_hostname("router_1") +client_1 = network.get_node_by_hostname("client_1") +client_2 = network.get_node_by_hostname("client_2") +switch_1 = network.get_node_by_hostname("switch_1") +switch_2 = network.get_node_by_hostname("switch_2") +web_server = network.get_node_by_hostname("web_server") + +dns_server = dc.software_manager.software["DNSServer"] +dns_client: DNSClient = client_2.software_manager.software["DNSClient"] +web_db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] +web_browser: WebBrowser = client_2.software_manager.software["WebBrowser"] + +# print("before calling get webpage") +# router.acl.show() +# dns_server.show() +# client_2.arp.show() +# router.arp.show() +# print() + +# print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +# print("after calling get webpage") +# router.acl.show() +# dns_server.show() +# client_2.arp.show() +# router.arp.show() +# print() +# print("reset") +# print() +# print("im gonna reset") +# print() + +# web_db_client.connect() +# web_db_client.run() +# web_browser.run() +# print("client_2", client_2.operating_state) +# print("web_browser", web_browser.operating_state) +# print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +session.game.reset() +print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +session.game.reset() +print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +session.game.reset() +print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +session.game.reset() +print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +# print() +# +# print("before calling get webpage") +# router.acl.show() +# dns_server.show() +# client_2.arp.show() +# router.arp.show() +# print() +# +# print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) +# print("after calling get webpage") +# router.acl.show() +# dns_server.show() +# client_2.arp.show() +# router.arp.show() +# print() diff --git a/src/primaite/simulator/file_system/file.py b/src/primaite/simulator/file_system/file.py index f0984f89..608a1d78 100644 --- a/src/primaite/simulator/file_system/file.py +++ b/src/primaite/simulator/file_system/file.py @@ -77,14 +77,14 @@ class File(FileSystemItemABC): def set_original_state(self): """Sets the original state.""" - print(f"Setting File ({self.path}) original state on node {self.sys_log.hostname}") + _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.""" - print(f"Resetting File ({self.path}) state on node {self.sys_log.hostname}") + _LOGGER.debug(f"Resetting File ({self.path}) state on node {self.sys_log.hostname}") super().reset_component_for_episode(episode) @property diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index a6876786..31a3c5a0 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -37,7 +37,7 @@ class FileSystem(SimComponent): def set_original_state(self): """Sets the original state.""" - print(f"Setting FileSystem original state on node {self.sys_log.hostname}") + _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 @@ -48,9 +48,8 @@ class FileSystem(SimComponent): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - print(f"Resetting FileSystem state on node {self.sys_log.hostname}") + _LOGGER.debug(f"Resetting FileSystem state on node {self.sys_log.hostname}") # Move any 'original' folder that have been deleted back to folders - print(self._original_state) original_folder_uuids = self._original_state.pop("original_folder_uuids") for uuid in original_folder_uuids: if uuid in self.deleted_folders: diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index 24dbdd79..c45dd8c5 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -53,7 +53,7 @@ class Folder(FileSystemItemABC): def set_original_state(self): """Sets the original state.""" - print(f"Setting Folder ({self.name}) original state on node {self.sys_log.hostname}") + _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() @@ -70,7 +70,7 @@ class Folder(FileSystemItemABC): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - print(f"Resetting Folder ({self.name}) state on node {self.sys_log.hostname}") + _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.pop("original_file_uuids") for uuid in original_file_uuids: diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 1ee384f3..97b62f95 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -58,21 +58,19 @@ class Network(SimComponent): node.reset_component_for_episode(episode) for link in self.links.values(): link.reset_component_for_episode(episode) - + for node in self.nodes.values(): node.power_on() + for nic in node.nics.values(): + nic.enable() # Reset software for software in node.software_manager.software.values(): - software.reset_component_for_episode(episode) if isinstance(software, Service): software.start() elif isinstance(software, Application): software.run() - for nic in node.nics.values(): - nic.enable() - def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() self._node_request_manager = RequestManager() diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 9fb007ff..04c76c6b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -993,7 +993,6 @@ class Node(SimComponent): def set_original_state(self): """Sets the original state.""" - print(f"Setting node original state for {self.hostname}") for software in self.software_manager.software.values(): software.set_original_state() @@ -1020,7 +1019,6 @@ class Node(SimComponent): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - print(f"Resetting node state for {self.hostname}") super().reset_component_for_episode(episode) # Reset ARP Cache @@ -1039,11 +1037,13 @@ class Node(SimComponent): for nic in self.nics.values(): nic.reset_component_for_episode(episode) + for software in self.software_manager.software.values(): + software.reset_component_for_episode(episode) + if episode and self.sys_log: self.sys_log.current_episode = episode self.sys_log.setup_logger() - 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. diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index b1743fad..7b63d26e 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -35,14 +35,14 @@ class DatabaseClient(Application): def set_original_state(self): """Sets the original state.""" - print(f"Setting DatabaseClient WebServer original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting DataBaseClient state on node {self.software_manager.node.hostname}") + _LOGGER.debug(f"Resetting DataBaseClient state on node {self.software_manager.node.hostname}") super().reset_component_for_episode(episode) self._query_success_tracker.clear() @@ -195,4 +195,6 @@ class DatabaseClient(Application): self._query_success_tracker[query_id] = status_code == 200 if self._query_success_tracker[query_id]: _LOGGER.debug(f"Received payload {payload}") + else: + self.connected = False return True diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 88560240..8f12df4e 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -2,6 +2,7 @@ from ipaddress import IPv4Address from typing import Dict, Optional from urllib.parse import urlparse +from primaite import getLogger from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.network.protocols.http import ( HttpRequestMethod, @@ -14,6 +15,8 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.application import Application from primaite.simulator.system.services.dns.dns_client import DNSClient +_LOGGER = getLogger(__name__) + class WebBrowser(Application): """ @@ -43,14 +46,14 @@ class WebBrowser(Application): def set_original_state(self): """Sets the original state.""" - print(f"Setting WebBrowser original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting WebBrowser state on node {self.software_manager.node.hostname}") + _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: diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index 925d1df0..f9621ba5 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -2,6 +2,7 @@ from datetime import datetime from ipaddress import IPv4Address from typing import Any, Dict, List, Literal, Optional, Union +from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -10,6 +11,8 @@ from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.service import Service, ServiceOperatingState from primaite.simulator.system.software import SoftwareHealthState +_LOGGER = getLogger(__name__) + class DatabaseService(Service): """ @@ -40,7 +43,7 @@ class DatabaseService(Service): def set_original_state(self): """Sets the original state.""" - print(f"Setting DatabaseService original state on node {self.software_manager.node.hostname}") + _LOGGER.debug(f"Setting DatabaseService original state on node {self.software_manager.node.hostname}") super().set_original_state() vals_to_include = { "password", diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index 147387ae..2d3879ff 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -31,14 +31,13 @@ class DNSClient(Service): def set_original_state(self): """Sets the original state.""" - print(f"Setting DNSClient original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting DNSClient state on node {self.software_manager.node.hostname}") self.dns_cache.clear() super().reset_component_for_episode(episode) diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index 7842a07e..8decf7e9 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -30,14 +30,13 @@ class DNSServer(Service): def set_original_state(self): """Sets the original state.""" - print(f"Setting DNSServer original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting DNSServer state on node {self.software_manager.node.hostname}") self.dns_table.clear() for key, value in self._original_state["dns_table_orig"].items(): self.dns_table[key] = value diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 011b597f..23d52342 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -1,6 +1,7 @@ from ipaddress import IPv4Address from typing import Optional +from primaite import getLogger from primaite.simulator.file_system.file_system import File from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -9,6 +10,8 @@ from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC from primaite.simulator.system.services.service import ServiceOperatingState +_LOGGER = getLogger(__name__) + class FTPClient(FTPServiceABC): """ @@ -30,14 +33,14 @@ class FTPClient(FTPServiceABC): def set_original_state(self): """Sets the original state.""" - print(f"Setting FTPClient original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting FTPClient state on node {self.software_manager.node.hostname}") + _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: diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 811a8939..44d0455f 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -1,12 +1,15 @@ from ipaddress import IPv4Address from typing import Any, Dict, Optional +from primaite import getLogger from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC from primaite.simulator.system.services.service import ServiceOperatingState +_LOGGER = getLogger(__name__) + class FTPServer(FTPServiceABC): """ @@ -31,14 +34,14 @@ class FTPServer(FTPServiceABC): def set_original_state(self): """Sets the original state.""" - print(f"Setting FTPServer original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting FTPServer state on node {self.software_manager.node.hostname}") + _LOGGER.debug(f"Resetting FTPServer state on node {self.software_manager.node.hostname}") self.connections.clear() super().reset_component_for_episode(episode) diff --git a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py index 75cdee85..44a56cf1 100644 --- a/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py +++ b/src/primaite/simulator/system/services/red_services/data_manipulation_bot.py @@ -2,11 +2,14 @@ from enum import IntEnum from ipaddress import IPv4Address from typing import Optional +from primaite import getLogger from primaite.game.science import simulate_trial from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.database_client import DatabaseClient +_LOGGER = getLogger(__name__) + class DataManipulationAttackStage(IntEnum): """ @@ -49,7 +52,7 @@ class DataManipulationBot(DatabaseClient): def set_original_state(self): """Sets the original state.""" - print(f"Setting DataManipulationBot original state on node {self.software_manager.node.hostname}") + _LOGGER.debug(f"Setting DataManipulationBot original state on node {self.software_manager.node.hostname}") super().set_original_state() vals_to_include = { "server_ip_address", @@ -64,7 +67,7 @@ class DataManipulationBot(DatabaseClient): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - print(f"Resetting DataManipulationBot state on node {self.software_manager.node.hostname}") + _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: 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 f34bba37..bff29a47 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -2,6 +2,7 @@ from ipaddress import IPv4Address from typing import Any, Dict, Optional from urllib.parse import urlparse +from primaite import getLogger from primaite.simulator.network.protocols.http import ( HttpRequestMethod, HttpRequestPacket, @@ -13,6 +14,8 @@ from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.services.service import Service +_LOGGER = getLogger(__name__) + class WebServer(Service): """Class used to represent a Web Server Service in simulation.""" @@ -21,14 +24,14 @@ class WebServer(Service): def set_original_state(self): """Sets the original state.""" - print(f"Setting WebServer original state on node {self.software_manager.node.hostname}") + _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.""" - print(f"Resetting WebServer state on node {self.software_manager.node.hostname}") + _LOGGER.debug(f"Resetting WebServer state on node {self.software_manager.node.hostname}") super().reset_component_for_episode(episode) def describe_state(self) -> Dict: From a16116a688dc66c3a68efe671769c1c314eb899b Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 29 Nov 2023 13:22:15 +0000 Subject: [PATCH 056/107] Fix file system reset error --- src/primaite/simulator/file_system/file_system.py | 2 +- src/primaite/simulator/file_system/folder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index 31a3c5a0..25a584c4 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -50,7 +50,7 @@ class FileSystem(SimComponent): """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.pop("original_folder_uuids") + original_folder_uuids = self._original_state["original_folder_uuids"] for uuid in original_folder_uuids: if uuid in self.deleted_folders: self.folders[uuid] = self.deleted_folders.pop(uuid) diff --git a/src/primaite/simulator/file_system/folder.py b/src/primaite/simulator/file_system/folder.py index c45dd8c5..8fca4368 100644 --- a/src/primaite/simulator/file_system/folder.py +++ b/src/primaite/simulator/file_system/folder.py @@ -72,7 +72,7 @@ class Folder(FileSystemItemABC): """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.pop("original_file_uuids") + original_file_uuids = self._original_state["original_file_uuids"] for uuid in original_file_uuids: if uuid in self.deleted_files: self.files[uuid] = self.deleted_files.pop(uuid) From ac2f7ba757b3313e054cdc802d841539cc922ee0 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 29 Nov 2023 14:33:52 +0000 Subject: [PATCH 057/107] Fix web browser tests. --- src/primaite/simulator/system/applications/web_browser.py | 4 ++-- tests/e2e_integration_tests/test_primaite_session.py | 4 ++++ tests/integration_tests/system/test_web_client_server.py | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 8f12df4e..1531314d 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -76,7 +76,7 @@ class WebBrowser(Application): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - def get_webpage(self) -> bool: + def get_webpage(self, url: Optional[str] = None) -> bool: """ Retrieve the webpage. @@ -85,7 +85,7 @@ class WebBrowser(Application): :param: url: The address of the web page the browser requests :type: url: str """ - url = self.target_url + url = url or self.target_url if not self._can_perform_action(): return False diff --git a/tests/e2e_integration_tests/test_primaite_session.py b/tests/e2e_integration_tests/test_primaite_session.py index 086e9af8..f2b6aa3f 100644 --- a/tests/e2e_integration_tests/test_primaite_session.py +++ b/tests/e2e_integration_tests/test_primaite_session.py @@ -76,6 +76,10 @@ class TestPrimaiteSession: with pytest.raises(pydantic.ValidationError): session = TempPrimaiteSession.from_config(MISCONFIGURED_PATH) + @pytest.mark.skip( + reason="Currently software cannot be dynamically created/destroyed during simulation. Therefore, " + "reset doesn't implement software restore." + ) @pytest.mark.parametrize("temp_primaite_session", [[CFG_PATH]], indirect=True) def test_session_sim_reset(self, temp_primaite_session): with temp_primaite_session as session: diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index f2cc5b5d..3ee1e3ed 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -27,10 +27,11 @@ def test_web_page_get_users_page_request_with_domain_name(uc2_network): web_client: WebBrowser = client_1.software_manager.software["WebBrowser"] web_client.run() assert web_client.operating_state == ApplicationOperatingState.RUNNING + web_client.target_url = "http://arcd.com/users/" assert web_client.get_webpage() is True - # latest reponse should have status code 200 + # latest response should have status code 200 assert web_client.latest_response is not None assert web_client.latest_response.status_code == HttpStatusCode.OK From b2a52b2ec032a9482b8a75c0099229c33bc52247 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Wed, 29 Nov 2023 16:31:21 +0000 Subject: [PATCH 058/107] #2084: created a fixture that we can use to test things at a non end to end level --- src/primaite/simulator/network/networks.py | 12 ++- .../system/services/ftp/ftp_client.py | 5 +- tests/conftest.py | 82 +++++++++++++++++++ .../network/test_network_creation.py | 22 +++++ .../_system/_services/test_ftp_client.py | 72 ++++++++++++++++ 5 files changed, 190 insertions(+), 3 deletions(-) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index b7bd2e95..0b6fe8d4 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -51,14 +51,22 @@ def client_server_routed() -> Network: # Client 1 client_1 = Computer( - hostname="client_1", ip_address="192.168.2.2", subnet_mask="255.255.255.0", default_gateway="192.168.2.1" + hostname="client_1", + ip_address="192.168.2.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.2.1", + operating_state=NodeOperatingState.ON, ) client_1.power_on() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) # Server 1 server_1 = Server( - hostname="server_1", ip_address="192.168.1.2", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" + hostname="server_1", + ip_address="192.168.1.2", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, ) server_1.power_on() network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index b73eec7e..263d09b4 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -264,8 +264,11 @@ class FTPClient(FTPServiceABC): This helps prevent an FTP request loop - FTP client and servers can exist on the same node. """ + if not self._can_perform_action(): + return False + if payload.status_code is None: - self.sys_log.error(f"FTP Server could not be found - Error Code: {payload.status_code.value}") + self.sys_log.error(f"FTP Server could not be found - Error Code: {FTPStatusCode.NOT_FOUND.value}") return False self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}") diff --git a/tests/conftest.py b/tests/conftest.py index 8a1f885c..55db53c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,9 @@ from primaite.session.session import PrimaiteSession from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.hardware.nodes.switch import Switch from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port @@ -152,3 +154,83 @@ def client_server() -> Tuple[Computer, Server]: assert link.is_up return computer, server + + +@pytest.fixture(scope="function") +def example_network() -> Network: + """ + Create the network used for testing. + + Should only contain the nodes and links. + This would act as the base network and services and applications are installed in the relevant test file, + + -------------- -------------- + | client_1 |----- ----| server_1 | + -------------- | -------------- ------------ -------------- | -------------- + ------| switch_1 |------| router |------| switch_2 |------ + -------------- | -------------- ------------ -------------- | -------------- + | client_2 |---- ----| server_2 | + -------------- -------------- + """ + network = Network() + + # Router 1 + router_1 = Router(hostname="router_1", num_ports=5, operating_state=NodeOperatingState.ON) + router_1.configure_port(port=1, ip_address="192.168.1.1", subnet_mask="255.255.255.0") + router_1.configure_port(port=2, ip_address="192.168.10.1", subnet_mask="255.255.255.0") + + # Switch 1 + switch_1 = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON) + network.connect(endpoint_a=router_1.ethernet_ports[1], endpoint_b=switch_1.switch_ports[8]) + router_1.enable_port(1) + + # Switch 2 + switch_2 = Switch(hostname="switch_2", num_ports=8, operating_state=NodeOperatingState.ON) + network.connect(endpoint_a=router_1.ethernet_ports[2], endpoint_b=switch_2.switch_ports[8]) + router_1.enable_port(2) + + # Client 1 + client_1 = Computer( + hostname="client_1", + ip_address="192.168.10.21", + subnet_mask="255.255.255.0", + default_gateway="192.168.10.1", + operating_state=NodeOperatingState.ON, + ) + network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) + + # Client 2 + client_2 = Computer( + hostname="client_2", + ip_address="192.168.10.22", + subnet_mask="255.255.255.0", + default_gateway="192.168.10.1", + operating_state=NodeOperatingState.ON, + ) + network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) + + # Domain Controller + server_1 = Server( + hostname="server_1", + ip_address="192.168.1.10", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + + network.connect(endpoint_b=server_1.ethernet_port[1], endpoint_a=switch_1.switch_ports[1]) + + # Database Server + server_2 = Server( + hostname="server_2", + ip_address="192.168.1.14", + subnet_mask="255.255.255.0", + default_gateway="192.168.1.1", + operating_state=NodeOperatingState.ON, + ) + network.connect(endpoint_b=server_2.ethernet_port[1], endpoint_a=switch_1.switch_ports[3]) + + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) + router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) + + return network diff --git a/tests/integration_tests/network/test_network_creation.py b/tests/integration_tests/network/test_network_creation.py index 91218068..0af44dbb 100644 --- a/tests/integration_tests/network/test_network_creation.py +++ b/tests/integration_tests/network/test_network_creation.py @@ -2,6 +2,28 @@ import pytest from primaite.simulator.network.container import Network from primaite.simulator.network.hardware.base import NIC, Node +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.networks import client_server_routed + + +def test_network(example_network): + network: Network = example_network + client_1: Computer = network.get_node_by_hostname("client_1") + client_2: Computer = network.get_node_by_hostname("client_2") + server_1: Server = network.get_node_by_hostname("server_1") + server_2: Server = network.get_node_by_hostname("server_2") + + assert client_1.ping(client_2.ethernet_port[1].ip_address) + assert client_2.ping(client_1.ethernet_port[1].ip_address) + + assert server_1.ping(server_2.ethernet_port[1].ip_address) + assert server_2.ping(server_1.ethernet_port[1].ip_address) + + assert client_1.ping(server_1.ethernet_port[1].ip_address) + assert client_2.ping(server_1.ethernet_port[1].ip_address) + assert client_1.ping(server_2.ethernet_port[1].ip_address) + assert client_2.ping(server_2.ethernet_port[1].ip_address) def test_adding_removing_nodes(): diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index c079ebc4..1d7355a2 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -1,3 +1,5 @@ +from ipaddress import IPv4Address + import pytest from primaite.simulator.network.hardware.base import Node @@ -7,6 +9,7 @@ from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPS from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.service import ServiceOperatingState @pytest.fixture(scope="function") @@ -48,3 +51,72 @@ def test_ftp_client_store_file(ftp_client): ftp_client_service.receive(response) assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") + + +def test_ftp_should_not_process_commands_if_service_not_running(ftp_client): + """Method _process_ftp_command should return false if service is not running.""" + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.PORT, + ftp_command_args=Port.FTP, + status_code=FTPStatusCode.OK, + ) + + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service.stop() + assert ftp_client_service.operating_state is ServiceOperatingState.STOPPED + assert ftp_client_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR + + +def test_ftp_tries_to_senf_file__that_does_not_exist(ftp_client): + """Method send_file should return false if no file to send.""" + assert ftp_client.file_system.get_file(folder_name="root", file_name="test.txt") is None + + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + assert ftp_client_service.operating_state is ServiceOperatingState.RUNNING + assert ( + ftp_client_service.send_file( + dest_ip_address=IPv4Address("192.168.1.1"), + src_folder_name="root", + src_file_name="test.txt", + dest_folder_name="root", + dest_file_name="text.txt", + ) + is False + ) + + +def test_offline_ftp_client_receives_request(ftp_client): + """Receive should return false if the node the ftp client is installed on is offline.""" + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client.power_off() + + for i in range(ftp_client.shut_down_duration + 1): + ftp_client.apply_timestep(timestep=i) + + assert ftp_client.operating_state is NodeOperatingState.OFF + assert ftp_client_service.operating_state is ServiceOperatingState.STOPPED + + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.PORT, + ftp_command_args=Port.FTP, + status_code=FTPStatusCode.OK, + ) + + assert ftp_client_service.receive(payload=payload) is False + + +def test_receive_should_fail_if_payload_is_not_ftp(ftp_client): + """Receive should return false if the node the ftp client is installed on is not an FTPPacket.""" + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + assert ftp_client_service.receive(payload=None) is False + + +def test_receive_should_ignore_payload_with_none_status_code(ftp_client): + """Receive should ignore payload with no set status code to prevent infinite send/receive loops.""" + payload: FTPPacket = FTPPacket( + ftp_command=FTPCommand.PORT, + ftp_command_args=Port.FTP, + status_code=None, + ) + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + assert ftp_client_service.receive(payload=payload) is False From 9d39458ef39cea3a33608bf66d5096eb917d6b1d Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 29 Nov 2023 22:30:12 +0000 Subject: [PATCH 059/107] Deleted sandbox.py --- sandbox.py | 72 ------------------------------------------------------ 1 file changed, 72 deletions(-) delete mode 100644 sandbox.py diff --git a/sandbox.py b/sandbox.py deleted file mode 100644 index b08f15b1..00000000 --- a/sandbox.py +++ /dev/null @@ -1,72 +0,0 @@ -from primaite.config.load import example_config_path, load -from primaite.session.session import PrimaiteSession -from primaite.simulator.system.applications.database_client import DatabaseClient -from primaite.simulator.system.applications.web_browser import WebBrowser -from primaite.simulator.system.services.dns.dns_client import DNSClient - -cfg = load(example_config_path()) -session = PrimaiteSession.from_config(cfg) -network = session.game.simulation.network - -dc = network.get_node_by_hostname("domain_controller") -router = network.get_node_by_hostname("router_1") -client_1 = network.get_node_by_hostname("client_1") -client_2 = network.get_node_by_hostname("client_2") -switch_1 = network.get_node_by_hostname("switch_1") -switch_2 = network.get_node_by_hostname("switch_2") -web_server = network.get_node_by_hostname("web_server") - -dns_server = dc.software_manager.software["DNSServer"] -dns_client: DNSClient = client_2.software_manager.software["DNSClient"] -web_db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] -web_browser: WebBrowser = client_2.software_manager.software["WebBrowser"] - -# print("before calling get webpage") -# router.acl.show() -# dns_server.show() -# client_2.arp.show() -# router.arp.show() -# print() - -# print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -# print("after calling get webpage") -# router.acl.show() -# dns_server.show() -# client_2.arp.show() -# router.arp.show() -# print() -# print("reset") -# print() -# print("im gonna reset") -# print() - -# web_db_client.connect() -# web_db_client.run() -# web_browser.run() -# print("client_2", client_2.operating_state) -# print("web_browser", web_browser.operating_state) -# print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -session.game.reset() -print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -session.game.reset() -print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -session.game.reset() -print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -session.game.reset() -print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -# print() -# -# print("before calling get webpage") -# router.acl.show() -# dns_server.show() -# client_2.arp.show() -# router.arp.show() -# print() -# -# print("can get webpage", client_2.software_manager.software["WebBrowser"].get_webpage()) -# print("after calling get webpage") -# router.acl.show() -# dns_server.show() -# client_2.arp.show() -# router.arp.show() -# print() From 7c1ffb5ba16f0cecfa1300693329f2fc16a49d6f Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 13:48:57 +0000 Subject: [PATCH 060/107] #2084: change all instances of retrieving software from software['software_name'] to software.get() + adding some tests for describe state --- .../simulation_components/system/data_manipulation_bot.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/simulation_components/system/data_manipulation_bot.rst b/docs/source/simulation_components/system/data_manipulation_bot.rst index 5180974f..e9cfde71 100644 --- a/docs/source/simulation_components/system/data_manipulation_bot.rst +++ b/docs/source/simulation_components/system/data_manipulation_bot.rst @@ -54,7 +54,7 @@ Example ) network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) - data_manipulation_bot: DataManipulationBot = client_1.software_manager.software["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") data_manipulation_bot.run() From 3cf21e4015ece84c50584352b0b02beeec74a3b4 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 13:49:37 +0000 Subject: [PATCH 061/107] #2084: change all instances of retrieving software from software['software_name'] to software.get() + adding some tests for describe state --- .../simulation_components/system/software.rst | 2 +- src/primaite/simulator/domain/account.py | 2 +- src/primaite/simulator/network/networks.py | 10 +- .../system/applications/database_client.py | 5 +- .../system/applications/web_browser.py | 2 +- .../services/database/database_service.py | 4 +- .../simulator/system/services/service.py | 4 +- .../system/services/web_server/web_server.py | 2 +- tests/conftest.py | 2 +- .../environments/test_sb3_environment.py | 2 +- .../test_primaite_session.py | 2 +- .../test_uc2_data_manipulation_scenario.py | 6 +- .../system/test_application_on_node.py | 4 +- .../system/test_database_on_node.py | 24 ++-- .../system/test_dns_client_server.py | 4 +- .../system/test_ftp_client_server.py | 4 +- .../system/test_service_on_node.py | 4 +- .../system/test_web_client_server.py | 8 +- .../test_web_client_server_and_database.py | 14 +- .../_simulator/_domain/test_account.py | 134 +++++++++++++++++- .../_simulator/_network/test_container.py | 66 +++++++-- .../_applications/test_database_client.py | 122 ++++++++++++++++ .../_system/_applications/test_web_browser.py | 4 +- .../test_data_manipulation_bot.py | 4 +- .../_system/_services/test_database.py | 2 +- .../_system/_services/test_dns_client.py | 12 +- .../_system/_services/test_dns_server.py | 6 +- .../_system/_services/test_ftp_client.py | 14 +- .../_system/_services/test_ftp_server.py | 10 +- .../_system/_services/test_web_server.py | 12 +- 30 files changed, 394 insertions(+), 97 deletions(-) create mode 100644 tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index 1e5a0b6b..cd6b0aa3 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -28,7 +28,7 @@ See :ref:`Node Start up and Shut down` node.software_manager.install(WebServer) - web_server: WebServer = node.software_manager.software["WebServer"] + web_server: WebServer = node.software_manager.software.get("WebServer") assert web_server.operating_state is ServiceOperatingState.RUNNING # service is immediately ran after install node.power_off() diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index 1402a474..d9dad06a 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -72,7 +72,7 @@ class Account(SimComponent): "num_group_changes": self.num_group_changes, "username": self.username, "password": self.password, - "account_type": self.account_type.name, + "account_type": self.account_type.value, "enabled": self.enabled, } ) diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index 0b6fe8d4..4cd9c8d3 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -147,7 +147,7 @@ def arcd_uc2_network() -> Network: client_1.power_on() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) - db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] + db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") db_manipulation_bot.configure( server_ip_address=IPv4Address("192.168.1.14"), payload="DELETE", @@ -165,7 +165,7 @@ def arcd_uc2_network() -> Network: operating_state=NodeOperatingState.ON, ) client_2.power_on() - web_browser = client_2.software_manager.software["WebBrowser"] + web_browser = client_2.software_manager.software.get("WebBrowser") web_browser.target_url = "http://arcd.com/users/" network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) @@ -249,7 +249,7 @@ def arcd_uc2_network() -> Network: # noqa ] database_server.software_manager.install(DatabaseService) - database_service: DatabaseService = database_server.software_manager.software["DatabaseService"] # noqa + 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) # noqa @@ -268,7 +268,7 @@ def arcd_uc2_network() -> Network: web_server.power_on() web_server.software_manager.install(DatabaseClient) - database_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + database_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") database_client.configure(server_ip_address=IPv4Address("192.168.1.14")) network.connect(endpoint_b=web_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[2]) database_client.run() @@ -277,7 +277,7 @@ def arcd_uc2_network() -> Network: web_server.software_manager.install(WebServer) # register the web_server to a domain - dns_server_service: DNSServer = domain_controller.software_manager.software["DNSServer"] # noqa + dns_server_service: DNSServer = domain_controller.software_manager.software.get("DNSServer") # noqa dns_server_service.dns_register("arcd.com", web_server.ip_address) # Backup Server diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 7b63d26e..8c43c0b7 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -107,7 +107,7 @@ class DatabaseClient(Application): def disconnect(self): """Disconnect from the Database Service.""" - if self.connected and self.operating_state.RUNNING: + if self.connected and self.operating_state is ApplicationOperatingState.RUNNING: software_manager: SoftwareManager = self.software_manager software_manager.send_payload_to_session_manager( payload={"type": "disconnect"}, dest_ip_address=self.server_ip_address, dest_port=self.port @@ -186,6 +186,9 @@ class DatabaseClient(Application): :param session_id: The session id the payload relates to. :return: True. """ + if not self._can_perform_action(): + return False + if isinstance(payload, dict) and payload.get("type"): if payload["type"] == "connect_response": self.connected = payload["response"] == True diff --git a/src/primaite/simulator/system/applications/web_browser.py b/src/primaite/simulator/system/applications/web_browser.py index 1531314d..7533f6f3 100644 --- a/src/primaite/simulator/system/applications/web_browser.py +++ b/src/primaite/simulator/system/applications/web_browser.py @@ -99,7 +99,7 @@ class WebBrowser(Application): return False # get the IP address of the domain name via DNS - dns_client: DNSClient = self.software_manager.software["DNSClient"] + dns_client: DNSClient = self.software_manager.software.get("DNSClient") domain_exists = dns_client.check_domain_exists(target_domain=parsed_url.hostname) # if domain does not exist, the request fails diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index f9621ba5..6a7c80ca 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -80,7 +80,7 @@ class DatabaseService(Service): return False software_manager: SoftwareManager = self.software_manager - ftp_client_service: FTPClient = software_manager.software["FTPClient"] + ftp_client_service: FTPClient = software_manager.software.get("FTPClient") # send backup copy of database file to FTP server response = ftp_client_service.send_file( @@ -104,7 +104,7 @@ class DatabaseService(Service): return False software_manager: SoftwareManager = self.software_manager - ftp_client_service: FTPClient = software_manager.software["FTPClient"] + ftp_client_service: FTPClient = software_manager.software.get("FTPClient") # retrieve backup file from backup server response = ftp_client_service.request_file( diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 6d6cda86..e60b7700 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -109,8 +109,8 @@ class Service(IOSoftware): """ state = super().describe_state() state["operating_state"] = self.operating_state.value - state["health_state_actual"] = self.health_state_actual - state["health_state_visible"] = self.health_state_visible + state["health_state_actual"] = self.health_state_actual.value + state["health_state_visible"] = self.health_state_visible.value return state def stop(self) -> None: 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 bff29a47..e63b875a 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -120,7 +120,7 @@ class WebServer(Service): if path.startswith("users"): # get data from DatabaseServer - db_client: DatabaseClient = self.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = self.software_manager.software.get("DatabaseClient") # get all users if db_client.query("SELECT"): # query succeeded diff --git a/tests/conftest.py b/tests/conftest.py index 55db53c5..c81e4b98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -228,7 +228,7 @@ def example_network() -> Network: default_gateway="192.168.1.1", operating_state=NodeOperatingState.ON, ) - network.connect(endpoint_b=server_2.ethernet_port[1], endpoint_a=switch_1.switch_ports[3]) + network.connect(endpoint_b=server_2.ethernet_port[1], endpoint_a=switch_1.switch_ports[2]) router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.ARP, dst_port=Port.ARP, position=22) router_1.acl.add_rule(action=ACLAction.PERMIT, protocol=IPProtocol.ICMP, position=23) diff --git a/tests/e2e_integration_tests/environments/test_sb3_environment.py b/tests/e2e_integration_tests/environments/test_sb3_environment.py index c1c028a2..91cf5c1e 100644 --- a/tests/e2e_integration_tests/environments/test_sb3_environment.py +++ b/tests/e2e_integration_tests/environments/test_sb3_environment.py @@ -11,7 +11,7 @@ from primaite.game.game import PrimaiteGame from primaite.session.environment import PrimaiteGymEnv -@pytest.mark.skip(reason="no way of currently testing this") +# @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: diff --git a/tests/e2e_integration_tests/test_primaite_session.py b/tests/e2e_integration_tests/test_primaite_session.py index ed10ca24..7785e4ae 100644 --- a/tests/e2e_integration_tests/test_primaite_session.py +++ b/tests/e2e_integration_tests/test_primaite_session.py @@ -11,7 +11,7 @@ MISCONFIGURED_PATH = TEST_ASSETS_ROOT / "configs/bad_primaite_session.yaml" MULTI_AGENT_PATH = TEST_ASSETS_ROOT / "configs/multi_agent_session.yaml" -@pytest.mark.skip(reason="no way of currently testing this") +# @pytest.mark.skip(reason="no way of currently testing this") class TestPrimaiteSession: @pytest.mark.parametrize("temp_primaite_session", [[CFG_PATH]], indirect=True) def test_creating_session(self, temp_primaite_session): diff --git a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py index 81bbfc96..0dc2c031 100644 --- a/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py +++ b/tests/e2e_integration_tests/test_uc2_data_manipulation_scenario.py @@ -8,13 +8,13 @@ from primaite.simulator.system.services.red_services.data_manipulation_bot impor def test_data_manipulation(uc2_network): """Tests the UC2 data manipulation scenario end-to-end. Is a work in progress.""" client_1: Computer = uc2_network.get_node_by_hostname("client_1") - db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] + db_manipulation_bot: DataManipulationBot = client_1.software_manager.software.get("DataManipulationBot") database_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = database_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = database_server.software_manager.software.get("DatabaseService") web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_service.backup_database() diff --git a/tests/integration_tests/system/test_application_on_node.py b/tests/integration_tests/system/test_application_on_node.py index cce586da..46be5e55 100644 --- a/tests/integration_tests/system/test_application_on_node.py +++ b/tests/integration_tests/system/test_application_on_node.py @@ -18,7 +18,7 @@ def populated_node(application_class) -> Tuple[Application, Computer]: ) computer.software_manager.install(application_class) - app = computer.software_manager.software["TestApplication"] + app = computer.software_manager.software.get("TestApplication") app.run() return app, computer @@ -35,7 +35,7 @@ def test_service_on_offline_node(application_class): ) computer.software_manager.install(application_class) - app: Application = computer.software_manager.software["TestApplication"] + app: Application = computer.software_manager.software.get("TestApplication") computer.power_off() diff --git a/tests/integration_tests/system/test_database_on_node.py b/tests/integration_tests/system/test_database_on_node.py index ef2b2956..98c8c87b 100644 --- a/tests/integration_tests/system/test_database_on_node.py +++ b/tests/integration_tests/system/test_database_on_node.py @@ -10,10 +10,10 @@ from primaite.simulator.system.services.service import ServiceOperatingState def test_database_client_server_connection(uc2_network): web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") assert len(db_service.connections) == 1 @@ -23,10 +23,10 @@ def test_database_client_server_connection(uc2_network): def test_database_client_server_correct_password(uc2_network): web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") db_client.disconnect() @@ -40,10 +40,10 @@ def test_database_client_server_correct_password(uc2_network): def test_database_client_server_incorrect_password(uc2_network): web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") db_client.disconnect() db_client.configure(server_ip_address=IPv4Address("192.168.1.14"), server_password="54321") @@ -56,7 +56,7 @@ def test_database_client_server_incorrect_password(uc2_network): def test_database_client_query(uc2_network): """Tests DB query across the network returns HTTP status 200 and date.""" web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") assert db_client.connected @@ -66,13 +66,13 @@ def test_database_client_query(uc2_network): def test_create_database_backup(uc2_network): """Run the backup_database method and check if the FTP server has the relevant file.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") # back up should be created assert db_service.backup_database() is True backup_server: Server = uc2_network.get_node_by_hostname("backup_server") - ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + ftp_server: FTPServer = backup_server.software_manager.software.get("FTPServer") # backup file should exist in the backup server assert ftp_server.file_system.get_file(folder_name=db_service.uuid, file_name="database.db") is not None @@ -81,7 +81,7 @@ def test_create_database_backup(uc2_network): def test_restore_backup(uc2_network): """Run the restore_backup method and check if the backup is properly restored.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") # create a back up assert db_service.backup_database() is True @@ -100,13 +100,13 @@ def test_restore_backup(uc2_network): def test_database_client_cannot_query_offline_database_server(uc2_network): """Tests DB query across the network returns HTTP status 404 when db server is offline.""" db_server: Server = uc2_network.get_node_by_hostname("database_server") - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") assert db_server.operating_state is NodeOperatingState.ON assert db_service.operating_state is ServiceOperatingState.RUNNING web_server: Server = uc2_network.get_node_by_hostname("web_server") - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") assert db_client.connected assert db_client.query("SELECT") is True diff --git a/tests/integration_tests/system/test_dns_client_server.py b/tests/integration_tests/system/test_dns_client_server.py index 70657112..a54bf23f 100644 --- a/tests/integration_tests/system/test_dns_client_server.py +++ b/tests/integration_tests/system/test_dns_client_server.py @@ -17,14 +17,14 @@ def dns_client_and_dns_server(client_server) -> Tuple[DNSClient, Computer, DNSSe # Install DNS Client on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software["DNSClient"] + dns_client: DNSClient = computer.software_manager.software.get("DNSClient") dns_client.start() # set server as DNS Server dns_client.dns_server = IPv4Address(server.nics.get(next(iter(server.nics))).ip_address) # Install DNS Server on server server.software_manager.install(DNSServer) - dns_server: DNSServer = server.software_manager.software["DNSServer"] + dns_server: DNSServer = server.software_manager.software.get("DNSServer") dns_server.start() # register arcd.com as a domain dns_server.dns_register( diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index 32ea7f2b..1a6a8f41 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -16,12 +16,12 @@ def ftp_client_and_ftp_server(client_server) -> Tuple[FTPClient, Computer, FTPSe # Install FTP Client service on computer computer.software_manager.install(FTPClient) - ftp_client: FTPClient = computer.software_manager.software["FTPClient"] + ftp_client: FTPClient = computer.software_manager.software.get("FTPClient") ftp_client.start() # Install FTP Server service on server server.software_manager.install(FTPServer) - ftp_server: FTPServer = server.software_manager.software["FTPServer"] + ftp_server: FTPServer = server.software_manager.software.get("FTPServer") ftp_server.start() return ftp_client, computer, ftp_server, server diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 9480c358..aab1e4da 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -17,7 +17,7 @@ def populated_node( ) server.software_manager.install(service_class) - service = server.software_manager.software["TestService"] + service = server.software_manager.software.get("TestService") service.start() return server, service @@ -34,7 +34,7 @@ def test_service_on_offline_node(service_class): ) computer.software_manager.install(service_class) - service: Service = computer.software_manager.software["TestService"] + service: Service = computer.software_manager.software.get("TestService") computer.power_off() diff --git a/tests/integration_tests/system/test_web_client_server.py b/tests/integration_tests/system/test_web_client_server.py index 41982805..b3d2e891 100644 --- a/tests/integration_tests/system/test_web_client_server.py +++ b/tests/integration_tests/system/test_web_client_server.py @@ -19,23 +19,23 @@ def web_client_and_web_server(client_server) -> Tuple[WebBrowser, Computer, WebS # Install Web Browser on computer computer.software_manager.install(WebBrowser) - web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") web_browser.run() # Install DNS Client service on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software["DNSClient"] + dns_client: DNSClient = computer.software_manager.software.get("DNSClient") # set dns server dns_client.dns_server = server.nics[next(iter(server.nics))].ip_address # Install Web Server service on server server.software_manager.install(WebServer) - web_server_service: WebServer = server.software_manager.software["WebServer"] + web_server_service: WebServer = server.software_manager.software.get("WebServer") web_server_service.start() # Install DNS Server service on server server.software_manager.install(DNSServer) - dns_server: DNSServer = server.software_manager.software["DNSServer"] + dns_server: DNSServer = server.software_manager.software.get("DNSServer") # register arcd.com to DNS dns_server.dns_register(domain_name="arcd.com", domain_ip_address=server.nics[next(iter(server.nics))].ip_address) diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index d7b5603d..17458968 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -60,28 +60,28 @@ def web_client_web_server_database() -> Tuple[Computer, Server, Server]: # Install DatabaseService on db server db_server.software_manager.install(DatabaseService) - db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] + db_service: DatabaseService = db_server.software_manager.software.get("DatabaseService") db_service.start() # Install Web Browser on computer computer.software_manager.install(WebBrowser) - web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") web_browser.run() # Install DNS Client service on computer computer.software_manager.install(DNSClient) - dns_client: DNSClient = computer.software_manager.software["DNSClient"] + dns_client: DNSClient = computer.software_manager.software.get("DNSClient") # set dns server dns_client.dns_server = web_server.nics[next(iter(web_server.nics))].ip_address # Install Web Server service on web server web_server.software_manager.install(WebServer) - web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service: WebServer = web_server.software_manager.software.get("WebServer") web_server_service.start() # Install DNS Server service on web server web_server.software_manager.install(DNSServer) - dns_server: DNSServer = web_server.software_manager.software["DNSServer"] + dns_server: DNSServer = web_server.software_manager.software.get("DNSServer") # register arcd.com to DNS dns_server.dns_register( domain_name="arcd.com", domain_ip_address=web_server.nics[next(iter(web_server.nics))].ip_address @@ -89,7 +89,7 @@ def web_client_web_server_database() -> Tuple[Computer, Server, Server]: # Install DatabaseClient service on web server web_server.software_manager.install(DatabaseClient) - db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] + db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_client.server_ip_address = IPv4Address(db_server_nic.ip_address) # set IP address of Database Server db_client.run() assert db_client.connect() @@ -101,6 +101,6 @@ def web_client_web_server_database() -> Tuple[Computer, Server, Server]: def test_web_client_requests_users(web_client_web_server_database): computer, web_server, db_server = web_client_web_server_database - web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") web_browser.get_webpage() diff --git a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py index 96c34996..01ad3871 100644 --- a/tests/unit_tests/_primaite/_simulator/_domain/test_account.py +++ b/tests/unit_tests/_primaite/_simulator/_domain/test_account.py @@ -1,18 +1,140 @@ """Test the account module of the simulator.""" +import pytest + from primaite.simulator.domain.account import Account, AccountType -def test_account_serialise(): +@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 + + +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 + 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 True + + account.log_on() + account.log_off() + account.disable() + account.set_original_state() + + 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 + 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 + + +def test_enable(account): + """Should enable the account.""" + account.enabled = False + account.enable() + assert account.enabled is True + + +def test_disable(account): + """Should disable the account.""" + account.enabled = True + account.disable() + assert account.enabled is False + + +def test_log_on_increments(account): + """Should increase the log on value by 1.""" + account.num_logons = 0 + account.log_on() + assert account.num_logons is 1 + + +def test_log_off_increments(account): + """Should increase the log on value by 1.""" + account.num_logoffs = 0 + account.log_off() + assert account.num_logoffs is 1 + + +def test_account_serialise(account): """Test that an account can be serialised. If pydantic throws error then this test fails.""" - acct = Account(username="Jake", password="JakePass1!", account_type=AccountType.USER) - serialised = acct.model_dump_json() + serialised = account.model_dump_json() print(serialised) -def test_account_deserialise(): +def test_account_deserialise(account): """Test that an account can be deserialised. The test fails if pydantic throws an error.""" acct_json = ( '{"uuid":"dfb2bcaa-d3a1-48fd-af3f-c943354622b4","num_logons":0,"num_logoffs":0,"num_group_changes":0,' - '"username":"Jake","password":"JakePass1!","account_type":2,"status":2,"request_manager":null}' + '"username":"Jake","password":"totally_hashed_password","account_type":2,"status":2,"request_manager":null}' ) - acct = Account.model_validate_json(acct_json) + assert Account.model_validate_json(acct_json) + + +def test_describe_state(account): + state = account.describe_state() + assert state["num_logons"] is 0 + assert state["num_logoffs"] is 0 + 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 True + + account.log_on() + state = account.describe_state() + assert state["num_logons"] is 1 + assert state["num_logoffs"] is 0 + 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 True + + account.log_off() + 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 True + + 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 diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index 66bd59a9..92b3a91b 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -3,6 +3,64 @@ import json 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.computer import Computer +from primaite.simulator.system.applications.database_client import DatabaseClient +from primaite.simulator.system.services.database.database_service import DatabaseService + + +@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 + + example_network.set_original_state() + + return example_network + + +def test_describe_state(example_network): + """Test that describe state works.""" + state = example_network.describe_state() + + assert len(state["nodes"]) is 7 + assert len(state["links"]) is 6 + + +def test_reset_network(example_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 = example_network.describe_state() + + client_1: Computer = example_network.get_node_by_hostname("client_1") + server_1: Computer = example_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 example_network.describe_state() is not state_before + + example_network.reset_component_for_episode(episode=1) + + assert client_1.operating_state is NodeOperatingState.ON + assert server_1.operating_state is NodeOperatingState.ON + + assert json.dumps(example_network.describe_state(), sort_keys=True, indent=2) == json.dumps( + state_before, sort_keys=True, indent=2 + ) def test_creating_container(): @@ -10,11 +68,3 @@ def test_creating_container(): net = Network() assert net.nodes == {} assert net.links == {} - - -@pytest.mark.skip(reason="Skipping until we tackle serialisation") -def test_describe_state(): - """Check that we can describe network state without raising errors, and that the result is JSON serialisable.""" - net = Network() - state = net.describe_state() - json.dumps(state) # if this function call raises an error, the test fails, state was not JSON-serialisable diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py new file mode 100644 index 00000000..59d44561 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_database_client.py @@ -0,0 +1,122 @@ +from ipaddress import IPv4Address +from typing import Tuple, Union + +import pytest + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.system.applications.application import ApplicationOperatingState +from primaite.simulator.system.applications.database_client import DatabaseClient + + +@pytest.fixture(scope="function") +def database_client_on_computer() -> Tuple[DatabaseClient, Computer]: + computer = Computer( + hostname="db_node", ip_address="192.168.0.1", subnet_mask="255.255.255.0", operating_state=NodeOperatingState.ON + ) + computer.software_manager.install(DatabaseClient) + + database_client: DatabaseClient = computer.software_manager.software.get("DatabaseClient") + database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) + database_client.run() + return database_client, computer + + +def test_creation(database_client_on_computer): + database_client, computer = database_client_on_computer + database_client.describe_state() + + +def test_connect_when_client_is_closed(database_client_on_computer): + """Database client should not connect when it is not running.""" + database_client, computer = database_client_on_computer + + database_client.close() + assert database_client.operating_state is ApplicationOperatingState.CLOSED + + assert database_client.connect() is False + + +def test_connect_to_database_fails_on_reattempt(database_client_on_computer): + """Database client should return False when the attempt to connect fails.""" + database_client, computer = database_client_on_computer + + database_client.connected = False + assert database_client._connect(server_ip_address=IPv4Address("192.168.0.1"), is_reattempt=True) is False + + +def test_disconnect_when_client_is_closed(database_client_on_computer): + """Database client disconnect should not do anything when it is not running.""" + database_client, computer = database_client_on_computer + + database_client.connected = True + assert database_client.server_ip_address is not None + + database_client.close() + assert database_client.operating_state is ApplicationOperatingState.CLOSED + + database_client.disconnect() + + assert database_client.connected is True + assert database_client.server_ip_address is not None + + +def test_disconnect(database_client_on_computer): + """Database client should set connected to False and remove the database server ip address.""" + database_client, computer = database_client_on_computer + + database_client.connected = True + + assert database_client.operating_state is ApplicationOperatingState.RUNNING + assert database_client.server_ip_address is not None + + database_client.disconnect() + + assert database_client.connected is False + assert database_client.server_ip_address is None + + +def test_query_when_client_is_closed(database_client_on_computer): + """Database client should return False when it is not running.""" + database_client, computer = database_client_on_computer + + database_client.close() + assert database_client.operating_state is ApplicationOperatingState.CLOSED + + assert database_client.query(sql="test") is False + + +def test_query_failed_reattempt(database_client_on_computer): + """Database client query should return False if the reattempt fails.""" + database_client, computer = database_client_on_computer + + def return_false(): + return False + + database_client.connect = return_false + + database_client.connected = False + assert database_client.query(sql="test", is_reattempt=True) is False + + +def test_query_fail_to_connect(database_client_on_computer): + """Database client query should return False if the connect attempt fails.""" + database_client, computer = database_client_on_computer + + def return_false(): + return False + + database_client.connect = return_false + database_client.connected = False + + assert database_client.query(sql="test") is False + + +def test_client_receives_response_when_closed(database_client_on_computer): + """Database client receive should return False when it is closed.""" + database_client, computer = database_client_on_computer + + database_client.close() + assert database_client.operating_state is ApplicationOperatingState.CLOSED + + database_client.receive(payload={}, session_id="") diff --git a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py index 83426409..dc8f7419 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_applications/test_web_browser.py @@ -21,7 +21,7 @@ def web_browser() -> WebBrowser: operating_state=NodeOperatingState.ON, ) # Web Browser should be pre-installed in computer - web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") web_browser.run() assert web_browser.operating_state is ApplicationOperatingState.RUNNING return web_browser @@ -36,7 +36,7 @@ def test_create_web_client(): operating_state=NodeOperatingState.ON, ) # Web Browser should be pre-installed in computer - web_browser: WebBrowser = computer.software_manager.software["WebBrowser"] + web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") assert web_browser.name is "WebBrowser" assert web_browser.port is Port.HTTP assert web_browser.protocol is IPProtocol.TCP diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py index 3b1e4aa4..2c4826bf 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/_red_services/test_data_manipulation_bot.py @@ -19,11 +19,11 @@ def dm_client() -> Node: @pytest.fixture def dm_bot(dm_client) -> DataManipulationBot: - return dm_client.software_manager.software["DataManipulationBot"] + return dm_client.software_manager.software.get("DataManipulationBot") def test_create_dm_bot(dm_client): - data_manipulation_bot: DataManipulationBot = dm_client.software_manager.software["DataManipulationBot"] + 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 diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py index 7662fbff..4d96b584 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_database.py @@ -8,7 +8,7 @@ from primaite.simulator.system.services.database.database_service import Databas def database_server() -> Node: node = Node(hostname="db_node") node.software_manager.install(DatabaseService) - node.software_manager.software["DatabaseService"].start() + node.software_manager.software.get("DatabaseService").start() return node diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py index 71517855..2bcb512d 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_client.py @@ -26,14 +26,14 @@ def dns_client() -> Node: def test_create_dns_client(dns_client): assert dns_client is not None - dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") assert dns_client_service.name is "DNSClient" assert dns_client_service.port is Port.DNS assert dns_client_service.protocol is IPProtocol.TCP def test_dns_client_add_domain_to_cache_when_not_running(dns_client): - dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") assert dns_client.operating_state is NodeOperatingState.OFF assert dns_client_service.operating_state is ServiceOperatingState.STOPPED @@ -46,7 +46,7 @@ def test_dns_client_add_domain_to_cache_when_not_running(dns_client): def test_dns_client_check_domain_exists_when_not_running(dns_client): dns_client.operating_state = NodeOperatingState.ON - dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") dns_client_service.start() assert dns_client.operating_state is NodeOperatingState.ON @@ -73,7 +73,7 @@ def test_dns_client_check_domain_exists_when_not_running(dns_client): def test_dns_client_check_domain_in_cache(dns_client): """Test to make sure that the check_domain_in_cache returns the correct values.""" dns_client.operating_state = NodeOperatingState.ON - dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") dns_client_service.start() # add a domain to the dns client cache @@ -85,7 +85,7 @@ def test_dns_client_check_domain_in_cache(dns_client): def test_dns_client_receive(dns_client): """Test to make sure the DNS Client knows how to deal with request responses.""" - dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") dns_client_service.receive( payload=DNSPacket( @@ -99,6 +99,6 @@ def test_dns_client_receive(dns_client): def test_dns_client_receive_non_dns_payload(dns_client): - dns_client_service: DNSClient = dns_client.software_manager.software["DNSClient"] + dns_client_service: DNSClient = dns_client.software_manager.software.get("DNSClient") assert dns_client_service.receive(payload=None) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py index 5b65dfc2..eb042c92 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_dns_server.py @@ -26,7 +26,7 @@ def dns_server() -> Node: def test_create_dns_server(dns_server): assert dns_server is not None - dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] + dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") assert dns_server_service.name is "DNSServer" assert dns_server_service.port is Port.DNS assert dns_server_service.protocol is IPProtocol.TCP @@ -34,7 +34,7 @@ def test_create_dns_server(dns_server): def test_dns_server_domain_name_registration(dns_server): """Test to check if the domain name registration works.""" - dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] + dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") # register the web server in the domain controller dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) @@ -46,7 +46,7 @@ def test_dns_server_domain_name_registration(dns_server): def test_dns_server_receive(dns_server): """Test to make sure that the DNS Server correctly responds to a DNS Client request.""" - dns_server_service: DNSServer = dns_server.software_manager.software["DNSServer"] + dns_server_service: DNSServer = dns_server.software_manager.software.get("DNSServer") # register the web server in the domain controller dns_server_service.dns_register(domain_name="real-domain.com", domain_ip_address=IPv4Address("192.168.1.12")) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py index 1d7355a2..134f82bd 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_client.py @@ -26,7 +26,7 @@ def ftp_client() -> Node: def test_create_ftp_client(ftp_client): assert ftp_client is not None - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") assert ftp_client_service.name is "FTPClient" assert ftp_client_service.port is Port.FTP assert ftp_client_service.protocol is IPProtocol.TCP @@ -47,7 +47,7 @@ def test_ftp_client_store_file(ftp_client): status_code=FTPStatusCode.OK, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") ftp_client_service.receive(response) assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") @@ -61,7 +61,7 @@ def test_ftp_should_not_process_commands_if_service_not_running(ftp_client): status_code=FTPStatusCode.OK, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") ftp_client_service.stop() assert ftp_client_service.operating_state is ServiceOperatingState.STOPPED assert ftp_client_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR @@ -71,7 +71,7 @@ def test_ftp_tries_to_senf_file__that_does_not_exist(ftp_client): """Method send_file should return false if no file to send.""" assert ftp_client.file_system.get_file(folder_name="root", file_name="test.txt") is None - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") assert ftp_client_service.operating_state is ServiceOperatingState.RUNNING assert ( ftp_client_service.send_file( @@ -87,7 +87,7 @@ def test_ftp_tries_to_senf_file__that_does_not_exist(ftp_client): def test_offline_ftp_client_receives_request(ftp_client): """Receive should return false if the node the ftp client is installed on is offline.""" - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") ftp_client.power_off() for i in range(ftp_client.shut_down_duration + 1): @@ -107,7 +107,7 @@ def test_offline_ftp_client_receives_request(ftp_client): def test_receive_should_fail_if_payload_is_not_ftp(ftp_client): """Receive should return false if the node the ftp client is installed on is not an FTPPacket.""" - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") assert ftp_client_service.receive(payload=None) is False @@ -118,5 +118,5 @@ def test_receive_should_ignore_payload_with_none_status_code(ftp_client): ftp_command_args=Port.FTP, status_code=None, ) - ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + ftp_client_service: FTPClient = ftp_client.software_manager.software.get("FTPClient") assert ftp_client_service.receive(payload=payload) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py index 0c849106..2b26c932 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp_server.py @@ -25,7 +25,7 @@ def ftp_server() -> Node: def test_create_ftp_server(ftp_server): assert ftp_server is not None - ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") assert ftp_server_service.name is "FTPServer" assert ftp_server_service.port is Port.FTP assert ftp_server_service.protocol is IPProtocol.TCP @@ -45,7 +45,7 @@ def test_ftp_server_store_file(ftp_server): packet_payload_size=24, ) - ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") ftp_server_service.receive(response) assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") @@ -59,7 +59,7 @@ def test_ftp_server_should_send_error_if_port_arg_is_invalid(ftp_server): packet_payload_size=24, ) - ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") assert ftp_server_service._process_ftp_command(payload=payload).status_code is FTPStatusCode.ERROR @@ -67,7 +67,7 @@ def test_ftp_server_receives_non_ftp_packet(ftp_server): """Receive should return false if the service receives a non ftp packet.""" response: FTPPacket = None - ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") assert ftp_server_service.receive(response) is False @@ -83,7 +83,7 @@ def test_offline_ftp_server_receives_request(ftp_server): packet_payload_size=24, ) - ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + ftp_server_service: FTPServer = ftp_server.software_manager.software.get("FTPServer") ftp_server_service.stop() assert ftp_server_service.operating_state is ServiceOperatingState.STOPPED assert ftp_server_service.receive(response) is False diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py index e6f0b9d9..bbccda27 100644 --- a/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_web_server.py @@ -18,13 +18,13 @@ def web_server() -> Server: hostname="web_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1" ) node.software_manager.install(software_class=WebServer) - node.software_manager.software["WebServer"].start() + node.software_manager.software.get("WebServer").start() return node def test_create_web_server(web_server): assert web_server is not None - web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service: WebServer = web_server.software_manager.software.get("WebServer") assert web_server_service.name is "WebServer" assert web_server_service.port is Port.HTTP assert web_server_service.protocol is IPProtocol.TCP @@ -33,7 +33,7 @@ def test_create_web_server(web_server): def test_handling_get_request_not_found_path(web_server): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/fake-path") - web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service: WebServer = web_server.software_manager.software.get("WebServer") response: HttpResponsePacket = web_server_service._handle_get_request(payload=payload) assert response.status_code == HttpStatusCode.NOT_FOUND @@ -42,7 +42,7 @@ def test_handling_get_request_not_found_path(web_server): def test_handling_get_request_home_page(web_server): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/") - web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service: WebServer = web_server.software_manager.software.get("WebServer") response: HttpResponsePacket = web_server_service._handle_get_request(payload=payload) assert response.status_code == HttpStatusCode.OK @@ -51,7 +51,7 @@ def test_handling_get_request_home_page(web_server): def test_process_http_request_get(web_server): payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/") - web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service: WebServer = web_server.software_manager.software.get("WebServer") assert web_server_service._process_http_request(payload=payload) is True @@ -59,6 +59,6 @@ def test_process_http_request_get(web_server): def test_process_http_request_method_not_allowed(web_server): payload = HttpRequestPacket(request_method=HttpRequestMethod.DELETE, request_url="http://domain.com/") - web_server_service: WebServer = web_server.software_manager.software["WebServer"] + web_server_service: WebServer = web_server.software_manager.software.get("WebServer") assert web_server_service._process_http_request(payload=payload) is False From d9de57757f85a021634d61bde26ef35561fe1bfd Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 15:47:31 +0000 Subject: [PATCH 062/107] #2084: more tests + remove concurrency in test to make sure coverage works --- .azure/azure-ci-build-pipeline.yaml | 2 +- src/primaite/simulator/network/utils.py | 2 + .../_network/_hardware/nodes/test_switch.py | 17 +++++ .../_simulator/_network/test_container.py | 63 ++++++++++++++++--- .../_simulator/_network/test_utils.py | 11 ++++ 5 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py create mode 100644 tests/unit_tests/_primaite/_simulator/_network/test_utils.py diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 49d76937..6951e350 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -86,5 +86,5 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -n auto --cov=src --cov-report=html:coverage_report --cov-fail-under=80 + pytest --cov=src --cov-report=html:coverage_report --cov-fail-under=80 displayName: 'Run tests and code coverage' diff --git a/src/primaite/simulator/network/utils.py b/src/primaite/simulator/network/utils.py index 496f5e13..33085bd6 100644 --- a/src/primaite/simulator/network/utils.py +++ b/src/primaite/simulator/network/utils.py @@ -5,6 +5,8 @@ def convert_bytes_to_megabits(B: Union[int, float]) -> float: # noqa - Keep it """ Convert Bytes (file size) to Megabits (data transfer). + Technically Mebibits - but for simplicity sake, we'll call it megabit + :param B: The file size in Bytes. :return: File bits to transfer in Megabits. """ diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py new file mode 100644 index 00000000..d2d0e52c --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/nodes/test_switch.py @@ -0,0 +1,17 @@ +import pytest + +from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState +from primaite.simulator.network.hardware.nodes.switch import Switch + + +@pytest.fixture(scope="function") +def switch() -> Switch: + switch: Switch = Switch(hostname="switch_1", num_ports=8, operating_state=NodeOperatingState.ON) + switch.show() + return switch + + +def test_describe_state(switch): + state = switch.describe_state() + assert len(state.get("ports")) is 8 + assert state.get("num_ports") is 8 diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index 92b3a91b..021d6777 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -3,6 +3,7 @@ import json import pytest from primaite.simulator.network.container import Network +from primaite.simulator.network.hardware.base import Link, Node from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer from primaite.simulator.system.applications.database_client import DatabaseClient @@ -17,19 +18,20 @@ def network(example_network) -> Network: assert len(example_network.servers) is 2 example_network.set_original_state() + example_network.show() return example_network -def test_describe_state(example_network): +def test_describe_state(network): """Test that describe state works.""" - state = example_network.describe_state() + state = network.describe_state() assert len(state["nodes"]) is 7 assert len(state["links"]) is 6 -def test_reset_network(example_network): +def test_reset_network(network): """ Test that the network is properly reset. @@ -37,10 +39,10 @@ def test_reset_network(example_network): etc are also removed/reinstalled """ - state_before = example_network.describe_state() + state_before = network.describe_state() - client_1: Computer = example_network.get_node_by_hostname("client_1") - server_1: Computer = example_network.get_node_by_hostname("server_1") + 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 @@ -51,14 +53,14 @@ def test_reset_network(example_network): server_1.power_off() assert server_1.operating_state is NodeOperatingState.SHUTTING_DOWN - assert example_network.describe_state() is not state_before + assert network.describe_state() is not state_before - example_network.reset_component_for_episode(episode=1) + network.reset_component_for_episode(episode=1) assert client_1.operating_state is NodeOperatingState.ON assert server_1.operating_state is NodeOperatingState.ON - assert json.dumps(example_network.describe_state(), sort_keys=True, indent=2) == json.dumps( + assert json.dumps(network.describe_state(), sort_keys=True, indent=2) == json.dumps( state_before, sort_keys=True, indent=2 ) @@ -68,3 +70,46 @@ def test_creating_container(): net = Network() assert net.nodes == {} assert net.links == {} + net.show() + + +def test_apply_timestep_to_nodes(network): + """Calling apply_timestep on the network should apply to the nodes within it.""" + client_1: Computer = network.get_node_by_hostname("client_1") + assert client_1.operating_state is NodeOperatingState.ON + + client_1.power_off() + + for i in range(client_1.shut_down_duration + 1): + network.apply_timestep(timestep=i) + + assert client_1.operating_state is NodeOperatingState.OFF + + +def test_removing_node_that_does_not_exist(network): + """Node that does not exist on network should not affect existing nodes.""" + assert len(network.nodes) is 7 + + network.remove_node(Node(hostname="new_node")) + assert len(network.nodes) is 7 + + +def test_remove_node(network): + """Remove node should remove the correct node.""" + assert len(network.nodes) is 7 + + client_1: Computer = network.get_node_by_hostname("client_1") + network.remove_node(client_1) + + assert network.get_node_by_hostname("client_1") is None + assert len(network.nodes) is 6 + + +def test_remove_link(network): + """Remove link should remove the correct link.""" + assert len(network.links) is 6 + link: Link = network.links.get(next(iter(network.links))) + + network.remove_link(link) + assert len(network.links) is 5 + assert network.links.get(link.uuid) is None diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_utils.py b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py new file mode 100644 index 00000000..a0c1da45 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_network/test_utils.py @@ -0,0 +1,11 @@ +from primaite.simulator.network.utils import convert_bytes_to_megabits, convert_megabits_to_bytes + + +def test_convert_bytes_to_megabits(): + assert round(convert_bytes_to_megabits(B=131072), 5) == float(1) + assert round(convert_bytes_to_megabits(B=69420), 5) == float(0.52963) + + +def test_convert_megabits_to_bytes(): + assert round(convert_megabits_to_bytes(Mbits=1), 5) == float(131072) + assert round(convert_megabits_to_bytes(Mbits=float(0.52963)), 5) == float(69419.66336) From 423436c3adb3e9c71a7ef00e6edad51e398427e0 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 16:32:31 +0000 Subject: [PATCH 063/107] #2084: testing webbrowser requesting database service user data via web server --- .../system/applications/database_client.py | 3 +- tests/conftest.py | 14 +++--- .../test_web_client_server_and_database.py | 46 ++++++++++--------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/primaite/simulator/system/applications/database_client.py b/src/primaite/simulator/system/applications/database_client.py index 8c43c0b7..f57246fc 100644 --- a/src/primaite/simulator/system/applications/database_client.py +++ b/src/primaite/simulator/system/applications/database_client.py @@ -73,7 +73,8 @@ class DatabaseClient(Application): if not self.connected: return self._connect(self.server_ip_address, self.server_password) - return False + # already connected + return True def _connect( self, server_ip_address: IPv4Address, password: Optional[str] = None, is_reattempt: bool = False diff --git a/tests/conftest.py b/tests/conftest.py index c81e4b98..1ab07dd8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,13 +164,13 @@ def example_network() -> Network: Should only contain the nodes and links. This would act as the base network and services and applications are installed in the relevant test file, - -------------- -------------- - | client_1 |----- ----| server_1 | - -------------- | -------------- ------------ -------------- | -------------- - ------| switch_1 |------| router |------| switch_2 |------ - -------------- | -------------- ------------ -------------- | -------------- - | client_2 |---- ----| server_2 | - -------------- -------------- + -------------- -------------- + | client_1 |----- ----| server_1 | + -------------- | -------------- -------------- -------------- | -------------- + ------| switch_1 |------| router_1 |------| switch_2 |------ + -------------- | -------------- -------------- -------------- | -------------- + | client_2 |---- ----| server_2 | + -------------- -------------- """ network = Network() diff --git a/tests/integration_tests/system/test_web_client_server_and_database.py b/tests/integration_tests/system/test_web_client_server_and_database.py index 17458968..a4ef3d52 100644 --- a/tests/integration_tests/system/test_web_client_server_and_database.py +++ b/tests/integration_tests/system/test_web_client_server_and_database.py @@ -6,7 +6,9 @@ import pytest from primaite.simulator.network.hardware.base import Link from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.web_browser import WebBrowser from primaite.simulator.system.services.database.database_service import DatabaseService @@ -16,31 +18,30 @@ from primaite.simulator.system.services.web_server.web_server import WebServer @pytest.fixture(scope="function") -def web_client_web_server_database() -> Tuple[Computer, Server, Server]: - # Create Computer - computer: Computer = Computer( - hostname="test_computer", - ip_address="192.168.0.1", - subnet_mask="255.255.255.0", - default_gateway="192.168.1.1", - operating_state=NodeOperatingState.ON, +def web_client_web_server_database(example_network) -> Tuple[Computer, Server, Server]: + # add rules to network router + router_1: Router = example_network.get_node_by_hostname("router_1") + router_1.acl.add_rule( + action=ACLAction.PERMIT, src_port=Port.POSTGRES_SERVER, dst_port=Port.POSTGRES_SERVER, position=0 ) + # Allow DNS requests + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1) + + # Allow FTP requests + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2) + + # Open port 80 for web server + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=3) + + # Create Computer + computer: Computer = example_network.get_node_by_hostname("client_1") + # Create Web Server - web_server = Server( - hostname="web_server", - ip_address="192.168.0.2", - subnet_mask="255.255.255.0", - operating_state=NodeOperatingState.ON, - ) + web_server: Server = example_network.get_node_by_hostname("server_1") # Create Database Server - db_server = Server( - hostname="db_server", - ip_address="192.168.0.3", - subnet_mask="255.255.255.0", - operating_state=NodeOperatingState.ON, - ) + db_server = example_network.get_node_by_hostname("server_2") # Get the NICs computer_nic = computer.nics[next(iter(computer.nics))] @@ -66,6 +67,7 @@ def web_client_web_server_database() -> Tuple[Computer, Server, Server]: # Install Web Browser on computer computer.software_manager.install(WebBrowser) web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") + web_browser.target_url = "http://arcd.com/users/" web_browser.run() # Install DNS Client service on computer @@ -92,15 +94,15 @@ def web_client_web_server_database() -> Tuple[Computer, Server, Server]: db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_client.server_ip_address = IPv4Address(db_server_nic.ip_address) # set IP address of Database Server db_client.run() + assert dns_client.check_domain_exists("arcd.com") assert db_client.connect() return computer, web_server, db_server -@pytest.mark.skip(reason="waiting for a way to set this up correctly") def test_web_client_requests_users(web_client_web_server_database): computer, web_server, db_server = web_client_web_server_database web_browser: WebBrowser = computer.software_manager.software.get("WebBrowser") - web_browser.get_webpage() + assert web_browser.get_webpage() From 9d4e564e0e47bf878ea5a3d83562178af73aa0f3 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 18:32:03 +0000 Subject: [PATCH 064/107] #2084: upload reports --- .azure/azure-ci-build-pipeline.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 6951e350..0b02626c 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -86,5 +86,14 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest --cov=src --cov-report=html:coverage_report --cov-fail-under=80 + pytest --cov=src --cov-report=html:coverage_report --cov-report=xml --cov-fail-under=80 displayName: 'Run tests and code coverage' + + - task: PublishCodeCoverageResults@1 + displayName: 'Publish coverage report' + condition: succeededOrFailed() + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: 'coverage.xml' + reportDirectory: 'coverage_report' + failIfCoverageEmpty: true From bfb631f88ccf8ce7983e5cb1ed295b199a310363 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 18:45:27 +0000 Subject: [PATCH 065/107] #2084: upload reports - use default htmlcov location --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 0b02626c..12a454fa 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -86,7 +86,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest --cov=src --cov-report=html:coverage_report --cov-report=xml --cov-fail-under=80 + pytest --cov=src --cov-report=html --cov-report=xml --cov-fail-under=80 displayName: 'Run tests and code coverage' - task: PublishCodeCoverageResults@1 From d60250e1b870db09eb6fc0c9433a992b9ae6efc0 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 19:21:11 +0000 Subject: [PATCH 066/107] #2084: upload reports - azure cannot find things --- .azure/azure-ci-build-pipeline.yaml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 12a454fa..04d35ab2 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -6,6 +6,18 @@ trigger: - bugfix/* - release/* +pr: + autoCancel: true # automatically cancel PR if new push made + drafts: true # get triggered when doing drafts + branches: + include: + - main + - dev + - feature/* + - hotfix/* + - bugfix/* + - release/* + parameters: # https://stackoverflow.com/a/70046417 - name: matrix @@ -94,6 +106,6 @@ stages: condition: succeededOrFailed() inputs: codeCoverageTool: Cobertura - summaryFileLocation: 'coverage.xml' - reportDirectory: 'coverage_report' + summaryFileLocation: './coverage.xml' + reportDirectory: './coverage_report' failIfCoverageEmpty: true From 4572afac6926c7024d75400f313d7518526848e1 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 19:34:18 +0000 Subject: [PATCH 067/107] #2084: upload reports - debug --- .azure/azure-ci-build-pipeline.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 04d35ab2..ef8f984b 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -101,6 +101,10 @@ stages: pytest --cov=src --cov-report=html --cov-report=xml --cov-fail-under=80 displayName: 'Run tests and code coverage' + + - script: pwd + - script: ls + - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' condition: succeededOrFailed() From 4c1bb7d786ee71475fa269960d1574856db193b7 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 19:43:23 +0000 Subject: [PATCH 068/107] #2084: upload reports - debug --- .azure/azure-ci-build-pipeline.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index ef8f984b..777a4e50 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -64,6 +64,9 @@ stages: versionSpec: ${{ item.py }} displayName: 'Use Python ${{ item.py }}' + - script: pwd + - script: ls + - script: | python -m pip install pre-commit pre-commit install @@ -101,10 +104,6 @@ stages: pytest --cov=src --cov-report=html --cov-report=xml --cov-fail-under=80 displayName: 'Run tests and code coverage' - - - script: pwd - - script: ls - - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' condition: succeededOrFailed() From 5b5021362696b3355ec8031aa681e87c21279a9b Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 19:58:35 +0000 Subject: [PATCH 069/107] #2084: upload reports - debug --- .azure/azure-ci-build-pipeline.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 777a4e50..45df6539 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -64,9 +64,6 @@ stages: versionSpec: ${{ item.py }} displayName: 'Use Python ${{ item.py }}' - - script: pwd - - script: ls - - script: | python -m pip install pre-commit pre-commit install @@ -103,12 +100,3 @@ stages: - script: | pytest --cov=src --cov-report=html --cov-report=xml --cov-fail-under=80 displayName: 'Run tests and code coverage' - - - task: PublishCodeCoverageResults@1 - displayName: 'Publish coverage report' - condition: succeededOrFailed() - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: './coverage.xml' - reportDirectory: './coverage_report' - failIfCoverageEmpty: true From c2f7d737f786494620f96deda065ed36df837a3f Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Thu, 30 Nov 2023 21:11:35 +0000 Subject: [PATCH 070/107] #2084: missed change to logger --- src/primaite/game/agent/actions.py | 2 +- src/primaite/game/agent/observations.py | 8 ++++---- src/primaite/game/agent/rewards.py | 4 ++-- src/primaite/simulator/core.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/primaite/game/agent/actions.py b/src/primaite/game/agent/actions.py index c70d4d66..8eed3ba4 100644 --- a/src/primaite/game/agent/actions.py +++ b/src/primaite/game/agent/actions.py @@ -424,7 +424,7 @@ class NetworkACLAddRuleAction(AbstractAction): elif permission == 2: permission_str = "DENY" else: - _LOGGER.warn(f"{self.__class__} received permission {permission}, expected 0 or 1.") + _LOGGER.warning(f"{self.__class__} received permission {permission}, expected 0 or 1.") if protocol_id == 0: return ["do_nothing"] # NOT SUPPORTED, JUST DO NOTHING IF WE COME ACROSS THIS diff --git a/src/primaite/game/agent/observations.py b/src/primaite/game/agent/observations.py index 93fd81b8..767514b4 100644 --- a/src/primaite/game/agent/observations.py +++ b/src/primaite/game/agent/observations.py @@ -264,7 +264,7 @@ class FolderObservation(AbstractObservation): 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.warn(msg) + _LOGGER.warning(msg) self.default_observation = { "health_status": 0, @@ -438,7 +438,7 @@ class NodeObservation(AbstractObservation): 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.warn(msg) + _LOGGER.warning(msg) # truncate service list self.folders: List[FolderObservation] = folders @@ -448,7 +448,7 @@ class NodeObservation(AbstractObservation): 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.warn(msg) + _LOGGER.warning(msg) self.nics: List[NicObservation] = nics while len(self.nics) < num_nics_per_node: @@ -456,7 +456,7 @@ class NodeObservation(AbstractObservation): while len(self.nics) > num_nics_per_node: truncated_nic = self.nics.pop() msg = f"Too many NICs in Node observation for node. Truncating service {truncated_nic.where[-1]}" - _LOGGER.warn(msg) + _LOGGER.warning(msg) self.logon_status: bool = logon_status diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 3466114c..ca6d8a12 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -210,7 +210,7 @@ class WebServer404Penalty(AbstractReward): f"{cls.__name__} could not be initialised from config because node_ref and service_ref were not " "found in reward config." ) - _LOGGER.warn(msg) + _LOGGER.warning(msg) return DummyReward() # TODO: should we error out with incorrect inputs? Probably! node_uuid = game.ref_map_nodes[node_ref] service_uuid = game.ref_map_services[service_ref] @@ -219,7 +219,7 @@ class WebServer404Penalty(AbstractReward): f"{cls.__name__} could not be initialised because node {node_ref} and service {service_ref} were not" " found in the simulator." ) - _LOGGER.warn(msg) + _LOGGER.warning(msg) return DummyReward() # TODO: consider erroring here as well return cls(node_uuid=node_uuid, service_uuid=service_uuid) diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 18a470cd..5e1953e2 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -113,7 +113,7 @@ class RequestManager(BaseModel): """ if name in self.request_types: msg = f"Overwriting request type {name}." - _LOGGER.warn(msg) + _LOGGER.warning(msg) self.request_types[name] = request_type @@ -252,6 +252,6 @@ class SimComponent(BaseModel): def parent(self, new_parent: Union["SimComponent", None]) -> None: if self._parent and new_parent: msg = f"Overwriting parent of {self.uuid}. Old parent: {self._parent.uuid}, New parent: {new_parent.uuid}" - _LOGGER.warn(msg) + _LOGGER.warning(msg) raise RuntimeWarning(msg) self._parent = new_parent From 3eb9a5ef1c21552c48037ee49bb4c1e2bd1ca5ad Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 09:13:17 +0000 Subject: [PATCH 071/107] #2084: publish coverage report --- .azure/azure-ci-build-pipeline.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 45df6539..b9a80fc4 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -100,3 +100,11 @@ stages: - script: | pytest --cov=src --cov-report=html --cov-report=xml --cov-fail-under=80 displayName: 'Run tests and code coverage' + + - task: PublishCodeCoverageResults@1 + displayName: 'Publish coverage report' + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: './coverage.xml' + reportDirectory: './htmlcov' + failIfCoverageEmpty: true From f1c706631f44f53db7701f9c153bbb5e94383230 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 10:02:12 +0000 Subject: [PATCH 072/107] #2084: publish coverage report --- .azure/azure-ci-build-pipeline.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index b9a80fc4..05de0050 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -105,6 +105,7 @@ stages: displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura - summaryFileLocation: './coverage.xml' - reportDirectory: './htmlcov' + summaryFileLocation: $(System.DefaultWorkingDirectory)/coverage.xml + pathToSources: $(System.DefaultWorkingDirectory)/src/ + reportDirectory: $(System.DefaultWorkingDirectory)/htmlcov/ failIfCoverageEmpty: true From 4ad93b09617c39da7bf7b8bac684fc9dd054a5f1 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 10:32:48 +0000 Subject: [PATCH 073/107] #2084: publish coverage report + more verbose test output --- .azure/azure-ci-build-pipeline.yaml | 11 +++++++++-- .gitignore | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 05de0050..58d5454e 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,14 +98,21 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest --cov=src --cov-report=html --cov-report=xml --cov-fail-under=80 + pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:coverage.xml --cov-report html:src/coverage/html displayName: 'Run tests and code coverage' + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testRunner: JUnit + testResultsFiles: 'junit/**.xml' + testRunTitle: 'Publish test results' + - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura summaryFileLocation: $(System.DefaultWorkingDirectory)/coverage.xml pathToSources: $(System.DefaultWorkingDirectory)/src/ - reportDirectory: $(System.DefaultWorkingDirectory)/htmlcov/ + reportDirectory: $(System.DefaultWorkingDirectory)/src/coverage/html/ failIfCoverageEmpty: true diff --git a/.gitignore b/.gitignore index f6231bac..ef842c6e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports +junit/ htmlcov/ .tox/ .nox/ From 3642e87eda51f2317a15dad7908d78f2b5246a10 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Dec 2023 11:07:57 +0000 Subject: [PATCH 074/107] Remove distracting debug print statements --- src/primaite/session/policy/sb3.py | 3 +-- .../simulator/system/services/database/database_service.py | 2 +- .../simulator/system/services/web_server/web_server.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/primaite/session/policy/sb3.py b/src/primaite/session/policy/sb3.py index 051e2770..254baf4d 100644 --- a/src/primaite/session/policy/sb3.py +++ b/src/primaite/session/policy/sb3.py @@ -51,14 +51,13 @@ class SB3Policy(PolicyABC, identifier="SB3"): def eval(self, n_episodes: int, deterministic: bool) -> None: """Evaluate the agent.""" - reward_data = evaluate_policy( + _ = evaluate_policy( self._agent, self.session.env, n_eval_episodes=n_episodes, deterministic=deterministic, return_episode_rewards=True, ) - print(reward_data) def save(self, save_path: Path) -> None: """ diff --git a/src/primaite/simulator/system/services/database/database_service.py b/src/primaite/simulator/system/services/database/database_service.py index f9621ba5..bba4e777 100644 --- a/src/primaite/simulator/system/services/database/database_service.py +++ b/src/primaite/simulator/system/services/database/database_service.py @@ -56,7 +56,7 @@ class DatabaseService(Service): def reset_component_for_episode(self, episode: int): """Reset the original state of the SimComponent.""" - print("Resetting DatabaseService original state on node {self.software_manager.node.hostname}") + _LOGGER.debug("Resetting DatabaseService original state on node {self.software_manager.node.hostname}") self.connections.clear() super().reset_component_for_episode(episode) 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 bff29a47..e5f3dccc 100644 --- a/src/primaite/simulator/system/services/web_server/web_server.py +++ b/src/primaite/simulator/system/services/web_server/web_server.py @@ -47,7 +47,6 @@ class WebServer(Service): state["last_response_status_code"] = ( self.last_response_status_code.value if isinstance(self.last_response_status_code, HttpStatusCode) else None ) - print(state) return state def __init__(self, **kwargs): From 74b8f58b365e784999abc29e8735fe618bdf01ab Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 11:22:30 +0000 Subject: [PATCH 075/107] #2084: debug pipeline --- .azure/azure-ci-build-pipeline.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 58d5454e..3b46302a 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -108,6 +108,14 @@ stages: testResultsFiles: 'junit/**.xml' testRunTitle: 'Publish test results' + - script: | + pwd && ls + displayName: 'debug root' + + - script: | + cd src && pwd && ls + displayName: 'debug src' + - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' inputs: From 6430a7588d57c57211a3920d8f377de3abed2dec Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 11:38:34 +0000 Subject: [PATCH 076/107] #2084: debug pipeline --- .azure/azure-ci-build-pipeline.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 3b46302a..fa0fec5b 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -109,13 +109,17 @@ stages: testRunTitle: 'Publish test results' - script: | - pwd && ls + echo '$(System.DefaultWorkingDirectory)' && pwd && ls displayName: 'debug root' - script: | cd src && pwd && ls displayName: 'debug src' + - script: | + cd src/coverage/html && pwd && ls + displayName: 'debug src/coverage/html' + - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' inputs: From 738aeed0a5eff9e0ee6eadcdc564f7f4efe83d29 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 12:05:12 +0000 Subject: [PATCH 077/107] #2084: debug pipeline --- .azure/azure-ci-build-pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index fa0fec5b..dffa5aa5 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -113,11 +113,11 @@ stages: displayName: 'debug root' - script: | - cd src && pwd && ls + cd $(System.DefaultWorkingDirectory)/src && pwd && ls displayName: 'debug src' - script: | - cd src/coverage/html && pwd && ls + cd $(System.DefaultWorkingDirectory)/src/coverage/html && pwd && ls displayName: 'debug src/coverage/html' - task: PublishCodeCoverageResults@1 From 1dbea3041a7f77e4ed523e1ea8887cfbf56c2120 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 12:46:12 +0000 Subject: [PATCH 078/107] #2084: add files + remove extra slash --- .azure/azure-ci-build-pipeline.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index dffa5aa5..72d5427a 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -126,5 +126,6 @@ stages: codeCoverageTool: Cobertura summaryFileLocation: $(System.DefaultWorkingDirectory)/coverage.xml pathToSources: $(System.DefaultWorkingDirectory)/src/ - reportDirectory: $(System.DefaultWorkingDirectory)/src/coverage/html/ + reportDirectory: $(System.DefaultWorkingDirectory)/src/coverage/html + additionalCodeCoverageFiles: $(System.DefaultWorkingDirectory)/src/coverage/html/*.* failIfCoverageEmpty: true From c21a52d3f7d09a880d880ce9440307aae2d12fa3 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 13:26:27 +0000 Subject: [PATCH 079/107] #2084: debug pipeline --- .azure/azure-ci-build-pipeline.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 72d5427a..b919383e 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -113,12 +113,8 @@ stages: displayName: 'debug root' - script: | - cd $(System.DefaultWorkingDirectory)/src && pwd && ls - displayName: 'debug src' - - - script: | - cd $(System.DefaultWorkingDirectory)/src/coverage/html && pwd && ls - displayName: 'debug src/coverage/html' + cd $(System.DefaultWorkingDirectory)/htmlcov && pwd && ls + displayName: 'debug htmlcov' - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' @@ -126,6 +122,6 @@ stages: codeCoverageTool: Cobertura summaryFileLocation: $(System.DefaultWorkingDirectory)/coverage.xml pathToSources: $(System.DefaultWorkingDirectory)/src/ - reportDirectory: $(System.DefaultWorkingDirectory)/src/coverage/html - additionalCodeCoverageFiles: $(System.DefaultWorkingDirectory)/src/coverage/html/*.* + reportDirectory: $(System.DefaultWorkingDirectory)/htmlcov + additionalCodeCoverageFiles: $(System.DefaultWorkingDirectory)/htmlcov/*.* failIfCoverageEmpty: true From 4f57403751a2e491dc8dbc07286f40d89d35d623 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 13:52:06 +0000 Subject: [PATCH 080/107] #2084: debug pipeline --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index b919383e..850568f6 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,7 +98,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:coverage.xml --cov-report html:src/coverage/html + pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:coverage.xml --cov-report html displayName: 'Run tests and code coverage' - task: PublishTestResults@2 From 656cb03b16b9b0bcbbf4a3389799bd116da42b06 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 14:34:42 +0000 Subject: [PATCH 081/107] #2084: print content of coverage.xml --- .azure/azure-ci-build-pipeline.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 850568f6..bddcc86c 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -116,6 +116,10 @@ stages: cd $(System.DefaultWorkingDirectory)/htmlcov && pwd && ls displayName: 'debug htmlcov' + - script: | + cat $(System.DefaultWorkingDirectory)/coverage.xml + displayName: 'debug coverage file' + - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' inputs: From d6fedf007919de30be1d7ee3ab4aa1606d341ef0 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 14:58:22 +0000 Subject: [PATCH 082/107] #2084: maybe pointing to different source might help --- .azure/azure-ci-build-pipeline.yaml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index bddcc86c..147b8c14 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -108,24 +108,12 @@ stages: testResultsFiles: 'junit/**.xml' testRunTitle: 'Publish test results' - - script: | - echo '$(System.DefaultWorkingDirectory)' && pwd && ls - displayName: 'debug root' - - - script: | - cd $(System.DefaultWorkingDirectory)/htmlcov && pwd && ls - displayName: 'debug htmlcov' - - - script: | - cat $(System.DefaultWorkingDirectory)/coverage.xml - displayName: 'debug coverage file' - - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura summaryFileLocation: $(System.DefaultWorkingDirectory)/coverage.xml - pathToSources: $(System.DefaultWorkingDirectory)/src/ + pathToSources: $(System.DefaultWorkingDirectory)/ reportDirectory: $(System.DefaultWorkingDirectory)/htmlcov additionalCodeCoverageFiles: $(System.DefaultWorkingDirectory)/htmlcov/*.* failIfCoverageEmpty: true From 321d1f7219d134e18e98db584e8273f7c06385d6 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Dec 2023 14:58:34 +0000 Subject: [PATCH 083/107] Fix rllib marl problems --- .gitignore | 1 + .../example_config_2_rl_agents.yaml | 108 +- .../training_example_ray_multi_agent.ipynb | 44 +- .../training_example_ray_single_agent.ipynb | 969 ++++++++++++++++-- src/primaite/session/environment.py | 21 +- src/primaite/session/policy/rllib.py | 15 +- 6 files changed, 989 insertions(+), 169 deletions(-) diff --git a/.gitignore b/.gitignore index f6231bac..a6404ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,4 @@ simulation_output/ benchmark/output # src/primaite/notebooks/scratch.ipynb src/primaite/notebooks/scratch.py +sandbox.py 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 9450c419..b811bfa5 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,14 +1,10 @@ training_config: - rl_framework: RLLIB_single_agent - rl_algorithm: PPO - seed: 333 - n_learn_episodes: 1 - n_eval_episodes: 5 - max_steps_per_episode: 256 - deterministic_eval: false - n_agents: 1 + rl_framework: RLLIB_multi_agent + # rl_framework: SB3 + n_agents: 2 agent_references: - - defender + - defender_1 + - defender_2 io_settings: save_checkpoints: true @@ -36,31 +32,26 @@ agents: action_space: action_list: - type: DONOTHING - # - # - type: NODE_LOGON - # - type: NODE_LOGOFF - # - type: NODE_APPLICATION_EXECUTE - # options: - # execution_definition: - # target_address: arcd.com - + - type: NODE_APPLICATION_EXECUTE options: nodes: - node_ref: client_2 + applications: + - application_ref: client_2_web_browser max_folders_per_node: 1 max_files_per_folder: 1 max_services_per_node: 1 - max_nics_per_node: 2 - max_acl_rules: 10 + max_applications_per_node: 1 reward_function: reward_components: - type: DUMMY agent_settings: - start_step: 5 - frequency: 4 - variance: 3 + start_settings: + start_step: 5 + frequency: 4 + variance: 3 - ref: client_1_data_manipulation_red_bot team: RED @@ -69,38 +60,20 @@ agents: observation_space: type: UC2RedObservation options: - nodes: - - node_ref: client_1 - observations: - - logon_status - - operating_status - services: - - service_ref: data_manipulation_bot - observations: - operating_status - health_status - folders: {} + nodes: {} action_space: action_list: - type: DONOTHING - #\n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Python version:3.10.12
Ray version:2.8.0
\n", + "\n", + "
\n", + "\n" + ], + "text/plain": [ + "RayContext(dashboard_url='', python_version='3.10.12', ray_version='2.8.0', ray_commit='105355bd253d6538ed34d331f6a4bdf0e38ace3a', protocol_version=None)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ray.init(local_mode=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "env_config = {\"cfg\":cfg}\n", "\n", - "game = PrimaiteGame.from_config(cfg)" + "config = (\n", + " PPOConfig()\n", + " .environment(env=PrimaiteRayEnv, env_config=env_config, disable_env_checking=True)\n", + " .rollouts(num_rollout_workers=0,)\n", + " .training(train_batch_size=128)\n", + ")\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":job_id:01000000\n", + ":task_name:bundle_reservation_check_func\n", + ":actor_name:PPO\n", + "2023-12-01 14:53:17,868::ERROR::primaite.simulator.network.hardware.base::190::NIC 3e:e9:64:e8:cf:89/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,869::ERROR::primaite.simulator.network.hardware.base::190::NIC 74:17:08:49:f5:30/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,870::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,871::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,872::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,875: Added node 1c94cb0c-b62c-43eb-b5d3-4a5d1937f845 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,878: Added node 3197ef0c-0ce8-4b63-bde8-91e7f95d59ef to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,884: Added node 835b8e76-0b1e-4112-9897-0808a87fd9de to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,888::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,889: Added service 6a19bda9-7f0e-4f77-a5bc-b473d3418df0 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,890::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,891: Added service 6fc138ff-e698-4c4d-82ca-0aa990df6669 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,893: Added application 601f573a-5480-492d-8508-4b1ccc45100f to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,895::ERROR::primaite.simulator.network.hardware.base::190::NIC fd:19:e4:d5:6e:c8/192.168.1.10 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,896::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,898: Added service 901a736f-8fd0-49e9-9369-ef1da8a6a5a4 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,900::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,903: Added service e240939c-b017-4e67-b161-8e5d96cbe061 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,905: Added application dada880f-ec1f-4ed3-a4b6-3f8c68a6c750 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,906::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,908: Added service d2201c7b-418c-49f7-bed0-c93f520f0352 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", + "2023-12-01 14:53:17,909: Added node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,912::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,914: Added service 2c843f1e-643a-40d3-9477-8870926f49e8 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,916::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,918: Added service b52910ee-66d7-4006-8044-fb27a95cc00f to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,920: Added application dd11b7fb-a31a-418d-bfdb-fb862c0c38b2 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,922::ERROR::primaite.simulator.network.hardware.base::190::NIC b9:01:34:d2:50:53/192.168.1.12 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,923::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,926: Added service a1d7a58a-df23-4ea0-934f-b4f397f252e5 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,927::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,928: Added service 742f59a4-369e-4a31-a95e-19adb9115cb7 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,930: Added application 5a93e02b-5485-46ad-9c61-4f32d4673ec3 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,934: Added application 040a27ae-8c65-47fd-a60b-7c72a46e7806 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,936::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,938: Added service f6241fbe-524e-4e94-8e94-232bcd8d0914 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", + "2023-12-01 14:53:17,939: Added node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,941::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,942: Added service f69c460a-5385-4244-8582-508f806e52e4 to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,943::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,944: Added service f36088c4-0817-47a2-bd4d-1162fee46b63 to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,946: Added application 4de6ebfd-46ae-419f-b36e-3b2b079eff9d to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,949::ERROR::primaite.simulator.network.hardware.base::190::NIC 70:fb:95:ae:8b:e9/192.168.1.14 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,951::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,953: Added service 5a01c753-1c0b-4f9e-8cf9-bdab8e1151e7 to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,954::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,956: Added service 8f28720e-4374-4fe5-9b30-f2b3943691ff to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,957: Added application 500d5dc5-1bca-4904-8b2a-8d5f3c4db978 to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,958::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,960: Added service a5e5963e-c071-4715-9042-5e5887e26f3a to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,962::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,967: Added service 82dd1118-3e3c-4f72-8e62-3217e72b0360 to node 2f002450-027d-4791-832b-01327350d7e7\n", + "2023-12-01 14:53:17,969: Added node 2f002450-027d-4791-832b-01327350d7e7 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,972::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,973: Added service d8d2c796-cae8-4d6b-acd1-76a30fb3dd87 to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,974::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,975: Added service bd319940-2ad7-456b-88e9-a49b1c994edd to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,977: Added application d30f9307-d8d5-41f0-b74d-94b878b3023a to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,978::ERROR::primaite.simulator.network.hardware.base::190::NIC e8:c5:48:91:62:fe/192.168.1.16 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:17,980::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,983: Added service e926ebb1-6d91-4400-94f3-7dfab8e82eab to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,985::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,987: Added service af73e5f5-c754-4936-9018-37ef69140ced to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,988: Added application 13fd868b-e730-486f-ab83-e1bf2446e504 to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,989::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:17,991: Added service ab453a9d-62dc-4437-b0dc-c9d587962a0b to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", + "2023-12-01 14:53:17,992: Added node 858361fa-1b42-4456-b184-59fa44b0c89b to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:17,995::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,003: Added service d9b44dbd-f153-4f3c-b03d-b6441a917834 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", + "2023-12-01 14:53:18,005::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,006: Added service c3da251b-500a-40dd-8c25-e26a35b5b767 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", + "2023-12-01 14:53:18,008: Added application 8f692912-fb57-40bb-ad56-d38970d34430 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", + "2023-12-01 14:53:18,010::ERROR::primaite.simulator.network.hardware.base::190::NIC a3:59:d7:fe:28:08/192.168.1.110 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,011::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + ":job_id:01000000\n", + ":task_name:bundle_reservation_check_func\n", + ":actor_name:PPO\n", + "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" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-12-01 14:53:18,013: Added service be9a1ad7-252b-47c7-ae20-e8202cb890ab to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", + "2023-12-01 14:53:18,020::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,022: Added service c5d5b514-cd30-4c7d-a4b7-b16fce71ccac to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", + "2023-12-01 14:53:18,024: Added application 918b8a5e-e339-4cb1-bb8b-ca2f3ec34372 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", + "2023-12-01 14:53:18,026::ERROR::primaite.simulator.network.hardware.base::190::NIC c9:b5:db:9d:71:4d/192.168.10.110 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,027: Added node 7c47bb4e-deea-4c23-910d-6ba524f73bbc to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:18,040::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,042: Added service 66ca1dd3-0997-427b-8663-402141122e75 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,044::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,045: Added service f87b344d-8a8e-4d9d-a341-df5d3bb538ba to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,047: Added application ab20f1e3-e567-4d02-9e16-febcd57c2630 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,057::ERROR::primaite.simulator.network.hardware.base::190::NIC f5:2e:2d:55:76:d3/192.168.10.21 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,058::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,060: Added service 640815d4-878f-4dff-a607-08887b9045a7 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,061::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,063: Added service 655735a9-6db2-41df-b462-f8c14ea53e35 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,072: Added application 84fc60ba-b67e-4b72-86d1-d247fa7e31f7 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,074::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,075: Added service 8f83391f-a59b-4bdb-8fe7-7fe7e19bf1e9 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,077: Added application 75147ae5-cdce-4ac0-a884-fb9bd8f96387 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", + "2023-12-01 14:53:18,078: Added node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:18,084::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,086: Added service 02915c59-69e2-4248-9e46-16d627460448 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,087::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,088: Added service 3ec73168-2bad-41cd-b872-7747487410c5 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,089: Added application 2acc2c11-7eed-4382-b596-389a52b42915 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,090::ERROR::primaite.simulator.network.hardware.base::190::NIC 18:f5:a0:7f:c8:60/192.168.10.22 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,091::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,093: Added service 99ae3de9-6a18-45b7-b52e-929b2be8b69b to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,093::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,095: Added service 80932931-e76e-4b98-8d05-3998f3a23a75 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,096: Added application 267775b8-36ad-4627-a5d3-dfd42ba5ecbf to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,098::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", + "2023-12-01 14:53:18,100: Added service b5c7f3cb-d928-433d-8b07-a6a778337c27 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,102: Added application a3f18b82-33be-4659-96cb-2f069c3a2aa4 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", + "2023-12-01 14:53:18,103: Added node d8a6abb1-8929-490f-a997-5e3dcb452027 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", + "2023-12-01 14:53:18,148::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,151::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,153::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,154::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,155::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,155::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,156::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,157::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,159::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,163::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,165::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,167::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "installing DNSClient on node client_1\n", + "installing DNSClient on node client_2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":actor_name:PPO\n", + "2023-12-01 14:53:18,581::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,582::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,583::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,585::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,586::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,588::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,590::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,591::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,593::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,602::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,604::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:18,604::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + ":actor_name:PPO\n", + "Episode: 1, Step: 1, Reward: 0.5\n", + "Episode: 1, Step: 2, Reward: 0.5\n", + "Episode: 1, Step: 3, Reward: 0.5\n", + "Episode: 1, Step: 4, Reward: 0.5\n", + "Episode: 1, Step: 5, Reward: 0.5\n", + "Episode: 1, Step: 6, Reward: 0.5\n", + "Episode: 1, Step: 7, Reward: 0.5\n", + "Episode: 1, Step: 8, Reward: 0.5\n", + "Episode: 1, Step: 9, Reward: 0.5\n", + "Episode: 1, Step: 10, Reward: 0.5\n", + "Episode: 1, Step: 11, Reward: 0.5\n", + "Episode: 1, Step: 12, Reward: 0.5\n", + "Episode: 1, Step: 13, Reward: 0.5\n", + "Episode: 1, Step: 14, Reward: 0.5\n", + "Episode: 1, Step: 15, Reward: 0.5\n", + "Episode: 1, Step: 16, Reward: 0.5\n", + "Episode: 1, Step: 17, Reward: 0.5\n", + "Episode: 1, Step: 18, Reward: 0.5\n", + "Episode: 1, Step: 19, Reward: 0.5\n", + "Episode: 1, Step: 20, Reward: 0.5\n", + "Episode: 1, Step: 21, Reward: 0.5\n", + "Episode: 1, Step: 22, Reward: 0.5\n", + "Episode: 1, Step: 23, Reward: 0.5\n", + "Episode: 1, Step: 24, Reward: 0.5\n", + "Episode: 1, Step: 25, Reward: 0.5\n", + "Episode: 1, Step: 26, Reward: 0.5\n", + "Episode: 1, Step: 27, Reward: 0.5\n", + "Episode: 1, Step: 28, Reward: 0.5\n", + "Episode: 1, Step: 29, Reward: 0.5\n", + "Episode: 1, Step: 30, Reward: 0.5\n", + "Episode: 1, Step: 31, Reward: 0.5\n", + "Episode: 1, Step: 32, Reward: 0.5\n", + "Episode: 1, Step: 33, Reward: 0.5\n", + "Episode: 1, Step: 34, Reward: 0.5\n", + "Episode: 1, Step: 35, Reward: 0.5\n", + "Episode: 1, Step: 36, Reward: 0.5\n", + "Episode: 1, Step: 37, Reward: 0.5\n", + "Episode: 1, Step: 38, Reward: 0.5\n", + "Episode: 1, Step: 39, Reward: 0.5\n", + "Episode: 1, Step: 40, Reward: 0.5\n", + "Episode: 1, Step: 41, Reward: 0.5\n", + "Episode: 1, Step: 42, Reward: 0.5\n", + "Episode: 1, Step: 43, Reward: 0.5\n", + "Episode: 1, Step: 44, Reward: 0.5\n", + "Episode: 1, Step: 45, Reward: 0.5\n", + "Episode: 1, Step: 46, Reward: 0.5\n", + "Episode: 1, Step: 47, Reward: 0.5\n", + "Episode: 1, Step: 48, Reward: 0.5\n", + "Episode: 1, Step: 49, Reward: 0.5\n", + "Episode: 1, Step: 50, Reward: 0.5\n", + "Episode: 1, Step: 51, Reward: 0.5\n", + "Episode: 1, Step: 52, Reward: 0.5\n", + "Episode: 1, Step: 53, Reward: 0.5\n", + "Episode: 1, Step: 54, Reward: 0.5\n", + "Episode: 1, Step: 55, Reward: 0.5\n", + "Episode: 1, Step: 56, Reward: 0.5\n", + "Episode: 1, Step: 57, Reward: 0.5\n", + "Episode: 1, Step: 58, Reward: 0.5\n", + "Episode: 1, Step: 59, Reward: 0.5\n", + "Episode: 1, Step: 60, Reward: 0.5\n", + "Episode: 1, Step: 61, Reward: 0.5\n", + "Episode: 1, Step: 62, Reward: 0.5\n", + "Episode: 1, Step: 63, Reward: 0.5\n", + "Episode: 1, Step: 64, Reward: 0.5\n", + "Episode: 1, Step: 65, Reward: 0.5\n", + "Episode: 1, Step: 66, Reward: 0.5\n", + "Episode: 1, Step: 67, Reward: 0.5\n", + "Episode: 1, Step: 68, Reward: 0.5\n", + "Episode: 1, Step: 69, Reward: 0.5\n", + "Episode: 1, Step: 70, Reward: 0.5\n", + "Episode: 1, Step: 71, Reward: 0.5\n", + "Episode: 1, Step: 72, Reward: 0.5\n", + "Episode: 1, Step: 73, Reward: 0.5\n", + "Episode: 1, Step: 74, Reward: 0.5\n", + "Episode: 1, Step: 75, Reward: 0.5\n", + "Episode: 1, Step: 76, Reward: 0.5\n", + "Episode: 1, Step: 77, Reward: 0.5\n", + "Episode: 1, Step: 78, Reward: 0.5\n", + "Episode: 1, Step: 79, Reward: 0.5\n", + "Episode: 1, Step: 80, Reward: 0.5\n", + "Episode: 1, Step: 81, Reward: 0.5\n", + "Episode: 1, Step: 82, Reward: 0.5\n", + "Episode: 1, Step: 83, Reward: 0.5\n", + "Episode: 1, Step: 84, Reward: 0.5\n", + "Episode: 1, Step: 85, Reward: 0.5\n", + "Episode: 1, Step: 86, Reward: 0.5\n", + "Episode: 1, Step: 87, Reward: 0.5\n", + "Episode: 1, Step: 88, Reward: 0.5\n", + "Episode: 1, Step: 89, Reward: 0.5\n", + "Episode: 1, Step: 90, Reward: 0.5\n", + "Episode: 1, Step: 91, Reward: 0.5\n", + "Episode: 1, Step: 92, Reward: 0.5\n", + "Episode: 1, Step: 93, Reward: 0.5\n", + "Episode: 1, Step: 94, Reward: 0.5\n", + "Episode: 1, Step: 95, Reward: 0.5\n", + "Episode: 1, Step: 96, Reward: 0.5\n", + "Episode: 1, Step: 97, Reward: 0.5\n", + "Episode: 1, Step: 98, Reward: 0.5\n", + "Episode: 1, Step: 99, Reward: 0.5\n", + "Episode: 1, Step: 100, Reward: 0.5\n", + "Episode: 1, Step: 101, Reward: 0.5\n", + "Episode: 1, Step: 102, Reward: 0.5\n", + "Episode: 1, Step: 103, Reward: 0.5\n", + "Episode: 1, Step: 104, Reward: 0.5\n", + "Episode: 1, Step: 105, Reward: 0.5\n", + "Episode: 1, Step: 106, Reward: 0.5\n", + "Episode: 1, Step: 107, Reward: 0.5\n", + "Episode: 1, Step: 108, Reward: 0.5\n", + "Episode: 1, Step: 109, Reward: 0.5\n", + "Episode: 1, Step: 110, Reward: 0.5\n", + "Episode: 1, Step: 111, Reward: 0.5\n", + "Episode: 1, Step: 112, Reward: 0.5\n", + "Episode: 1, Step: 113, Reward: 0.5\n", + "Episode: 1, Step: 114, Reward: 0.5\n", + "Episode: 1, Step: 115, Reward: 0.5\n", + "Episode: 1, Step: 116, Reward: 0.5\n", + "Episode: 1, Step: 117, Reward: 0.5\n", + "Episode: 1, Step: 118, Reward: 0.5\n", + "Episode: 1, Step: 119, Reward: 0.5\n", + "Episode: 1, Step: 120, Reward: 0.5\n", + "Episode: 1, Step: 121, Reward: 0.5\n", + "Episode: 1, Step: 122, Reward: 0.5\n", + "Episode: 1, Step: 123, Reward: 0.5\n", + "Episode: 1, Step: 124, Reward: 0.5\n", + "Episode: 1, Step: 125, Reward: 0.5\n", + "Episode: 1, Step: 126, Reward: 0.5\n", + "Episode: 1, Step: 127, Reward: 0.5\n", + "Episode: 1, Step: 128, Reward: 0.5\n", + "Episode: 1, Step: 129, Reward: 0.5\n", + "\n", + "Episode: 1, Step: 130, Reward: 0.5\n", + "Episode: 1, Step: 131, Reward: 0.5\n", + "Episode: 1, Step: 132, Reward: 0.5\n", + "Episode: 1, Step: 133, Reward: 0.5\n", + "Episode: 1, Step: 134, Reward: 0.5\n", + "Episode: 1, Step: 135, Reward: 0.5\n", + "Episode: 1, Step: 136, Reward: 0.5\n", + "Episode: 1, Step: 137, Reward: 0.5\n", + "Episode: 1, Step: 138, Reward: 0.5\n", + "Episode: 1, Step: 139, Reward: 0.5\n", + "Episode: 1, Step: 140, Reward: 0.5\n", + "Episode: 1, Step: 141, Reward: 0.5\n", + "Episode: 1, Step: 142, Reward: 0.5\n", + "Episode: 1, Step: 143, Reward: 0.5\n", + "Episode: 1, Step: 144, Reward: 0.5\n", + "Episode: 1, Step: 145, Reward: 0.5\n", + "Episode: 1, Step: 146, Reward: 0.5\n", + "Episode: 1, Step: 147, Reward: 0.5\n", + "Episode: 1, Step: 148, Reward: 0.5\n", + "Episode: 1, Step: 149, Reward: 0.5\n", + "Episode: 1, Step: 150, Reward: 0.5\n", + "Episode: 1, Step: 151, Reward: 0.5\n", + "Episode: 1, Step: 152, Reward: 0.5\n", + "Episode: 1, Step: 153, Reward: 0.5\n", + "Episode: 1, Step: 154, Reward: 0.5\n", + "Episode: 1, Step: 155, Reward: 0.5\n", + "Episode: 1, Step: 156, Reward: 0.5\n", + "Episode: 1, Step: 157, Reward: 0.5\n", + "Episode: 1, Step: 158, Reward: 0.5\n", + "Episode: 1, Step: 159, Reward: 0.5\n", + "Episode: 1, Step: 160, Reward: 0.5\n", + "Episode: 1, Step: 161, Reward: 0.5\n", + "Episode: 1, Step: 162, Reward: 0.5\n", + "Episode: 1, Step: 163, Reward: 0.5\n", + "Episode: 1, Step: 164, Reward: 0.5\n", + "Episode: 1, Step: 165, Reward: 0.5\n", + "Episode: 1, Step: 166, Reward: 0.5\n", + "Episode: 1, Step: 167, Reward: 0.5\n", + "Episode: 1, Step: 168, Reward: 0.5\n", + "Episode: 1, Step: 169, Reward: 0.5\n", + "Episode: 1, Step: 170, Reward: 0.5\n", + "Episode: 1, Step: 171, Reward: 0.5\n", + "Episode: 1, Step: 172, Reward: 0.5\n", + "Episode: 1, Step: 173, Reward: 0.5\n", + "Episode: 1, Step: 174, Reward: 0.5\n", + "Episode: 1, Step: 175, Reward: 0.5\n", + "Episode: 1, Step: 176, Reward: 0.5\n", + "Episode: 1, Step: 177, Reward: 0.5\n", + "Episode: 1, Step: 178, Reward: 0.5\n", + "Episode: 1, Step: 179, Reward: 0.5\n", + "Episode: 1, Step: 180, Reward: 0.5\n", + "Episode: 1, Step: 181, Reward: 0.5\n", + "Episode: 1, Step: 182, Reward: 0.5\n", + "Episode: 1, Step: 183, Reward: 0.5\n", + "Episode: 1, Step: 184, Reward: 0.5\n", + "Episode: 1, Step: 185, Reward: 0.5\n", + "Episode: 1, Step: 186, Reward: 0.5\n", + "Episode: 1, Step: 187, Reward: 0.5\n", + "Episode: 1, Step: 188, Reward: 0.5\n", + "Episode: 1, Step: 189, Reward: 0.5\n", + "Episode: 1, Step: 190, Reward: 0.5\n", + "Episode: 1, Step: 191, Reward: 0.5\n", + "Episode: 1, Step: 192, Reward: 0.5\n", + "Episode: 1, Step: 193, Reward: 0.5\n", + "Episode: 1, Step: 194, Reward: 0.5\n", + "Episode: 1, Step: 195, Reward: 0.5\n", + "Episode: 1, Step: 196, Reward: 0.5\n", + "Episode: 1, Step: 197, Reward: 0.5\n", + "Episode: 1, Step: 198, Reward: 0.5\n", + "Episode: 1, Step: 199, Reward: 0.5\n", + "Episode: 1, Step: 200, Reward: 0.5\n", + "Episode: 1, Step: 201, Reward: 0.5\n", + "Episode: 1, Step: 202, Reward: 0.5\n", + "Episode: 1, Step: 203, Reward: 0.5\n", + "Episode: 1, Step: 204, Reward: 0.5\n", + "Episode: 1, Step: 205, Reward: 0.5\n", + "Episode: 1, Step: 206, Reward: 0.5\n", + "Episode: 1, Step: 207, Reward: 0.5\n", + "Episode: 1, Step: 208, Reward: 0.5\n", + "Episode: 1, Step: 209, Reward: 0.5\n", + "Episode: 1, Step: 210, Reward: 0.5\n", + "Episode: 1, Step: 211, Reward: 0.5\n", + "Episode: 1, Step: 212, Reward: 0.5\n", + "Episode: 1, Step: 213, Reward: 0.5\n", + "Episode: 1, Step: 214, Reward: 0.5\n", + "Episode: 1, Step: 215, Reward: 0.5\n", + "Episode: 1, Step: 216, Reward: 0.5\n", + "Episode: 1, Step: 217, Reward: 0.5\n", + "Episode: 1, Step: 218, Reward: 0.5\n", + "Episode: 1, Step: 219, Reward: 0.5\n", + "Episode: 1, Step: 220, Reward: 0.5\n", + "Episode: 1, Step: 221, Reward: 0.5\n", + "Episode: 1, Step: 222, Reward: 0.5\n", + "Episode: 1, Step: 223, Reward: 0.5\n", + "Episode: 1, Step: 224, Reward: 0.5\n", + "Episode: 1, Step: 225, Reward: 0.5\n", + "Episode: 1, Step: 226, Reward: 0.5\n", + "Episode: 1, Step: 227, Reward: 0.5\n", + "Episode: 1, Step: 228, Reward: 0.5\n", + "Episode: 1, Step: 229, Reward: 0.5\n", + "Episode: 1, Step: 230, Reward: 0.5\n", + "Episode: 1, Step: 231, Reward: 0.5\n", + "Episode: 1, Step: 232, Reward: 0.5\n", + "Episode: 1, Step: 233, Reward: 0.5\n", + "Episode: 1, Step: 234, Reward: 0.5\n", + "Episode: 1, Step: 235, Reward: 0.5\n", + "Episode: 1, Step: 236, Reward: 0.5\n", + "Episode: 1, Step: 237, Reward: 0.5\n", + "Episode: 1, Step: 238, Reward: 0.5\n", + "Episode: 1, Step: 239, Reward: 0.5\n", + "Episode: 1, Step: 240, Reward: 0.5\n", + "Episode: 1, Step: 241, Reward: 0.5\n", + "Episode: 1, Step: 242, Reward: 0.5\n", + "Episode: 1, Step: 243, Reward: 0.5\n", + "Episode: 1, Step: 244, Reward: 0.5\n", + "Episode: 1, Step: 245, Reward: 0.5\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-12-01 14:53:21,247::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,248::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,249::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,251::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,252::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,254::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,256::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,259::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,262::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,292::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,293::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:21,294::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Episode: 1, Step: 246, Reward: 0.5\n", + "Episode: 1, Step: 247, Reward: 0.5\n", + "Episode: 1, Step: 248, Reward: 0.5\n", + "Episode: 1, Step: 249, Reward: 0.5\n", + "Episode: 1, Step: 250, Reward: 0.5\n", + "Episode: 1, Step: 251, Reward: 0.5\n", + "Episode: 1, Step: 252, Reward: 0.5\n", + "Episode: 1, Step: 253, Reward: 0.5\n", + "Episode: 1, Step: 254, Reward: 0.5\n", + "Episode: 1, Step: 255, Reward: 0.5\n", + "Episode: 1, Step: 256, Reward: 0.5\n", + "Episode: 2, Step: 1, Reward: 0.5\n", + "Episode: 2, Step: 2, Reward: 0.5\n", + "Episode: 2, Step: 3, Reward: 0.5\n", + "Episode: 2, Step: 4, Reward: 0.5\n", + "Episode: 2, Step: 5, Reward: 0.5\n", + "Episode: 2, Step: 6, Reward: 0.5\n", + "Episode: 2, Step: 7, Reward: 0.5\n", + "Episode: 2, Step: 8, Reward: 0.5\n", + "Episode: 2, Step: 9, Reward: 0.5\n", + "Episode: 2, Step: 10, Reward: 0.5\n", + "Episode: 2, Step: 11, Reward: 0.5\n", + "Episode: 2, Step: 12, Reward: 0.5\n", + "Episode: 2, Step: 13, Reward: 0.5\n", + "Episode: 2, Step: 14, Reward: 0.5\n", + "Episode: 2, Step: 15, Reward: 0.5\n", + "Episode: 2, Step: 16, Reward: 0.5\n", + "Episode: 2, Step: 17, Reward: 0.5\n", + "Episode: 2, Step: 18, Reward: 0.5\n", + "Episode: 2, Step: 19, Reward: 0.5\n", + "Episode: 2, Step: 20, Reward: 0.5\n", + "Episode: 2, Step: 21, Reward: 0.5\n", + "Episode: 2, Step: 22, Reward: 0.5\n", + "Episode: 2, Step: 23, Reward: 0.5\n", + "Episode: 2, Step: 24, Reward: 0.5\n", + "Episode: 2, Step: 25, Reward: 0.5\n", + "Episode: 2, Step: 26, Reward: 0.5\n", + "Episode: 2, Step: 27, Reward: 0.5\n", + "Episode: 2, Step: 28, Reward: 0.5\n", + "Episode: 2, Step: 29, Reward: 0.5\n", + "Episode: 2, Step: 30, Reward: 0.5\n", + "Episode: 2, Step: 31, Reward: 0.5\n", + "Episode: 2, Step: 32, Reward: 0.5\n", + "Episode: 2, Step: 33, Reward: 0.5\n", + "Episode: 2, Step: 34, Reward: 0.5\n", + "Episode: 2, Step: 35, Reward: 0.5\n", + "Episode: 2, Step: 36, Reward: 0.5\n", + "Episode: 2, Step: 37, Reward: 0.5\n", + "Episode: 2, Step: 38, Reward: 0.5\n", + "Episode: 2, Step: 39, Reward: 0.5\n", + "Episode: 2, Step: 40, Reward: 0.5\n", + "Episode: 2, Step: 41, Reward: 0.5\n", + "Episode: 2, Step: 42, Reward: 0.5\n", + "Episode: 2, Step: 43, Reward: 0.5\n", + "Episode: 2, Step: 44, Reward: 0.5\n", + "Episode: 2, Step: 45, Reward: 0.5\n", + "Episode: 2, Step: 46, Reward: 0.5\n", + "Episode: 2, Step: 47, Reward: 0.5\n", + "Episode: 2, Step: 48, Reward: 0.5\n", + "Episode: 2, Step: 49, Reward: 0.5\n", + "Episode: 2, Step: 50, Reward: 0.5\n", + "Episode: 2, Step: 51, Reward: 0.5\n", + "Episode: 2, Step: 52, Reward: 0.5\n", + "Episode: 2, Step: 53, Reward: 0.5\n", + "Episode: 2, Step: 54, Reward: 0.5\n", + "Episode: 2, Step: 55, Reward: 0.5\n", + "Episode: 2, Step: 56, Reward: 0.5\n", + "Episode: 2, Step: 57, Reward: 0.5\n", + "Episode: 2, Step: 58, Reward: 0.5\n", + "Episode: 2, Step: 59, Reward: 0.5\n", + "Episode: 2, Step: 60, Reward: 0.5\n", + "Episode: 2, Step: 61, Reward: 0.5\n", + "Episode: 2, Step: 62, Reward: 0.5\n", + "Episode: 2, Step: 63, Reward: 0.5\n", + "Episode: 2, Step: 64, Reward: 0.5\n", + "Episode: 2, Step: 65, Reward: 0.5\n", + "Episode: 2, Step: 66, Reward: 0.5\n", + "Episode: 2, Step: 67, Reward: 0.5\n", + "Episode: 2, Step: 68, Reward: 0.5\n", + "Episode: 2, Step: 69, Reward: 0.5\n", + "Episode: 2, Step: 70, Reward: 0.5\n", + "Episode: 2, Step: 71, Reward: 0.5\n", + "Episode: 2, Step: 72, Reward: 0.5\n", + "Episode: 2, Step: 73, Reward: 0.5\n", + "Episode: 2, Step: 74, Reward: 0.5\n", + "Episode: 2, Step: 75, Reward: 0.5\n", + "Episode: 2, Step: 76, Reward: 0.5\n", + "Episode: 2, Step: 77, Reward: 0.5\n", + "Episode: 2, Step: 78, Reward: 0.5\n", + "Episode: 2, Step: 79, Reward: 0.5\n", + "Episode: 2, Step: 80, Reward: 0.5\n", + "Episode: 2, Step: 81, Reward: 0.5\n", + "Episode: 2, Step: 82, Reward: 0.5\n", + "Episode: 2, Step: 83, Reward: 0.5\n", + "Episode: 2, Step: 84, Reward: 0.5\n", + "Episode: 2, Step: 85, Reward: 0.5\n", + "Episode: 2, Step: 86, Reward: 0.5\n", + "Episode: 2, Step: 87, Reward: 0.5\n", + "Episode: 2, Step: 88, Reward: 0.5\n", + "Episode: 2, Step: 89, Reward: 0.5\n", + "Episode: 2, Step: 90, Reward: 0.5\n", + "Episode: 2, Step: 91, Reward: 0.5\n", + "Episode: 2, Step: 92, Reward: 0.5\n", + "Episode: 2, Step: 93, Reward: 0.5\n", + "Episode: 2, Step: 94, Reward: 0.5\n", + "Episode: 2, Step: 95, Reward: 0.5\n", + "Episode: 2, Step: 96, Reward: 0.5\n", + "Episode: 2, Step: 97, Reward: 0.5\n", + "Episode: 2, Step: 98, Reward: 0.5\n", + "Episode: 2, Step: 99, Reward: 0.5\n", + "Episode: 2, Step: 100, Reward: 0.5\n", + "Episode: 2, Step: 101, Reward: 0.5\n", + "Episode: 2, Step: 102, Reward: 0.5\n", + "Episode: 2, Step: 103, Reward: 0.5\n", + "Episode: 2, Step: 104, Reward: 0.5\n", + "Episode: 2, Step: 105, Reward: 0.5\n", + "Episode: 2, Step: 106, Reward: 0.5\n", + "Episode: 2, Step: 107, Reward: 0.5\n", + "Episode: 2, Step: 108, Reward: 0.5\n", + "Episode: 2, Step: 109, Reward: 0.5\n", + "Episode: 2, Step: 110, Reward: 0.5\n", + "Episode: 2, Step: 111, Reward: 0.5\n", + "Episode: 2, Step: 112, Reward: 0.5\n", + "Episode: 2, Step: 113, Reward: 0.5\n", + "Episode: 2, Step: 114, Reward: 0.5\n", + "Episode: 2, Step: 115, Reward: 0.5\n", + "Episode: 2, Step: 116, Reward: 0.5\n", + "Episode: 2, Step: 117, Reward: 0.5\n", + "Episode: 2, Step: 118, Reward: 0.5\n", + "Episode: 2, Step: 119, Reward: 0.5\n", + "Episode: 2, Step: 120, Reward: 0.5\n", + "Episode: 2, Step: 121, Reward: 0.5\n", + "Episode: 2, Step: 122, Reward: 0.5\n", + "Episode: 2, Step: 123, Reward: 0.5\n", + "Episode: 2, Step: 124, Reward: 0.5\n", + "Episode: 2, Step: 125, Reward: 0.5\n", + "Episode: 2, Step: 126, Reward: 0.5\n", + "Episode: 2, Step: 127, Reward: 0.5\n", + "Episode: 2, Step: 128, Reward: 0.5\n", + "Episode: 2, Step: 129, Reward: 0.5\n", + "Episode: 2, Step: 130, Reward: 0.5\n", + "Episode: 2, Step: 131, Reward: 0.5\n", + "Episode: 2, Step: 132, Reward: 0.5\n", + "Episode: 2, Step: 133, Reward: 0.5\n", + "Episode: 2, Step: 134, Reward: 0.5\n", + "Episode: 2, Step: 135, Reward: 0.5\n", + "Episode: 2, Step: 136, Reward: 0.5\n", + "Episode: 2, Step: 137, Reward: 0.5\n", + "Episode: 2, Step: 138, Reward: 0.5\n", + "Episode: 2, Step: 139, Reward: 0.5\n", + "Episode: 2, Step: 140, Reward: 0.5\n", + "Episode: 2, Step: 141, Reward: 0.5\n", + "Episode: 2, Step: 142, Reward: 0.5\n", + "Episode: 2, Step: 143, Reward: 0.5\n", + "Episode: 2, Step: 144, Reward: 0.5\n", + "Episode: 2, Step: 145, Reward: 0.5\n", + "Episode: 2, Step: 146, Reward: 0.5\n", + "Episode: 2, Step: 147, Reward: 0.5\n", + "Episode: 2, Step: 148, Reward: 0.5\n", + "Episode: 2, Step: 149, Reward: 0.5\n", + "Episode: 2, Step: 150, Reward: 0.5\n", + "Episode: 2, Step: 151, Reward: 0.5\n", + "Episode: 2, Step: 152, Reward: 0.5\n", + "Episode: 2, Step: 153, Reward: 0.5\n", + "Episode: 2, Step: 154, Reward: 0.5\n", + "Episode: 2, Step: 155, Reward: 0.5\n", + "Episode: 2, Step: 156, Reward: 0.5\n", + "Episode: 2, Step: 157, Reward: 0.5\n", + "Episode: 2, Step: 158, Reward: 0.5\n", + "Episode: 2, Step: 159, Reward: 0.5\n", + "Episode: 2, Step: 160, Reward: 0.5\n", + "Episode: 2, Step: 161, Reward: 0.5\n", + "Episode: 2, Step: 162, Reward: 0.5\n", + "Episode: 2, Step: 163, Reward: 0.5\n", + "Episode: 2, Step: 164, Reward: 0.5\n", + "Episode: 2, Step: 165, Reward: 0.5\n", + "Episode: 2, Step: 166, Reward: 0.5\n", + "Episode: 2, Step: 167, Reward: 0.5\n", + "Episode: 2, Step: 168, Reward: 0.5\n", + "Episode: 2, Step: 169, Reward: 0.5\n", + "Episode: 2, Step: 170, Reward: 0.5\n", + "Episode: 2, Step: 171, Reward: 0.5\n", + "Episode: 2, Step: 172, Reward: 0.5\n", + "Episode: 2, Step: 173, Reward: 0.5\n", + "Episode: 2, Step: 174, Reward: 0.5\n", + "Episode: 2, Step: 175, Reward: 0.5\n", + "Episode: 2, Step: 176, Reward: 0.5\n", + "Episode: 2, Step: 177, Reward: 0.5\n", + "Episode: 2, Step: 178, Reward: 0.5\n", + "Episode: 2, Step: 179, Reward: 0.5\n", + "Episode: 2, Step: 180, Reward: 0.5\n", + "Episode: 2, Step: 181, Reward: 0.5\n", + "Episode: 2, Step: 182, Reward: 0.5\n", + "Episode: 2, Step: 183, Reward: 0.5\n", + "Episode: 2, Step: 184, Reward: 0.5\n", + "Episode: 2, Step: 185, Reward: 0.5\n", + "Episode: 2, Step: 186, Reward: 0.5\n", + "Episode: 2, Step: 187, Reward: 0.5\n", + "Episode: 2, Step: 188, Reward: 0.5\n", + "Episode: 2, Step: 189, Reward: 0.5\n", + "Episode: 2, Step: 190, Reward: 0.5\n", + "Episode: 2, Step: 191, Reward: 0.5\n", + "Episode: 2, Step: 192, Reward: 0.5\n", + "Episode: 2, Step: 193, Reward: 0.5\n", + "Episode: 2, Step: 194, Reward: 0.5\n", + "Episode: 2, Step: 195, Reward: 0.5\n", + "Episode: 2, Step: 196, Reward: 0.5\n", + "Episode: 2, Step: 197, Reward: 0.5\n", + "Episode: 2, Step: 198, Reward: 0.5\n", + "Episode: 2, Step: 199, Reward: 0.5\n", + "Episode: 2, Step: 200, Reward: 0.5\n", + "Episode: 2, Step: 201, Reward: 0.5\n", + "Episode: 2, Step: 202, Reward: 0.5\n", + "Episode: 2, Step: 203, Reward: 0.5\n", + "Episode: 2, Step: 204, Reward: 0.5\n", + "Episode: 2, Step: 205, Reward: 0.5\n", + "Episode: 2, Step: 206, Reward: 0.5\n", + "Episode: 2, Step: 207, Reward: 0.5\n", + "Episode: 2, Step: 208, Reward: 0.5\n", + "Episode: 2, Step: 209, Reward: 0.5\n", + "Episode: 2, Step: 210, Reward: 0.5\n", + "Episode: 2, Step: 211, Reward: 0.5\n", + "Episode: 2, Step: 212, Reward: 0.5\n", + "Episode: 2, Step: 213, Reward: 0.5\n", + "Episode: 2, Step: 214, Reward: 0.5\n", + "Episode: 2, Step: 215, Reward: 0.5\n", + "Episode: 2, Step: 216, Reward: 0.5\n", + "Episode: 2, Step: 217, Reward: 0.5\n", + "Episode: 2, Step: 218, Reward: 0.5\n", + "Episode: 2, Step: 219, Reward: 0.5\n", + "Episode: 2, Step: 220, Reward: 0.5\n", + "Episode: 2, Step: 221, Reward: 0.5\n", + "Episode: 2, Step: 222, Reward: 0.5\n", + "Episode: 2, Step: 223, Reward: 0.5\n", + "Episode: 2, Step: 224, Reward: 0.5\n", + "Episode: 2, Step: 225, Reward: 0.5\n", + "Episode: 2, Step: 226, Reward: 0.5\n", + "Episode: 2, Step: 227, Reward: 0.5\n", + "Episode: 2, Step: 228, Reward: 0.5\n", + "Episode: 2, Step: 229, Reward: 0.5\n", + "Episode: 2, Step: 230, Reward: 0.5\n", + "Episode: 2, Step: 231, Reward: 0.5\n", + "Episode: 2, Step: 232, Reward: 0.5\n", + "Episode: 2, Step: 233, Reward: 0.5\n", + "Episode: 2, Step: 234, Reward: 0.5\n", + "Episode: 2, Step: 235, Reward: 0.5\n", + "Episode: 2, Step: 236, Reward: 0.5\n", + "Episode: 2, Step: 237, Reward: 0.5\n", + "Episode: 2, Step: 238, Reward: 0.5\n", + "Episode: 2, Step: 239, Reward: 0.5\n", + "Episode: 2, Step: 240, Reward: 0.5\n", + "Episode: 2, Step: 241, Reward: 0.5\n", + "Episode: 2, Step: 242, Reward: 0.5\n", + "Episode: 2, Step: 243, Reward: 0.5\n", + "Episode: 2, Step: 244, Reward: 0.5\n", + "Episode: 2, Step: 245, Reward: 0.5\n", + "Episode: 2, Step: 246, Reward: 0.5\n", + "Episode: 2, Step: 247, Reward: 0.5\n", + "Episode: 2, Step: 248, Reward: 0.5\n", + "Episode: 2, Step: 249, Reward: 0.5\n", + "Episode: 2, Step: 250, Reward: 0.5\n", + "Episode: 2, Step: 251, Reward: 0.5\n", + "Episode: 2, Step: 252, Reward: 0.5\n", + "Episode: 2, Step: 253, Reward: 0.5\n", + "Episode: 2, Step: 254, Reward: 0.5\n", + "Episode: 2, Step: 255, Reward: 0.5\n", + "Episode: 2, Step: 256, Reward: 0.5\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-12-01 14:53:24,371::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,373::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,375::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,375::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,376::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,377::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,379::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,380::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,381::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,402::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,404::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", + "2023-12-01 14:53:24,406::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Episode: 3, Step: 1, Reward: 0.5\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-12-01 14:53:24,878\tINFO storage.py:563 -- Checkpoint successfully created at: Checkpoint(filesystem=local, path=/home/cade/ray_results/PPO_2023-12-01_14-53-17/PPO_PrimaiteRayEnv_5cbc4_00000_0_2023-12-01_14-53-17/checkpoint_000000)\n", + "2023-12-01 14:53:25,098\tINFO tune.py:1047 -- Total run time: 7.37 seconds (7.31 seconds for the tuning loop).\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "ResultGrid<[\n", + " Result(\n", + " metrics={'custom_metrics': {}, 'episode_media': {}, 'info': {'learner': {'__all__': {'num_agent_steps_trained': 128.0, 'num_env_steps_trained': 128.0, 'total_loss': 9.403312460581462}, 'default_policy': {'total_loss': 9.403312460581462, 'policy_loss': -0.06894568807135025, 'vf_loss': 9.469796816507975, 'vf_loss_unclipped': 416.65203653971355, 'vf_explained_var': 0.0007335106531778971, 'entropy': 3.864323592185974, 'mean_kl_loss': 0.012305201259247648, 'default_optimizer_lr': 4.999999999999999e-05, 'curr_lr': 5e-05, 'curr_entropy_coeff': 0.0, 'curr_kl_coeff': 0.20000000298023224}}, 'num_env_steps_sampled': 512, 'num_env_steps_trained': 0, 'num_agent_steps_sampled': 512, 'num_agent_steps_trained': 0}, 'sampler_results': {'episode_reward_max': 128.0, 'episode_reward_min': 128.0, 'episode_reward_mean': 128.0, 'episode_len_mean': 256.0, 'episode_media': {}, 'episodes_this_iter': 1, 'policy_reward_min': {}, 'policy_reward_max': {}, 'policy_reward_mean': {}, 'custom_metrics': {}, 'hist_stats': {'episode_reward': [128.0, 128.0], 'episode_lengths': [256, 256]}, 'sampler_perf': {'mean_raw_obs_processing_ms': 0.8607522543689299, 'mean_inference_ms': 2.1271821797748984, 'mean_action_processing_ms': 0.15329866429338604, 'mean_env_wait_ms': 6.184263571370873, 'mean_env_render_ms': 0.0}, 'num_faulty_episodes': 0, 'connector_metrics': {'ObsPreprocessorConnector_ms': 0.010561943054199219, 'StateBufferConnector_ms': 0.004971027374267578, 'ViewRequirementAgentConnector_ms': 0.29495954513549805}}, 'episode_reward_max': 128.0, 'episode_reward_min': 128.0, 'episode_reward_mean': 128.0, 'episode_len_mean': 256.0, 'episodes_this_iter': 1, 'policy_reward_min': {}, 'policy_reward_max': {}, 'policy_reward_mean': {}, 'hist_stats': {'episode_reward': [128.0, 128.0], 'episode_lengths': [256, 256]}, 'sampler_perf': {'mean_raw_obs_processing_ms': 0.8607522543689299, 'mean_inference_ms': 2.1271821797748984, 'mean_action_processing_ms': 0.15329866429338604, 'mean_env_wait_ms': 6.184263571370873, 'mean_env_render_ms': 0.0}, 'num_faulty_episodes': 0, 'connector_metrics': {'ObsPreprocessorConnector_ms': 0.010561943054199219, 'StateBufferConnector_ms': 0.004971027374267578, 'ViewRequirementAgentConnector_ms': 0.29495954513549805}, 'num_healthy_workers': 0, 'num_in_flight_async_reqs': 0, 'num_remote_worker_restarts': 0, 'num_agent_steps_sampled': 512, 'num_agent_steps_trained': 0, 'num_env_steps_sampled': 512, 'num_env_steps_trained': 0, 'num_env_steps_sampled_this_iter': 128, 'num_env_steps_trained_this_iter': 0, 'num_env_steps_sampled_throughput_per_sec': 85.63165451744611, 'num_env_steps_trained_throughput_per_sec': 0.0, 'num_steps_trained_this_iter': 0, 'agent_timesteps_total': 512, 'timers': {'training_iteration_time_ms': 1530.574, 'sample_time_ms': 1196.582, 'synch_weights_time_ms': 1.912}, 'counters': {'num_env_steps_sampled': 512, 'num_env_steps_trained': 0, 'num_agent_steps_sampled': 512, 'num_agent_steps_trained': 0}, 'perf': {'cpu_util_percent': 55.25, 'ram_util_percent': 58.8}},\n", + " path='/home/cade/ray_results/PPO_2023-12-01_14-53-17/PPO_PrimaiteRayEnv_5cbc4_00000_0_2023-12-01_14-53-17',\n", + " filesystem='local',\n", + " checkpoint=Checkpoint(filesystem=local, path=/home/cade/ray_results/PPO_2023-12-01_14-53-17/PPO_PrimaiteRayEnv_5cbc4_00000_0_2023-12-01_14-53-17/checkpoint_000000)\n", + " )\n", + "]>" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "gym = PrimaiteRayEnv({\"game\":game})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import ray\n", - "from ray.rllib.algorithms import ppo" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ray.shutdown()\n", - "ray.init()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "env_config = {\"game\":game}\n", - "config = {\n", - " \"env\" : PrimaiteRayEnv,\n", - " \"env_config\" : env_config,\n", - " \"disable_env_checking\": True,\n", - " \"num_rollout_workers\": 0,\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "algo = ppo.PPO(config=config)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for i in range(5):\n", - " result = algo.train()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "algo.save(\"temp/deleteme\")" + "tune.Tuner(\n", + " \"PPO\",\n", + " run_config=air.RunConfig(\n", + " stop={\"timesteps_total\": 512}\n", + " ),\n", + " param_space=config\n", + ").fit()\n" ] } ], diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index a5fdade9..87cf4f2d 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -69,14 +69,15 @@ class PrimaiteGymEnv(gymnasium.Env): class PrimaiteRayEnv(gymnasium.Env): """Ray wrapper that accepts a single `env_config` parameter in init function for compatibility with Ray.""" - def __init__(self, env_config: Dict[str, PrimaiteGame]) -> None: + 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] """ - self.env = PrimaiteGymEnv(game=env_config["game"]) + self.env = PrimaiteGymEnv(game=PrimaiteGame.from_config(env_config["cfg"])) + self.env.game.episode_counter -= 1 self.action_space = self.env.action_space self.observation_space = self.env.observation_space @@ -92,14 +93,14 @@ class PrimaiteRayEnv(gymnasium.Env): class PrimaiteRayMARLEnv(MultiAgentEnv): """Ray Environment that inherits from MultiAgentEnv to allow training MARL systems.""" - def __init__(self, env_config: Optional[Dict] = None) -> None: + 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] """ - self.game: PrimaiteGame = env_config["game"] + self.game: PrimaiteGame = PrimaiteGame.from_config(env_config["cfg"]) """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!""" @@ -108,7 +109,10 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): self.terminateds = set() self.truncateds = set() self.observation_space = gymnasium.spaces.Dict( - {name: agent.observation_manager.space for name, agent in self.agents.items()} + { + name: gymnasium.spaces.flatten_space(agent.observation_manager.space) + for name, agent in self.agents.items() + } ) self.action_space = gymnasium.spaces.Dict( {name: agent.action_manager.space for name, agent in self.agents.items()} @@ -159,4 +163,9 @@ class PrimaiteRayMARLEnv(MultiAgentEnv): def _get_obs(self) -> Dict[str, ObsType]: """Return the current observation.""" - return {name: agent.observation_manager.current_observation for name, agent in self.agents.items()} + obs = {} + for name, agent in self.agents.items(): + unflat_space = agent.observation_manager.space + unflat_obs = agent.observation_manager.current_observation + obs[name] = gymnasium.spaces.flatten(unflat_space, unflat_obs) + return obs diff --git a/src/primaite/session/policy/rllib.py b/src/primaite/session/policy/rllib.py index be181797..ca69a2a8 100644 --- a/src/primaite/session/policy/rllib.py +++ b/src/primaite/session/policy/rllib.py @@ -12,6 +12,10 @@ from ray import air, tune from ray.rllib.algorithms import ppo from ray.rllib.algorithms.ppo import PPOConfig +from primaite import getLogger + +_LOGGER = getLogger(__name__) + class RaySingleAgentPolicy(PolicyABC, identifier="RLLIB_single_agent"): """Single agent RL policy using Ray RLLib.""" @@ -19,7 +23,7 @@ class RaySingleAgentPolicy(PolicyABC, identifier="RLLIB_single_agent"): def __init__(self, session: "PrimaiteSession", algorithm: Literal["PPO", "A2C"], seed: Optional[int] = None): super().__init__(session=session) - config = { + self.config = { "env": PrimaiteRayEnv, "env_config": {"game": session.game}, "disable_env_checking": True, @@ -29,12 +33,13 @@ class RaySingleAgentPolicy(PolicyABC, identifier="RLLIB_single_agent"): ray.shutdown() ray.init() - self._algo = ppo.PPO(config=config) - def learn(self, n_episodes: int, timesteps_per_episode: int) -> None: """Train the agent.""" - for ep in range(n_episodes): - self._algo.train() + self.config["training_iterations"] = n_episodes * timesteps_per_episode + self.config["train_batch_size"] = 128 + self._algo = ppo.PPO(config=self.config) + _LOGGER.info("Starting RLLIB training session") + self._algo.train() def eval(self, n_episodes: int, deterministic: bool) -> None: """Evaluate the agent.""" From 294f57c292dfbbdacc95b91f927c9fa9a99a0862 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 15:08:42 +0000 Subject: [PATCH 084/107] #2084: find paths with wildcard --- .azure/azure-ci-build-pipeline.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 147b8c14..4724cc87 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,7 +98,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:coverage.xml --cov-report html + pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:coverage.xml --cov-report html --cov-report term --cov-fail-under=80 displayName: 'Run tests and code coverage' - task: PublishTestResults@2 @@ -112,8 +112,8 @@ stages: displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura - summaryFileLocation: $(System.DefaultWorkingDirectory)/coverage.xml - pathToSources: $(System.DefaultWorkingDirectory)/ - reportDirectory: $(System.DefaultWorkingDirectory)/htmlcov - additionalCodeCoverageFiles: $(System.DefaultWorkingDirectory)/htmlcov/*.* + summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' + pathToSources: '$(System.DefaultWorkingDirectory)/src' + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' + additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/**/htmlcov/*.*' failIfCoverageEmpty: true From 6598c66da159ea446f02dc8a5a24830b43b6ba8a Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 15:18:38 +0000 Subject: [PATCH 085/107] #2084: i hope no one is keeping an eye on these atrocious commit messages --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 4724cc87..00b977db 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,7 +98,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:coverage.xml --cov-report html --cov-report term --cov-fail-under=80 + pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term --cov-fail-under=80 displayName: 'Run tests and code coverage' - task: PublishTestResults@2 From f0327da9b6d4e523346546c5871d42547099e07a Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 15:33:43 +0000 Subject: [PATCH 086/107] #2084: remove 80% requirement - causes tests to fail --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 00b977db..4c5afed8 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,7 +98,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term --cov-fail-under=80 + pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term displayName: 'Run tests and code coverage' - task: PublishTestResults@2 From eeedea2eff30eb458763c503a45b9827bcc2c928 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Dec 2023 15:36:07 +0000 Subject: [PATCH 087/107] Make more friendly user outputs when training SB3 --- .../config/_package_data/example_config.yaml | 4 +- src/primaite/game/agent/rewards.py | 1 + src/primaite/game/game.py | 13 +- .../training_example_ray_multi_agent.ipynb | 83 +- .../training_example_ray_single_agent.ipynb | 909 +----------------- src/primaite/session/environment.py | 5 +- src/primaite/session/session.py | 1 + src/primaite/simulator/core.py | 2 +- src/primaite/simulator/network/container.py | 2 +- .../simulator/network/hardware/base.py | 4 +- 10 files changed, 96 insertions(+), 928 deletions(-) diff --git a/src/primaite/config/_package_data/example_config.yaml b/src/primaite/config/_package_data/example_config.yaml index b68861e1..7d5b50d6 100644 --- a/src/primaite/config/_package_data/example_config.yaml +++ b/src/primaite/config/_package_data/example_config.yaml @@ -655,8 +655,8 @@ 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 services: diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index 3466114c..71945a24 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -239,6 +239,7 @@ class RewardFunction: self.reward_components: List[Tuple[AbstractReward, float]] = [] "attribute reward_components keeps track of reward components and the weights assigned to each." self.current_reward: float + self.total_reward: float = 0.0 def regsiter_component(self, component: AbstractReward, weight: float = 1.0) -> None: """Add a reward component to the reward function. diff --git a/src/primaite/game/game.py b/src/primaite/game/game.py index 38e9d5fc..a36cbea9 100644 --- a/src/primaite/game/game.py +++ b/src/primaite/game/game.py @@ -125,6 +125,7 @@ class PrimaiteGame: for agent in self.agents: agent.update_observation(state) 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.""" @@ -155,6 +156,8 @@ class PrimaiteGame: 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 def close(self) -> None: """Close the game, this will close the simulation.""" @@ -240,7 +243,7 @@ class PrimaiteGame: position=r_num, ) else: - print("invalid node type") + _LOGGER.warning(f"invalid node type {n_type} in config") if "services" in node_cfg: for service_cfg in node_cfg["services"]: new_service = None @@ -256,12 +259,12 @@ class PrimaiteGame: "FTPServer": FTPServer, } if service_type in service_types_mapping: - print(f"installing {service_type} on node {new_node.hostname}") + _LOGGER.debug(f"installing {service_type} on node {new_node.hostname}") new_node.software_manager.install(service_types_mapping[service_type]) new_service = new_node.software_manager.software[service_type] game.ref_map_services[service_ref] = new_service.uuid else: - print(f"service type not found {service_type}") + _LOGGER.warning(f"service type not found {service_type}") # service-dependent options if service_type == "DatabaseClient": if "options" in service_cfg: @@ -295,7 +298,7 @@ class PrimaiteGame: new_application = new_node.software_manager.software[application_type] game.ref_map_applications[application_ref] = new_application.uuid else: - print(f"application type not found {application_type}") + _LOGGER.warning(f"application type not found {application_type}") if application_type == "DataManipulationBot": if "options" in application_cfg: @@ -416,7 +419,7 @@ class PrimaiteGame: ) game.agents.append(new_agent) else: - print("agent type not found") + _LOGGER.warning(f"agent type {agent_type} not found") game.simulation.set_original_state() diff --git a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb index 3d5d7ba6..cd9ecfe7 100644 --- a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb @@ -1,5 +1,21 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train a Multi agent system using RLLIB\n", + "\n", + "This notebook will demonstrate how to use the `PrimaiteRayMARLEnv` to train a very basic system with two PPO agents." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### First, Import packages and read our config file." + ] + }, { "cell_type": "code", "execution_count": null, @@ -8,30 +24,28 @@ "source": [ "from primaite.game.game import PrimaiteGame\n", "import yaml\n", - "from primaite.config.load import example_config_path\n", "\n", - "from primaite.session.environment import PrimaiteRayEnv" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open('/home/cade/repos/PrimAITE/src/primaite/config/_package_data/example_config_2_rl_agents.yaml', 'r') as f:\n", - " cfg = yaml.safe_load(f)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + "from primaite.session.environment import PrimaiteRayEnv\n", + "from primaite import PRIMAITE_PATHS\n", + "\n", "import ray\n", "from ray import air, tune\n", - "from ray.rllib.algorithms.ppo import PPOConfig" + "from ray.rllib.algorithms.ppo import PPOConfig\n", + "from primaite.session.environment import PrimaiteRayMARLEnv\n", + "\n", + "# If you get an error saying this config file doesn't exist, you may need to run `primaite setup` in your command line\n", + "# to copy the files to your user data path.\n", + "with open(PRIMAITE_PATHS.user_config_path / 'example_config/example_config_2_rl_agents.yaml', 'r') as f:\n", + " cfg = yaml.safe_load(f)\n", + "\n", + "ray.init(local_mode=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Create a Ray algorithm config which accepts our two agents" ] }, { @@ -40,13 +54,10 @@ "metadata": {}, "outputs": [], "source": [ - "from primaite.session.environment import PrimaiteRayMARLEnv\n", - "\n", - "\n", "config = (\n", " PPOConfig()\n", " .multi_agent(\n", - " policies={'defender_1','defender_2'},\n", + " 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", @@ -55,6 +66,14 @@ " )\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set training parameters and start the training\n", + "This example will save outputs to a default Ray directory and use mostly default settings." + ] + }, { "cell_type": "code", "execution_count": null, @@ -69,20 +88,6 @@ " param_space=config\n", ").fit()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "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 ebd35d61..a89b29e4 100644 --- a/src/primaite/notebooks/training_example_ray_single_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_single_agent.ipynb @@ -1,18 +1,18 @@ { "cells": [ { - "cell_type": "code", - "execution_count": 1, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-12-01 14:53:13,421\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" - ] - } - ], + "source": [ + "## Train a Single agent system using RLLib\n", + "This notebook will demonstrate how to use PrimaiteRayEnv to train a basic PPO agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from primaite.game.game import PrimaiteGame\n", "import yaml\n", @@ -22,86 +22,26 @@ "from ray.rllib.algorithms import ppo\n", "from ray import air, tune\n", "import ray\n", - "from ray.rllib.algorithms.ppo import PPOConfig" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ + "from ray.rllib.algorithms.ppo import PPOConfig\n", + "\n", + "# If you get an error saying this config file doesn't exist, you may need to run `primaite setup` in your command line\n", + "# to copy the files to your user data path.\n", "with open(example_config_path(), 'r') as f:\n", - " cfg = yaml.safe_load(f)\n" + " cfg = yaml.safe_load(f)\n", + "\n", + "ray.init(local_mode=True)\n" ] }, { - "cell_type": "code", - "execution_count": 3, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-12-01 14:53:16,276\tINFO worker.py:1673 -- Started a local Ray instance.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9a775bf48837443dbdc6a3da9e9831f5", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "
\n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Python version:3.10.12
Ray version:2.8.0
\n", - "\n", - "
\n", - "
\n" - ], - "text/plain": [ - "RayContext(dashboard_url='', python_version='3.10.12', ray_version='2.8.0', ray_commit='105355bd253d6538ed34d331f6a4bdf0e38ace3a', protocol_version=None)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "ray.init(local_mode=True)" + "#### Create a Ray algorithm and pass it our config." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -110,808 +50,23 @@ "config = (\n", " PPOConfig()\n", " .environment(env=PrimaiteRayEnv, env_config=env_config, disable_env_checking=True)\n", - " .rollouts(num_rollout_workers=0,)\n", + " .rollouts(num_rollout_workers=0)\n", " .training(train_batch_size=128)\n", ")\n" ] }, { - "cell_type": "code", - "execution_count": 5, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - ":job_id:01000000\n", - ":task_name:bundle_reservation_check_func\n", - ":actor_name:PPO\n", - "2023-12-01 14:53:17,868::ERROR::primaite.simulator.network.hardware.base::190::NIC 3e:e9:64:e8:cf:89/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,869::ERROR::primaite.simulator.network.hardware.base::190::NIC 74:17:08:49:f5:30/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,870::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,871::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,872::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,875: Added node 1c94cb0c-b62c-43eb-b5d3-4a5d1937f845 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,878: Added node 3197ef0c-0ce8-4b63-bde8-91e7f95d59ef to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,884: Added node 835b8e76-0b1e-4112-9897-0808a87fd9de to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,888::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,889: Added service 6a19bda9-7f0e-4f77-a5bc-b473d3418df0 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,890::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,891: Added service 6fc138ff-e698-4c4d-82ca-0aa990df6669 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,893: Added application 601f573a-5480-492d-8508-4b1ccc45100f to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,895::ERROR::primaite.simulator.network.hardware.base::190::NIC fd:19:e4:d5:6e:c8/192.168.1.10 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,896::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,898: Added service 901a736f-8fd0-49e9-9369-ef1da8a6a5a4 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,900::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,903: Added service e240939c-b017-4e67-b161-8e5d96cbe061 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,905: Added application dada880f-ec1f-4ed3-a4b6-3f8c68a6c750 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,906::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,908: Added service d2201c7b-418c-49f7-bed0-c93f520f0352 to node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae\n", - "2023-12-01 14:53:17,909: Added node 7a31b3ca-b51d-4332-b9fb-ba5194ac5bae to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,912::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,914: Added service 2c843f1e-643a-40d3-9477-8870926f49e8 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,916::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,918: Added service b52910ee-66d7-4006-8044-fb27a95cc00f to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,920: Added application dd11b7fb-a31a-418d-bfdb-fb862c0c38b2 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,922::ERROR::primaite.simulator.network.hardware.base::190::NIC b9:01:34:d2:50:53/192.168.1.12 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,923::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,926: Added service a1d7a58a-df23-4ea0-934f-b4f397f252e5 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,927::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,928: Added service 742f59a4-369e-4a31-a95e-19adb9115cb7 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,930: Added application 5a93e02b-5485-46ad-9c61-4f32d4673ec3 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,934: Added application 040a27ae-8c65-47fd-a60b-7c72a46e7806 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,936::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,938: Added service f6241fbe-524e-4e94-8e94-232bcd8d0914 to node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036\n", - "2023-12-01 14:53:17,939: Added node 9b8bcb44-ec5a-42bc-a4a5-90d240f77036 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,941::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,942: Added service f69c460a-5385-4244-8582-508f806e52e4 to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,943::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,944: Added service f36088c4-0817-47a2-bd4d-1162fee46b63 to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,946: Added application 4de6ebfd-46ae-419f-b36e-3b2b079eff9d to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,949::ERROR::primaite.simulator.network.hardware.base::190::NIC 70:fb:95:ae:8b:e9/192.168.1.14 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,951::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,953: Added service 5a01c753-1c0b-4f9e-8cf9-bdab8e1151e7 to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,954::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,956: Added service 8f28720e-4374-4fe5-9b30-f2b3943691ff to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,957: Added application 500d5dc5-1bca-4904-8b2a-8d5f3c4db978 to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,958::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,960: Added service a5e5963e-c071-4715-9042-5e5887e26f3a to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,962::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,967: Added service 82dd1118-3e3c-4f72-8e62-3217e72b0360 to node 2f002450-027d-4791-832b-01327350d7e7\n", - "2023-12-01 14:53:17,969: Added node 2f002450-027d-4791-832b-01327350d7e7 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,972::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,973: Added service d8d2c796-cae8-4d6b-acd1-76a30fb3dd87 to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,974::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,975: Added service bd319940-2ad7-456b-88e9-a49b1c994edd to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,977: Added application d30f9307-d8d5-41f0-b74d-94b878b3023a to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,978::ERROR::primaite.simulator.network.hardware.base::190::NIC e8:c5:48:91:62:fe/192.168.1.16 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:17,980::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,983: Added service e926ebb1-6d91-4400-94f3-7dfab8e82eab to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,985::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,987: Added service af73e5f5-c754-4936-9018-37ef69140ced to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,988: Added application 13fd868b-e730-486f-ab83-e1bf2446e504 to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,989::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:17,991: Added service ab453a9d-62dc-4437-b0dc-c9d587962a0b to node 858361fa-1b42-4456-b184-59fa44b0c89b\n", - "2023-12-01 14:53:17,992: Added node 858361fa-1b42-4456-b184-59fa44b0c89b to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:17,995::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,003: Added service d9b44dbd-f153-4f3c-b03d-b6441a917834 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", - "2023-12-01 14:53:18,005::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,006: Added service c3da251b-500a-40dd-8c25-e26a35b5b767 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", - "2023-12-01 14:53:18,008: Added application 8f692912-fb57-40bb-ad56-d38970d34430 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", - "2023-12-01 14:53:18,010::ERROR::primaite.simulator.network.hardware.base::190::NIC a3:59:d7:fe:28:08/192.168.1.110 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,011::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - ":job_id:01000000\n", - ":task_name:bundle_reservation_check_func\n", - ":actor_name:PPO\n", - "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" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-12-01 14:53:18,013: Added service be9a1ad7-252b-47c7-ae20-e8202cb890ab to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", - "2023-12-01 14:53:18,020::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,022: Added service c5d5b514-cd30-4c7d-a4b7-b16fce71ccac to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", - "2023-12-01 14:53:18,024: Added application 918b8a5e-e339-4cb1-bb8b-ca2f3ec34372 to node 7c47bb4e-deea-4c23-910d-6ba524f73bbc\n", - "2023-12-01 14:53:18,026::ERROR::primaite.simulator.network.hardware.base::190::NIC c9:b5:db:9d:71:4d/192.168.10.110 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,027: Added node 7c47bb4e-deea-4c23-910d-6ba524f73bbc to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:18,040::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,042: Added service 66ca1dd3-0997-427b-8663-402141122e75 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,044::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,045: Added service f87b344d-8a8e-4d9d-a341-df5d3bb538ba to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,047: Added application ab20f1e3-e567-4d02-9e16-febcd57c2630 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,057::ERROR::primaite.simulator.network.hardware.base::190::NIC f5:2e:2d:55:76:d3/192.168.10.21 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,058::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,060: Added service 640815d4-878f-4dff-a607-08887b9045a7 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,061::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,063: Added service 655735a9-6db2-41df-b462-f8c14ea53e35 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,072: Added application 84fc60ba-b67e-4b72-86d1-d247fa7e31f7 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,074::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,075: Added service 8f83391f-a59b-4bdb-8fe7-7fe7e19bf1e9 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,077: Added application 75147ae5-cdce-4ac0-a884-fb9bd8f96387 to node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f\n", - "2023-12-01 14:53:18,078: Added node fee76ed0-ed6a-4ee2-bf2a-27de9bbfa17f to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:18,084::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,086: Added service 02915c59-69e2-4248-9e46-16d627460448 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,087::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,088: Added service 3ec73168-2bad-41cd-b872-7747487410c5 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,089: Added application 2acc2c11-7eed-4382-b596-389a52b42915 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,090::ERROR::primaite.simulator.network.hardware.base::190::NIC 18:f5:a0:7f:c8:60/192.168.10.22 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,091::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,093: Added service 99ae3de9-6a18-45b7-b52e-929b2be8b69b to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,093::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,095: Added service 80932931-e76e-4b98-8d05-3998f3a23a75 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,096: Added application 267775b8-36ad-4627-a5d3-dfd42ba5ecbf to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,098::WARNING::primaite.simulator.core::116::Overwriting request type scan.\n", - "2023-12-01 14:53:18,100: Added service b5c7f3cb-d928-433d-8b07-a6a778337c27 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,102: Added application a3f18b82-33be-4659-96cb-2f069c3a2aa4 to node d8a6abb1-8929-490f-a997-5e3dcb452027\n", - "2023-12-01 14:53:18,103: Added node d8a6abb1-8929-490f-a997-5e3dcb452027 to Network 07a10762-942f-409d-b36f-ea2ab7ddb136\n", - "2023-12-01 14:53:18,148::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,151::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,153::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,154::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,155::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,155::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,156::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,157::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,159::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,163::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,165::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,167::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "installing DNSClient on node client_1\n", - "installing DNSClient on node client_2\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - ":actor_name:PPO\n", - "2023-12-01 14:53:18,581::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,582::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,583::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,585::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,586::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,588::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,590::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,591::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,593::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,602::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,604::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:18,604::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - ":actor_name:PPO\n", - "Episode: 1, Step: 1, Reward: 0.5\n", - "Episode: 1, Step: 2, Reward: 0.5\n", - "Episode: 1, Step: 3, Reward: 0.5\n", - "Episode: 1, Step: 4, Reward: 0.5\n", - "Episode: 1, Step: 5, Reward: 0.5\n", - "Episode: 1, Step: 6, Reward: 0.5\n", - "Episode: 1, Step: 7, Reward: 0.5\n", - "Episode: 1, Step: 8, Reward: 0.5\n", - "Episode: 1, Step: 9, Reward: 0.5\n", - "Episode: 1, Step: 10, Reward: 0.5\n", - "Episode: 1, Step: 11, Reward: 0.5\n", - "Episode: 1, Step: 12, Reward: 0.5\n", - "Episode: 1, Step: 13, Reward: 0.5\n", - "Episode: 1, Step: 14, Reward: 0.5\n", - "Episode: 1, Step: 15, Reward: 0.5\n", - "Episode: 1, Step: 16, Reward: 0.5\n", - "Episode: 1, Step: 17, Reward: 0.5\n", - "Episode: 1, Step: 18, Reward: 0.5\n", - "Episode: 1, Step: 19, Reward: 0.5\n", - "Episode: 1, Step: 20, Reward: 0.5\n", - "Episode: 1, Step: 21, Reward: 0.5\n", - "Episode: 1, Step: 22, Reward: 0.5\n", - "Episode: 1, Step: 23, Reward: 0.5\n", - "Episode: 1, Step: 24, Reward: 0.5\n", - "Episode: 1, Step: 25, Reward: 0.5\n", - "Episode: 1, Step: 26, Reward: 0.5\n", - "Episode: 1, Step: 27, Reward: 0.5\n", - "Episode: 1, Step: 28, Reward: 0.5\n", - "Episode: 1, Step: 29, Reward: 0.5\n", - "Episode: 1, Step: 30, Reward: 0.5\n", - "Episode: 1, Step: 31, Reward: 0.5\n", - "Episode: 1, Step: 32, Reward: 0.5\n", - "Episode: 1, Step: 33, Reward: 0.5\n", - "Episode: 1, Step: 34, Reward: 0.5\n", - "Episode: 1, Step: 35, Reward: 0.5\n", - "Episode: 1, Step: 36, Reward: 0.5\n", - "Episode: 1, Step: 37, Reward: 0.5\n", - "Episode: 1, Step: 38, Reward: 0.5\n", - "Episode: 1, Step: 39, Reward: 0.5\n", - "Episode: 1, Step: 40, Reward: 0.5\n", - "Episode: 1, Step: 41, Reward: 0.5\n", - "Episode: 1, Step: 42, Reward: 0.5\n", - "Episode: 1, Step: 43, Reward: 0.5\n", - "Episode: 1, Step: 44, Reward: 0.5\n", - "Episode: 1, Step: 45, Reward: 0.5\n", - "Episode: 1, Step: 46, Reward: 0.5\n", - "Episode: 1, Step: 47, Reward: 0.5\n", - "Episode: 1, Step: 48, Reward: 0.5\n", - "Episode: 1, Step: 49, Reward: 0.5\n", - "Episode: 1, Step: 50, Reward: 0.5\n", - "Episode: 1, Step: 51, Reward: 0.5\n", - "Episode: 1, Step: 52, Reward: 0.5\n", - "Episode: 1, Step: 53, Reward: 0.5\n", - "Episode: 1, Step: 54, Reward: 0.5\n", - "Episode: 1, Step: 55, Reward: 0.5\n", - "Episode: 1, Step: 56, Reward: 0.5\n", - "Episode: 1, Step: 57, Reward: 0.5\n", - "Episode: 1, Step: 58, Reward: 0.5\n", - "Episode: 1, Step: 59, Reward: 0.5\n", - "Episode: 1, Step: 60, Reward: 0.5\n", - "Episode: 1, Step: 61, Reward: 0.5\n", - "Episode: 1, Step: 62, Reward: 0.5\n", - "Episode: 1, Step: 63, Reward: 0.5\n", - "Episode: 1, Step: 64, Reward: 0.5\n", - "Episode: 1, Step: 65, Reward: 0.5\n", - "Episode: 1, Step: 66, Reward: 0.5\n", - "Episode: 1, Step: 67, Reward: 0.5\n", - "Episode: 1, Step: 68, Reward: 0.5\n", - "Episode: 1, Step: 69, Reward: 0.5\n", - "Episode: 1, Step: 70, Reward: 0.5\n", - "Episode: 1, Step: 71, Reward: 0.5\n", - "Episode: 1, Step: 72, Reward: 0.5\n", - "Episode: 1, Step: 73, Reward: 0.5\n", - "Episode: 1, Step: 74, Reward: 0.5\n", - "Episode: 1, Step: 75, Reward: 0.5\n", - "Episode: 1, Step: 76, Reward: 0.5\n", - "Episode: 1, Step: 77, Reward: 0.5\n", - "Episode: 1, Step: 78, Reward: 0.5\n", - "Episode: 1, Step: 79, Reward: 0.5\n", - "Episode: 1, Step: 80, Reward: 0.5\n", - "Episode: 1, Step: 81, Reward: 0.5\n", - "Episode: 1, Step: 82, Reward: 0.5\n", - "Episode: 1, Step: 83, Reward: 0.5\n", - "Episode: 1, Step: 84, Reward: 0.5\n", - "Episode: 1, Step: 85, Reward: 0.5\n", - "Episode: 1, Step: 86, Reward: 0.5\n", - "Episode: 1, Step: 87, Reward: 0.5\n", - "Episode: 1, Step: 88, Reward: 0.5\n", - "Episode: 1, Step: 89, Reward: 0.5\n", - "Episode: 1, Step: 90, Reward: 0.5\n", - "Episode: 1, Step: 91, Reward: 0.5\n", - "Episode: 1, Step: 92, Reward: 0.5\n", - "Episode: 1, Step: 93, Reward: 0.5\n", - "Episode: 1, Step: 94, Reward: 0.5\n", - "Episode: 1, Step: 95, Reward: 0.5\n", - "Episode: 1, Step: 96, Reward: 0.5\n", - "Episode: 1, Step: 97, Reward: 0.5\n", - "Episode: 1, Step: 98, Reward: 0.5\n", - "Episode: 1, Step: 99, Reward: 0.5\n", - "Episode: 1, Step: 100, Reward: 0.5\n", - "Episode: 1, Step: 101, Reward: 0.5\n", - "Episode: 1, Step: 102, Reward: 0.5\n", - "Episode: 1, Step: 103, Reward: 0.5\n", - "Episode: 1, Step: 104, Reward: 0.5\n", - "Episode: 1, Step: 105, Reward: 0.5\n", - "Episode: 1, Step: 106, Reward: 0.5\n", - "Episode: 1, Step: 107, Reward: 0.5\n", - "Episode: 1, Step: 108, Reward: 0.5\n", - "Episode: 1, Step: 109, Reward: 0.5\n", - "Episode: 1, Step: 110, Reward: 0.5\n", - "Episode: 1, Step: 111, Reward: 0.5\n", - "Episode: 1, Step: 112, Reward: 0.5\n", - "Episode: 1, Step: 113, Reward: 0.5\n", - "Episode: 1, Step: 114, Reward: 0.5\n", - "Episode: 1, Step: 115, Reward: 0.5\n", - "Episode: 1, Step: 116, Reward: 0.5\n", - "Episode: 1, Step: 117, Reward: 0.5\n", - "Episode: 1, Step: 118, Reward: 0.5\n", - "Episode: 1, Step: 119, Reward: 0.5\n", - "Episode: 1, Step: 120, Reward: 0.5\n", - "Episode: 1, Step: 121, Reward: 0.5\n", - "Episode: 1, Step: 122, Reward: 0.5\n", - "Episode: 1, Step: 123, Reward: 0.5\n", - "Episode: 1, Step: 124, Reward: 0.5\n", - "Episode: 1, Step: 125, Reward: 0.5\n", - "Episode: 1, Step: 126, Reward: 0.5\n", - "Episode: 1, Step: 127, Reward: 0.5\n", - "Episode: 1, Step: 128, Reward: 0.5\n", - "Episode: 1, Step: 129, Reward: 0.5\n", - "\n", - "Episode: 1, Step: 130, Reward: 0.5\n", - "Episode: 1, Step: 131, Reward: 0.5\n", - "Episode: 1, Step: 132, Reward: 0.5\n", - "Episode: 1, Step: 133, Reward: 0.5\n", - "Episode: 1, Step: 134, Reward: 0.5\n", - "Episode: 1, Step: 135, Reward: 0.5\n", - "Episode: 1, Step: 136, Reward: 0.5\n", - "Episode: 1, Step: 137, Reward: 0.5\n", - "Episode: 1, Step: 138, Reward: 0.5\n", - "Episode: 1, Step: 139, Reward: 0.5\n", - "Episode: 1, Step: 140, Reward: 0.5\n", - "Episode: 1, Step: 141, Reward: 0.5\n", - "Episode: 1, Step: 142, Reward: 0.5\n", - "Episode: 1, Step: 143, Reward: 0.5\n", - "Episode: 1, Step: 144, Reward: 0.5\n", - "Episode: 1, Step: 145, Reward: 0.5\n", - "Episode: 1, Step: 146, Reward: 0.5\n", - "Episode: 1, Step: 147, Reward: 0.5\n", - "Episode: 1, Step: 148, Reward: 0.5\n", - "Episode: 1, Step: 149, Reward: 0.5\n", - "Episode: 1, Step: 150, Reward: 0.5\n", - "Episode: 1, Step: 151, Reward: 0.5\n", - "Episode: 1, Step: 152, Reward: 0.5\n", - "Episode: 1, Step: 153, Reward: 0.5\n", - "Episode: 1, Step: 154, Reward: 0.5\n", - "Episode: 1, Step: 155, Reward: 0.5\n", - "Episode: 1, Step: 156, Reward: 0.5\n", - "Episode: 1, Step: 157, Reward: 0.5\n", - "Episode: 1, Step: 158, Reward: 0.5\n", - "Episode: 1, Step: 159, Reward: 0.5\n", - "Episode: 1, Step: 160, Reward: 0.5\n", - "Episode: 1, Step: 161, Reward: 0.5\n", - "Episode: 1, Step: 162, Reward: 0.5\n", - "Episode: 1, Step: 163, Reward: 0.5\n", - "Episode: 1, Step: 164, Reward: 0.5\n", - "Episode: 1, Step: 165, Reward: 0.5\n", - "Episode: 1, Step: 166, Reward: 0.5\n", - "Episode: 1, Step: 167, Reward: 0.5\n", - "Episode: 1, Step: 168, Reward: 0.5\n", - "Episode: 1, Step: 169, Reward: 0.5\n", - "Episode: 1, Step: 170, Reward: 0.5\n", - "Episode: 1, Step: 171, Reward: 0.5\n", - "Episode: 1, Step: 172, Reward: 0.5\n", - "Episode: 1, Step: 173, Reward: 0.5\n", - "Episode: 1, Step: 174, Reward: 0.5\n", - "Episode: 1, Step: 175, Reward: 0.5\n", - "Episode: 1, Step: 176, Reward: 0.5\n", - "Episode: 1, Step: 177, Reward: 0.5\n", - "Episode: 1, Step: 178, Reward: 0.5\n", - "Episode: 1, Step: 179, Reward: 0.5\n", - "Episode: 1, Step: 180, Reward: 0.5\n", - "Episode: 1, Step: 181, Reward: 0.5\n", - "Episode: 1, Step: 182, Reward: 0.5\n", - "Episode: 1, Step: 183, Reward: 0.5\n", - "Episode: 1, Step: 184, Reward: 0.5\n", - "Episode: 1, Step: 185, Reward: 0.5\n", - "Episode: 1, Step: 186, Reward: 0.5\n", - "Episode: 1, Step: 187, Reward: 0.5\n", - "Episode: 1, Step: 188, Reward: 0.5\n", - "Episode: 1, Step: 189, Reward: 0.5\n", - "Episode: 1, Step: 190, Reward: 0.5\n", - "Episode: 1, Step: 191, Reward: 0.5\n", - "Episode: 1, Step: 192, Reward: 0.5\n", - "Episode: 1, Step: 193, Reward: 0.5\n", - "Episode: 1, Step: 194, Reward: 0.5\n", - "Episode: 1, Step: 195, Reward: 0.5\n", - "Episode: 1, Step: 196, Reward: 0.5\n", - "Episode: 1, Step: 197, Reward: 0.5\n", - "Episode: 1, Step: 198, Reward: 0.5\n", - "Episode: 1, Step: 199, Reward: 0.5\n", - "Episode: 1, Step: 200, Reward: 0.5\n", - "Episode: 1, Step: 201, Reward: 0.5\n", - "Episode: 1, Step: 202, Reward: 0.5\n", - "Episode: 1, Step: 203, Reward: 0.5\n", - "Episode: 1, Step: 204, Reward: 0.5\n", - "Episode: 1, Step: 205, Reward: 0.5\n", - "Episode: 1, Step: 206, Reward: 0.5\n", - "Episode: 1, Step: 207, Reward: 0.5\n", - "Episode: 1, Step: 208, Reward: 0.5\n", - "Episode: 1, Step: 209, Reward: 0.5\n", - "Episode: 1, Step: 210, Reward: 0.5\n", - "Episode: 1, Step: 211, Reward: 0.5\n", - "Episode: 1, Step: 212, Reward: 0.5\n", - "Episode: 1, Step: 213, Reward: 0.5\n", - "Episode: 1, Step: 214, Reward: 0.5\n", - "Episode: 1, Step: 215, Reward: 0.5\n", - "Episode: 1, Step: 216, Reward: 0.5\n", - "Episode: 1, Step: 217, Reward: 0.5\n", - "Episode: 1, Step: 218, Reward: 0.5\n", - "Episode: 1, Step: 219, Reward: 0.5\n", - "Episode: 1, Step: 220, Reward: 0.5\n", - "Episode: 1, Step: 221, Reward: 0.5\n", - "Episode: 1, Step: 222, Reward: 0.5\n", - "Episode: 1, Step: 223, Reward: 0.5\n", - "Episode: 1, Step: 224, Reward: 0.5\n", - "Episode: 1, Step: 225, Reward: 0.5\n", - "Episode: 1, Step: 226, Reward: 0.5\n", - "Episode: 1, Step: 227, Reward: 0.5\n", - "Episode: 1, Step: 228, Reward: 0.5\n", - "Episode: 1, Step: 229, Reward: 0.5\n", - "Episode: 1, Step: 230, Reward: 0.5\n", - "Episode: 1, Step: 231, Reward: 0.5\n", - "Episode: 1, Step: 232, Reward: 0.5\n", - "Episode: 1, Step: 233, Reward: 0.5\n", - "Episode: 1, Step: 234, Reward: 0.5\n", - "Episode: 1, Step: 235, Reward: 0.5\n", - "Episode: 1, Step: 236, Reward: 0.5\n", - "Episode: 1, Step: 237, Reward: 0.5\n", - "Episode: 1, Step: 238, Reward: 0.5\n", - "Episode: 1, Step: 239, Reward: 0.5\n", - "Episode: 1, Step: 240, Reward: 0.5\n", - "Episode: 1, Step: 241, Reward: 0.5\n", - "Episode: 1, Step: 242, Reward: 0.5\n", - "Episode: 1, Step: 243, Reward: 0.5\n", - "Episode: 1, Step: 244, Reward: 0.5\n", - "Episode: 1, Step: 245, Reward: 0.5\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-12-01 14:53:21,247::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,248::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,249::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,251::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,252::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,254::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,256::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,259::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,262::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,292::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,293::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:21,294::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Episode: 1, Step: 246, Reward: 0.5\n", - "Episode: 1, Step: 247, Reward: 0.5\n", - "Episode: 1, Step: 248, Reward: 0.5\n", - "Episode: 1, Step: 249, Reward: 0.5\n", - "Episode: 1, Step: 250, Reward: 0.5\n", - "Episode: 1, Step: 251, Reward: 0.5\n", - "Episode: 1, Step: 252, Reward: 0.5\n", - "Episode: 1, Step: 253, Reward: 0.5\n", - "Episode: 1, Step: 254, Reward: 0.5\n", - "Episode: 1, Step: 255, Reward: 0.5\n", - "Episode: 1, Step: 256, Reward: 0.5\n", - "Episode: 2, Step: 1, Reward: 0.5\n", - "Episode: 2, Step: 2, Reward: 0.5\n", - "Episode: 2, Step: 3, Reward: 0.5\n", - "Episode: 2, Step: 4, Reward: 0.5\n", - "Episode: 2, Step: 5, Reward: 0.5\n", - "Episode: 2, Step: 6, Reward: 0.5\n", - "Episode: 2, Step: 7, Reward: 0.5\n", - "Episode: 2, Step: 8, Reward: 0.5\n", - "Episode: 2, Step: 9, Reward: 0.5\n", - "Episode: 2, Step: 10, Reward: 0.5\n", - "Episode: 2, Step: 11, Reward: 0.5\n", - "Episode: 2, Step: 12, Reward: 0.5\n", - "Episode: 2, Step: 13, Reward: 0.5\n", - "Episode: 2, Step: 14, Reward: 0.5\n", - "Episode: 2, Step: 15, Reward: 0.5\n", - "Episode: 2, Step: 16, Reward: 0.5\n", - "Episode: 2, Step: 17, Reward: 0.5\n", - "Episode: 2, Step: 18, Reward: 0.5\n", - "Episode: 2, Step: 19, Reward: 0.5\n", - "Episode: 2, Step: 20, Reward: 0.5\n", - "Episode: 2, Step: 21, Reward: 0.5\n", - "Episode: 2, Step: 22, Reward: 0.5\n", - "Episode: 2, Step: 23, Reward: 0.5\n", - "Episode: 2, Step: 24, Reward: 0.5\n", - "Episode: 2, Step: 25, Reward: 0.5\n", - "Episode: 2, Step: 26, Reward: 0.5\n", - "Episode: 2, Step: 27, Reward: 0.5\n", - "Episode: 2, Step: 28, Reward: 0.5\n", - "Episode: 2, Step: 29, Reward: 0.5\n", - "Episode: 2, Step: 30, Reward: 0.5\n", - "Episode: 2, Step: 31, Reward: 0.5\n", - "Episode: 2, Step: 32, Reward: 0.5\n", - "Episode: 2, Step: 33, Reward: 0.5\n", - "Episode: 2, Step: 34, Reward: 0.5\n", - "Episode: 2, Step: 35, Reward: 0.5\n", - "Episode: 2, Step: 36, Reward: 0.5\n", - "Episode: 2, Step: 37, Reward: 0.5\n", - "Episode: 2, Step: 38, Reward: 0.5\n", - "Episode: 2, Step: 39, Reward: 0.5\n", - "Episode: 2, Step: 40, Reward: 0.5\n", - "Episode: 2, Step: 41, Reward: 0.5\n", - "Episode: 2, Step: 42, Reward: 0.5\n", - "Episode: 2, Step: 43, Reward: 0.5\n", - "Episode: 2, Step: 44, Reward: 0.5\n", - "Episode: 2, Step: 45, Reward: 0.5\n", - "Episode: 2, Step: 46, Reward: 0.5\n", - "Episode: 2, Step: 47, Reward: 0.5\n", - "Episode: 2, Step: 48, Reward: 0.5\n", - "Episode: 2, Step: 49, Reward: 0.5\n", - "Episode: 2, Step: 50, Reward: 0.5\n", - "Episode: 2, Step: 51, Reward: 0.5\n", - "Episode: 2, Step: 52, Reward: 0.5\n", - "Episode: 2, Step: 53, Reward: 0.5\n", - "Episode: 2, Step: 54, Reward: 0.5\n", - "Episode: 2, Step: 55, Reward: 0.5\n", - "Episode: 2, Step: 56, Reward: 0.5\n", - "Episode: 2, Step: 57, Reward: 0.5\n", - "Episode: 2, Step: 58, Reward: 0.5\n", - "Episode: 2, Step: 59, Reward: 0.5\n", - "Episode: 2, Step: 60, Reward: 0.5\n", - "Episode: 2, Step: 61, Reward: 0.5\n", - "Episode: 2, Step: 62, Reward: 0.5\n", - "Episode: 2, Step: 63, Reward: 0.5\n", - "Episode: 2, Step: 64, Reward: 0.5\n", - "Episode: 2, Step: 65, Reward: 0.5\n", - "Episode: 2, Step: 66, Reward: 0.5\n", - "Episode: 2, Step: 67, Reward: 0.5\n", - "Episode: 2, Step: 68, Reward: 0.5\n", - "Episode: 2, Step: 69, Reward: 0.5\n", - "Episode: 2, Step: 70, Reward: 0.5\n", - "Episode: 2, Step: 71, Reward: 0.5\n", - "Episode: 2, Step: 72, Reward: 0.5\n", - "Episode: 2, Step: 73, Reward: 0.5\n", - "Episode: 2, Step: 74, Reward: 0.5\n", - "Episode: 2, Step: 75, Reward: 0.5\n", - "Episode: 2, Step: 76, Reward: 0.5\n", - "Episode: 2, Step: 77, Reward: 0.5\n", - "Episode: 2, Step: 78, Reward: 0.5\n", - "Episode: 2, Step: 79, Reward: 0.5\n", - "Episode: 2, Step: 80, Reward: 0.5\n", - "Episode: 2, Step: 81, Reward: 0.5\n", - "Episode: 2, Step: 82, Reward: 0.5\n", - "Episode: 2, Step: 83, Reward: 0.5\n", - "Episode: 2, Step: 84, Reward: 0.5\n", - "Episode: 2, Step: 85, Reward: 0.5\n", - "Episode: 2, Step: 86, Reward: 0.5\n", - "Episode: 2, Step: 87, Reward: 0.5\n", - "Episode: 2, Step: 88, Reward: 0.5\n", - "Episode: 2, Step: 89, Reward: 0.5\n", - "Episode: 2, Step: 90, Reward: 0.5\n", - "Episode: 2, Step: 91, Reward: 0.5\n", - "Episode: 2, Step: 92, Reward: 0.5\n", - "Episode: 2, Step: 93, Reward: 0.5\n", - "Episode: 2, Step: 94, Reward: 0.5\n", - "Episode: 2, Step: 95, Reward: 0.5\n", - "Episode: 2, Step: 96, Reward: 0.5\n", - "Episode: 2, Step: 97, Reward: 0.5\n", - "Episode: 2, Step: 98, Reward: 0.5\n", - "Episode: 2, Step: 99, Reward: 0.5\n", - "Episode: 2, Step: 100, Reward: 0.5\n", - "Episode: 2, Step: 101, Reward: 0.5\n", - "Episode: 2, Step: 102, Reward: 0.5\n", - "Episode: 2, Step: 103, Reward: 0.5\n", - "Episode: 2, Step: 104, Reward: 0.5\n", - "Episode: 2, Step: 105, Reward: 0.5\n", - "Episode: 2, Step: 106, Reward: 0.5\n", - "Episode: 2, Step: 107, Reward: 0.5\n", - "Episode: 2, Step: 108, Reward: 0.5\n", - "Episode: 2, Step: 109, Reward: 0.5\n", - "Episode: 2, Step: 110, Reward: 0.5\n", - "Episode: 2, Step: 111, Reward: 0.5\n", - "Episode: 2, Step: 112, Reward: 0.5\n", - "Episode: 2, Step: 113, Reward: 0.5\n", - "Episode: 2, Step: 114, Reward: 0.5\n", - "Episode: 2, Step: 115, Reward: 0.5\n", - "Episode: 2, Step: 116, Reward: 0.5\n", - "Episode: 2, Step: 117, Reward: 0.5\n", - "Episode: 2, Step: 118, Reward: 0.5\n", - "Episode: 2, Step: 119, Reward: 0.5\n", - "Episode: 2, Step: 120, Reward: 0.5\n", - "Episode: 2, Step: 121, Reward: 0.5\n", - "Episode: 2, Step: 122, Reward: 0.5\n", - "Episode: 2, Step: 123, Reward: 0.5\n", - "Episode: 2, Step: 124, Reward: 0.5\n", - "Episode: 2, Step: 125, Reward: 0.5\n", - "Episode: 2, Step: 126, Reward: 0.5\n", - "Episode: 2, Step: 127, Reward: 0.5\n", - "Episode: 2, Step: 128, Reward: 0.5\n", - "Episode: 2, Step: 129, Reward: 0.5\n", - "Episode: 2, Step: 130, Reward: 0.5\n", - "Episode: 2, Step: 131, Reward: 0.5\n", - "Episode: 2, Step: 132, Reward: 0.5\n", - "Episode: 2, Step: 133, Reward: 0.5\n", - "Episode: 2, Step: 134, Reward: 0.5\n", - "Episode: 2, Step: 135, Reward: 0.5\n", - "Episode: 2, Step: 136, Reward: 0.5\n", - "Episode: 2, Step: 137, Reward: 0.5\n", - "Episode: 2, Step: 138, Reward: 0.5\n", - "Episode: 2, Step: 139, Reward: 0.5\n", - "Episode: 2, Step: 140, Reward: 0.5\n", - "Episode: 2, Step: 141, Reward: 0.5\n", - "Episode: 2, Step: 142, Reward: 0.5\n", - "Episode: 2, Step: 143, Reward: 0.5\n", - "Episode: 2, Step: 144, Reward: 0.5\n", - "Episode: 2, Step: 145, Reward: 0.5\n", - "Episode: 2, Step: 146, Reward: 0.5\n", - "Episode: 2, Step: 147, Reward: 0.5\n", - "Episode: 2, Step: 148, Reward: 0.5\n", - "Episode: 2, Step: 149, Reward: 0.5\n", - "Episode: 2, Step: 150, Reward: 0.5\n", - "Episode: 2, Step: 151, Reward: 0.5\n", - "Episode: 2, Step: 152, Reward: 0.5\n", - "Episode: 2, Step: 153, Reward: 0.5\n", - "Episode: 2, Step: 154, Reward: 0.5\n", - "Episode: 2, Step: 155, Reward: 0.5\n", - "Episode: 2, Step: 156, Reward: 0.5\n", - "Episode: 2, Step: 157, Reward: 0.5\n", - "Episode: 2, Step: 158, Reward: 0.5\n", - "Episode: 2, Step: 159, Reward: 0.5\n", - "Episode: 2, Step: 160, Reward: 0.5\n", - "Episode: 2, Step: 161, Reward: 0.5\n", - "Episode: 2, Step: 162, Reward: 0.5\n", - "Episode: 2, Step: 163, Reward: 0.5\n", - "Episode: 2, Step: 164, Reward: 0.5\n", - "Episode: 2, Step: 165, Reward: 0.5\n", - "Episode: 2, Step: 166, Reward: 0.5\n", - "Episode: 2, Step: 167, Reward: 0.5\n", - "Episode: 2, Step: 168, Reward: 0.5\n", - "Episode: 2, Step: 169, Reward: 0.5\n", - "Episode: 2, Step: 170, Reward: 0.5\n", - "Episode: 2, Step: 171, Reward: 0.5\n", - "Episode: 2, Step: 172, Reward: 0.5\n", - "Episode: 2, Step: 173, Reward: 0.5\n", - "Episode: 2, Step: 174, Reward: 0.5\n", - "Episode: 2, Step: 175, Reward: 0.5\n", - "Episode: 2, Step: 176, Reward: 0.5\n", - "Episode: 2, Step: 177, Reward: 0.5\n", - "Episode: 2, Step: 178, Reward: 0.5\n", - "Episode: 2, Step: 179, Reward: 0.5\n", - "Episode: 2, Step: 180, Reward: 0.5\n", - "Episode: 2, Step: 181, Reward: 0.5\n", - "Episode: 2, Step: 182, Reward: 0.5\n", - "Episode: 2, Step: 183, Reward: 0.5\n", - "Episode: 2, Step: 184, Reward: 0.5\n", - "Episode: 2, Step: 185, Reward: 0.5\n", - "Episode: 2, Step: 186, Reward: 0.5\n", - "Episode: 2, Step: 187, Reward: 0.5\n", - "Episode: 2, Step: 188, Reward: 0.5\n", - "Episode: 2, Step: 189, Reward: 0.5\n", - "Episode: 2, Step: 190, Reward: 0.5\n", - "Episode: 2, Step: 191, Reward: 0.5\n", - "Episode: 2, Step: 192, Reward: 0.5\n", - "Episode: 2, Step: 193, Reward: 0.5\n", - "Episode: 2, Step: 194, Reward: 0.5\n", - "Episode: 2, Step: 195, Reward: 0.5\n", - "Episode: 2, Step: 196, Reward: 0.5\n", - "Episode: 2, Step: 197, Reward: 0.5\n", - "Episode: 2, Step: 198, Reward: 0.5\n", - "Episode: 2, Step: 199, Reward: 0.5\n", - "Episode: 2, Step: 200, Reward: 0.5\n", - "Episode: 2, Step: 201, Reward: 0.5\n", - "Episode: 2, Step: 202, Reward: 0.5\n", - "Episode: 2, Step: 203, Reward: 0.5\n", - "Episode: 2, Step: 204, Reward: 0.5\n", - "Episode: 2, Step: 205, Reward: 0.5\n", - "Episode: 2, Step: 206, Reward: 0.5\n", - "Episode: 2, Step: 207, Reward: 0.5\n", - "Episode: 2, Step: 208, Reward: 0.5\n", - "Episode: 2, Step: 209, Reward: 0.5\n", - "Episode: 2, Step: 210, Reward: 0.5\n", - "Episode: 2, Step: 211, Reward: 0.5\n", - "Episode: 2, Step: 212, Reward: 0.5\n", - "Episode: 2, Step: 213, Reward: 0.5\n", - "Episode: 2, Step: 214, Reward: 0.5\n", - "Episode: 2, Step: 215, Reward: 0.5\n", - "Episode: 2, Step: 216, Reward: 0.5\n", - "Episode: 2, Step: 217, Reward: 0.5\n", - "Episode: 2, Step: 218, Reward: 0.5\n", - "Episode: 2, Step: 219, Reward: 0.5\n", - "Episode: 2, Step: 220, Reward: 0.5\n", - "Episode: 2, Step: 221, Reward: 0.5\n", - "Episode: 2, Step: 222, Reward: 0.5\n", - "Episode: 2, Step: 223, Reward: 0.5\n", - "Episode: 2, Step: 224, Reward: 0.5\n", - "Episode: 2, Step: 225, Reward: 0.5\n", - "Episode: 2, Step: 226, Reward: 0.5\n", - "Episode: 2, Step: 227, Reward: 0.5\n", - "Episode: 2, Step: 228, Reward: 0.5\n", - "Episode: 2, Step: 229, Reward: 0.5\n", - "Episode: 2, Step: 230, Reward: 0.5\n", - "Episode: 2, Step: 231, Reward: 0.5\n", - "Episode: 2, Step: 232, Reward: 0.5\n", - "Episode: 2, Step: 233, Reward: 0.5\n", - "Episode: 2, Step: 234, Reward: 0.5\n", - "Episode: 2, Step: 235, Reward: 0.5\n", - "Episode: 2, Step: 236, Reward: 0.5\n", - "Episode: 2, Step: 237, Reward: 0.5\n", - "Episode: 2, Step: 238, Reward: 0.5\n", - "Episode: 2, Step: 239, Reward: 0.5\n", - "Episode: 2, Step: 240, Reward: 0.5\n", - "Episode: 2, Step: 241, Reward: 0.5\n", - "Episode: 2, Step: 242, Reward: 0.5\n", - "Episode: 2, Step: 243, Reward: 0.5\n", - "Episode: 2, Step: 244, Reward: 0.5\n", - "Episode: 2, Step: 245, Reward: 0.5\n", - "Episode: 2, Step: 246, Reward: 0.5\n", - "Episode: 2, Step: 247, Reward: 0.5\n", - "Episode: 2, Step: 248, Reward: 0.5\n", - "Episode: 2, Step: 249, Reward: 0.5\n", - "Episode: 2, Step: 250, Reward: 0.5\n", - "Episode: 2, Step: 251, Reward: 0.5\n", - "Episode: 2, Step: 252, Reward: 0.5\n", - "Episode: 2, Step: 253, Reward: 0.5\n", - "Episode: 2, Step: 254, Reward: 0.5\n", - "Episode: 2, Step: 255, Reward: 0.5\n", - "Episode: 2, Step: 256, Reward: 0.5\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-12-01 14:53:24,371::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,373::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,375::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,375::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,376::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,377::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,379::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,380::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,381::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,402::ERROR::primaite.simulator.network.hardware.base::190::NIC 36:ad:98:49:2d:a7/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,404::ERROR::primaite.simulator.network.hardware.base::190::NIC c4:0e:cf:36:55:de/127.0.0.1 cannot be enabled as it is not connected to a Link\n", - "2023-12-01 14:53:24,406::ERROR::primaite.simulator.network.hardware.base::190::NIC ec:6e:b6:5c:8a:e7/127.0.0.1 cannot be enabled as it is not connected to a Link\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Episode: 3, Step: 1, Reward: 0.5\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2023-12-01 14:53:24,878\tINFO storage.py:563 -- Checkpoint successfully created at: Checkpoint(filesystem=local, path=/home/cade/ray_results/PPO_2023-12-01_14-53-17/PPO_PrimaiteRayEnv_5cbc4_00000_0_2023-12-01_14-53-17/checkpoint_000000)\n", - "2023-12-01 14:53:25,098\tINFO tune.py:1047 -- Total run time: 7.37 seconds (7.31 seconds for the tuning loop).\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "ResultGrid<[\n", - " Result(\n", - " metrics={'custom_metrics': {}, 'episode_media': {}, 'info': {'learner': {'__all__': {'num_agent_steps_trained': 128.0, 'num_env_steps_trained': 128.0, 'total_loss': 9.403312460581462}, 'default_policy': {'total_loss': 9.403312460581462, 'policy_loss': -0.06894568807135025, 'vf_loss': 9.469796816507975, 'vf_loss_unclipped': 416.65203653971355, 'vf_explained_var': 0.0007335106531778971, 'entropy': 3.864323592185974, 'mean_kl_loss': 0.012305201259247648, 'default_optimizer_lr': 4.999999999999999e-05, 'curr_lr': 5e-05, 'curr_entropy_coeff': 0.0, 'curr_kl_coeff': 0.20000000298023224}}, 'num_env_steps_sampled': 512, 'num_env_steps_trained': 0, 'num_agent_steps_sampled': 512, 'num_agent_steps_trained': 0}, 'sampler_results': {'episode_reward_max': 128.0, 'episode_reward_min': 128.0, 'episode_reward_mean': 128.0, 'episode_len_mean': 256.0, 'episode_media': {}, 'episodes_this_iter': 1, 'policy_reward_min': {}, 'policy_reward_max': {}, 'policy_reward_mean': {}, 'custom_metrics': {}, 'hist_stats': {'episode_reward': [128.0, 128.0], 'episode_lengths': [256, 256]}, 'sampler_perf': {'mean_raw_obs_processing_ms': 0.8607522543689299, 'mean_inference_ms': 2.1271821797748984, 'mean_action_processing_ms': 0.15329866429338604, 'mean_env_wait_ms': 6.184263571370873, 'mean_env_render_ms': 0.0}, 'num_faulty_episodes': 0, 'connector_metrics': {'ObsPreprocessorConnector_ms': 0.010561943054199219, 'StateBufferConnector_ms': 0.004971027374267578, 'ViewRequirementAgentConnector_ms': 0.29495954513549805}}, 'episode_reward_max': 128.0, 'episode_reward_min': 128.0, 'episode_reward_mean': 128.0, 'episode_len_mean': 256.0, 'episodes_this_iter': 1, 'policy_reward_min': {}, 'policy_reward_max': {}, 'policy_reward_mean': {}, 'hist_stats': {'episode_reward': [128.0, 128.0], 'episode_lengths': [256, 256]}, 'sampler_perf': {'mean_raw_obs_processing_ms': 0.8607522543689299, 'mean_inference_ms': 2.1271821797748984, 'mean_action_processing_ms': 0.15329866429338604, 'mean_env_wait_ms': 6.184263571370873, 'mean_env_render_ms': 0.0}, 'num_faulty_episodes': 0, 'connector_metrics': {'ObsPreprocessorConnector_ms': 0.010561943054199219, 'StateBufferConnector_ms': 0.004971027374267578, 'ViewRequirementAgentConnector_ms': 0.29495954513549805}, 'num_healthy_workers': 0, 'num_in_flight_async_reqs': 0, 'num_remote_worker_restarts': 0, 'num_agent_steps_sampled': 512, 'num_agent_steps_trained': 0, 'num_env_steps_sampled': 512, 'num_env_steps_trained': 0, 'num_env_steps_sampled_this_iter': 128, 'num_env_steps_trained_this_iter': 0, 'num_env_steps_sampled_throughput_per_sec': 85.63165451744611, 'num_env_steps_trained_throughput_per_sec': 0.0, 'num_steps_trained_this_iter': 0, 'agent_timesteps_total': 512, 'timers': {'training_iteration_time_ms': 1530.574, 'sample_time_ms': 1196.582, 'synch_weights_time_ms': 1.912}, 'counters': {'num_env_steps_sampled': 512, 'num_env_steps_trained': 0, 'num_agent_steps_sampled': 512, 'num_agent_steps_trained': 0}, 'perf': {'cpu_util_percent': 55.25, 'ram_util_percent': 58.8}},\n", - " path='/home/cade/ray_results/PPO_2023-12-01_14-53-17/PPO_PrimaiteRayEnv_5cbc4_00000_0_2023-12-01_14-53-17',\n", - " filesystem='local',\n", - " checkpoint=Checkpoint(filesystem=local, path=/home/cade/ray_results/PPO_2023-12-01_14-53-17/PPO_PrimaiteRayEnv_5cbc4_00000_0_2023-12-01_14-53-17/checkpoint_000000)\n", - " )\n", - "]>" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "source": [ + "#### Set training parameters and start the training" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "tune.Tuner(\n", " \"PPO\",\n", diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 87cf4f2d..4f4bb829 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -37,11 +37,14 @@ class PrimaiteGymEnv(gymnasium.Env): terminated = False truncated = self.game.calculate_truncated() info = {} - print(f"Episode: {self.game.episode_counter}, Step: {self.game.step_counter}, Reward: {reward}") return next_obs, reward, terminated, truncated, info def reset(self, seed: Optional[int] = None) -> Tuple[ObsType, Dict[str, Any]]: """Reset the environment.""" + print( + f"Resetting environment, episode {self.game.episode_counter}, " + "avg. reward: {self.game.rl_agents[0].reward_function.total_reward}" + ) self.game.reset() state = self.game.get_sim_state() self.game.update_agents(state) diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index 3919902a..3c8b40bd 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -62,6 +62,7 @@ class PrimaiteSession: def start_session(self) -> None: """Commence the training/eval session.""" + print("Staring Primaite Session") self.mode = SessionMode.TRAIN n_learn_episodes = self.training_options.n_learn_episodes n_eval_episodes = self.training_options.n_eval_episodes diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 18a470cd..08779d96 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -113,7 +113,7 @@ class RequestManager(BaseModel): """ if name in self.request_types: msg = f"Overwriting request type {name}." - _LOGGER.warn(msg) + _LOGGER.debug(msg) self.request_types[name] = request_type diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 97b62f95..e1780448 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -220,7 +220,7 @@ class Network(SimComponent): self._node_id_map[len(self.nodes)] = node node.parent = self self._nx_graph.add_node(node.hostname) - _LOGGER.info(f"Added node {node.uuid} to Network {self.uuid}") + _LOGGER.debug(f"Added node {node.uuid} to Network {self.uuid}") self._node_request_manager.add_request(name=node.uuid, request_type=RequestType(func=node._request_manager)) def get_node_by_hostname(self, hostname: str) -> Optional[Node]: diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 04c76c6b..a310a3f5 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -181,13 +181,13 @@ class NIC(SimComponent): if self.enabled: return if not self._connected_node: - _LOGGER.error(f"NIC {self} cannot be enabled as it is not connected to a Node") + _LOGGER.debug(f"NIC {self} cannot be enabled as it is not connected to a Node") return if self._connected_node.operating_state != NodeOperatingState.ON: self._connected_node.sys_log.error(f"NIC {self} cannot be enabled as the endpoint is not turned on") return if not self._connected_link: - _LOGGER.error(f"NIC {self} cannot be enabled as it is not connected to a Link") + _LOGGER.debug(f"NIC {self} cannot be enabled as it is not connected to a Link") return self.enabled = True From 3e3fd89618bd12a9d7ce2e0216d2e930bb4205c9 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Fri, 1 Dec 2023 15:41:10 +0000 Subject: [PATCH 088/107] Minor string fix --- src/primaite/session/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primaite/session/environment.py b/src/primaite/session/environment.py index 4f4bb829..c2f19f36 100644 --- a/src/primaite/session/environment.py +++ b/src/primaite/session/environment.py @@ -43,7 +43,7 @@ class PrimaiteGymEnv(gymnasium.Env): """Reset the environment.""" print( f"Resetting environment, episode {self.game.episode_counter}, " - "avg. reward: {self.game.rl_agents[0].reward_function.total_reward}" + f"avg. reward: {self.game.rl_agents[0].reward_function.total_reward}" ) self.game.reset() state = self.game.get_sim_state() From 9a8350fd8f7b81525144ac6d18280a08b9a91cae Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 15:49:20 +0000 Subject: [PATCH 089/107] #2084: artifact the report --- .azure/azure-ci-build-pipeline.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 4c5afed8..61b4cfc3 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,7 +98,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term + pytest -v tests/unit_tests --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term displayName: 'Run tests and code coverage' - task: PublishTestResults@2 @@ -108,12 +108,15 @@ stages: testResultsFiles: 'junit/**.xml' testRunTitle: 'Publish test results' + - publish: $(System.DefaultWorkingDirectory)/**/htmlcov/ + artifact: coverage_report + - task: PublishCodeCoverageResults@1 displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' pathToSources: '$(System.DefaultWorkingDirectory)/src' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov/' additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/**/htmlcov/*.*' failIfCoverageEmpty: true From 8a4978cf9625f82ee2cacd4843a8fc6df2c56a08 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 15:59:34 +0000 Subject: [PATCH 090/107] #2084: remove 80% requirement - causes tests to fail --- .azure/azure-ci-build-pipeline.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 61b4cfc3..2dffe61a 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -108,7 +108,7 @@ stages: testResultsFiles: 'junit/**.xml' testRunTitle: 'Publish test results' - - publish: $(System.DefaultWorkingDirectory)/**/htmlcov/ + - publish: $(System.DefaultWorkingDirectory)/htmlcov/ artifact: coverage_report - task: PublishCodeCoverageResults@1 @@ -117,6 +117,6 @@ stages: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' pathToSources: '$(System.DefaultWorkingDirectory)/src' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov/' - additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/**/htmlcov/*.*' + # reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov/' + # additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/**/htmlcov/*.*' failIfCoverageEmpty: true From 88f74d9eec9a6ea703eaaea88c2cb742d620a86e Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 16:08:02 +0000 Subject: [PATCH 091/107] #2084: remove debugs and comment out the uploading of report --- .azure/azure-ci-build-pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 2dffe61a..a59c5593 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -98,7 +98,7 @@ stages: displayName: 'Perform PrimAITE Setup' - script: | - pytest -v tests/unit_tests --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term + pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term displayName: 'Run tests and code coverage' - task: PublishTestResults@2 @@ -115,7 +115,7 @@ stages: displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura - summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' + summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' pathToSources: '$(System.DefaultWorkingDirectory)/src' # reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov/' # additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/**/htmlcov/*.*' From af8401440d7d7dfc2da6dc39f3b5deb5d5c46030 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Fri, 1 Dec 2023 16:29:05 +0000 Subject: [PATCH 092/107] #2084: using v2 publish codecov --- .azure/azure-ci-build-pipeline.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index a59c5593..11d53d73 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -111,12 +111,9 @@ stages: - publish: $(System.DefaultWorkingDirectory)/htmlcov/ artifact: coverage_report - - task: PublishCodeCoverageResults@1 + - task: PublishCodeCoverageResults@2 displayName: 'Publish coverage report' inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - pathToSources: '$(System.DefaultWorkingDirectory)/src' - # reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov/' - # additionalCodeCoverageFiles: '$(System.DefaultWorkingDirectory)/**/htmlcov/*.*' failIfCoverageEmpty: true From 31c4287f469051f5763535dcc1d1f429e1be93fc Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 01:05:13 +0000 Subject: [PATCH 093/107] #2084: applying github example fix to pipeline --- .azure/azure-ci-build-pipeline.yaml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 11d53d73..be46466b 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -97,6 +97,12 @@ stages: primaite setup displayName: 'Perform PrimAITE Setup' + - task: UseDotNet@2 + displayName: 'Install dotnet dependencies' + inputs: + packageType: 'sdk' + version: '2.1.x' + - script: | pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term displayName: 'Run tests and code coverage' @@ -111,9 +117,25 @@ stages: - publish: $(System.DefaultWorkingDirectory)/htmlcov/ artifact: coverage_report + # - task: PublishCodeCoverageResults@2 + # displayName: 'Publish coverage report' + # inputs: + # codeCoverageTool: Cobertura + # summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' + # failIfCoverageEmpty: true + - task: PublishCodeCoverageResults@2 - displayName: 'Publish coverage report' + displayName: 'Install code coverage upload dependencies' + # We only want the dependencies - this azure task is borked https://github.com/microsoft/azure-pipelines-tasks/issues/17756 + # ref: https://github.com/microsoft/azure-pipelines-tasks/issues/17756#issuecomment-1585620675 + condition: eq('true', 'false') # THIS WILL NEVER RUN ONCE TASK DECLARATION IS NEEDED TO DOWNLOAD IT SOURCES inputs: - codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - failIfCoverageEmpty: true + + - task: CmdLine@2 + displayName: Publish Code Coverage + env: { 'SYSTEM_ACCESSTOKEN': $(System.AccessToken) } #access token is needed to upload report to azure pipeline tabs + inputs: + script: | + mkdir /home/vsts/work/_temp/cobertura + "$(Dotnet_Root)/dotnet"dotnet `find /home/vsts/work/_tasks/ -name CoveragePublisher.Console.dll` '$(System.DefaultWorkingDirectory)/coverage.xml' --reportDirectory /home/vsts/work/_temp/cobertura From 6ecb47f5aedb4c521e40261f8cd34821332bfb5a Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 01:19:38 +0000 Subject: [PATCH 094/107] #2084: debug coverage file --- .azure/azure-ci-build-pipeline.yaml | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index be46466b..0e150a50 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -104,7 +104,7 @@ stages: version: '2.1.x' - script: | - pytest -v tests/ --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term + pytest -v --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term displayName: 'Run tests and code coverage' - task: PublishTestResults@2 @@ -124,18 +124,18 @@ stages: # summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' # failIfCoverageEmpty: true - - task: PublishCodeCoverageResults@2 - displayName: 'Install code coverage upload dependencies' - # We only want the dependencies - this azure task is borked https://github.com/microsoft/azure-pipelines-tasks/issues/17756 - # ref: https://github.com/microsoft/azure-pipelines-tasks/issues/17756#issuecomment-1585620675 - condition: eq('true', 'false') # THIS WILL NEVER RUN ONCE TASK DECLARATION IS NEEDED TO DOWNLOAD IT SOURCES - inputs: - summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' + # - task: PublishCodeCoverageResults@2 + # displayName: 'Install code coverage upload dependencies' + # # We only want the dependencies - this azure task is borked https://github.com/microsoft/azure-pipelines-tasks/issues/17756 + # # ref: https://github.com/microsoft/azure-pipelines-tasks/issues/17756#issuecomment-1585620675 + # condition: eq('true', 'false') # THIS WILL NEVER RUN ONCE TASK DECLARATION IS NEEDED TO DOWNLOAD IT SOURCES + # inputs: + # summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - - task: CmdLine@2 - displayName: Publish Code Coverage - env: { 'SYSTEM_ACCESSTOKEN': $(System.AccessToken) } #access token is needed to upload report to azure pipeline tabs - inputs: - script: | - mkdir /home/vsts/work/_temp/cobertura - "$(Dotnet_Root)/dotnet"dotnet `find /home/vsts/work/_tasks/ -name CoveragePublisher.Console.dll` '$(System.DefaultWorkingDirectory)/coverage.xml' --reportDirectory /home/vsts/work/_temp/cobertura + # - task: CmdLine@2 + # displayName: Publish Code Coverage + # env: { 'SYSTEM_ACCESSTOKEN': $(System.AccessToken) } #access token is needed to upload report to azure pipeline tabs + # inputs: + # script: | + # mkdir /home/vsts/work/_temp/cobertura + # "$(Dotnet_Root)/dotnet" `find /home/vsts/work/_tasks/ -name CoveragePublisher.Console.dll` '$(System.DefaultWorkingDirectory)/coverage.xml' --reportDirectory /home/vsts/work/_temp/cobertura From 47287ad1eb2d326e74a2381e51bd8e1bb5a6ed15 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 13:44:39 +0000 Subject: [PATCH 095/107] #2084: fixing 0% coverage --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 0e150a50..5759e70e 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -104,7 +104,7 @@ stages: version: '2.1.x' - script: | - pytest -v --cov=src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term + pytest -v --cov=$(System.DefaultWorkingDirectory)/src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term displayName: 'Run tests and code coverage' - task: PublishTestResults@2 From 2123fbb8f4476c5365a2bc2366f13db29be64672 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 14:17:34 +0000 Subject: [PATCH 096/107] #2084: more debugging --- .azure/azure-ci-build-pipeline.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 5759e70e..36704ac0 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -104,7 +104,9 @@ stages: version: '2.1.x' - script: | - pytest -v --cov=$(System.DefaultWorkingDirectory)/src/ -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-report xml:$(System.DefaultWorkingDirectory)/coverage.xml --cov-report html:$(System.DefaultWorkingDirectory)/htmlcov --cov-report term + coverage run -m pytest tests/unit_tests + coverage xml -o coverage.xml -i + coverage html -d htmlcov -i displayName: 'Run tests and code coverage' - task: PublishTestResults@2 @@ -117,6 +119,12 @@ stages: - publish: $(System.DefaultWorkingDirectory)/htmlcov/ artifact: coverage_report + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' + # - task: PublishCodeCoverageResults@2 # displayName: 'Publish coverage report' # inputs: From e48f0a6d68d249502149c448881d77512b9abff2 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 14:41:45 +0000 Subject: [PATCH 097/107] #2084: more debugging --- .azure/azure-ci-build-pipeline.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 36704ac0..cb76b5b1 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -56,7 +56,9 @@ stages: pool: vmImage: ${{ item.img }} - condition: or( eq(variables['Build.Reason'], 'PullRequest'), ${{ item.every_time }} ) +# TODO: dont forget to undo +# condition: or( eq(variables['Build.Reason'], 'PullRequest'), ${{ item.every_time }} ) + condition: ${{ item.every_time }} steps: - task: UsePythonVersion@0 @@ -119,11 +121,11 @@ stages: - publish: $(System.DefaultWorkingDirectory)/htmlcov/ artifact: coverage_report - - task: PublishCodeCoverageResults@1 + - task: PublishCodeCoverageResults@2 inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' + # reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' # - task: PublishCodeCoverageResults@2 # displayName: 'Publish coverage report' From 7b21f390c0fcbb864d00435eb8fb5b83d55a2fa8 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 15:31:36 +0000 Subject: [PATCH 098/107] #2084: more debugging --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index cb76b5b1..3e2237d3 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -106,7 +106,7 @@ stages: version: '2.1.x' - script: | - coverage run -m pytest tests/unit_tests + coverage run -m pytest -v -o junit_family=xunit2 --junitxml=junit/test-results.xml coverage xml -o coverage.xml -i coverage html -d htmlcov -i displayName: 'Run tests and code coverage' From 060a46e251a23c73f9b6280eef696250ec448873 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 18:44:27 +0000 Subject: [PATCH 099/107] #2084: more debugging --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 3e2237d3..77cae6fc 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -106,7 +106,7 @@ stages: version: '2.1.x' - script: | - coverage run -m pytest -v -o junit_family=xunit2 --junitxml=junit/test-results.xml + coverage run -m --source=primaite pytest -v -o junit_family=xunit2 --junitxml=junit/test-results.xml coverage xml -o coverage.xml -i coverage html -d htmlcov -i displayName: 'Run tests and code coverage' From 53f43dde0d623cf2c4bdae2ba21531e877993cce Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 19:07:10 +0000 Subject: [PATCH 100/107] #2084: cleaning up --- .azure/azure-ci-build-pipeline.yaml | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 77cae6fc..239369f5 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -56,9 +56,7 @@ stages: pool: vmImage: ${{ item.img }} -# TODO: dont forget to undo -# condition: or( eq(variables['Build.Reason'], 'PullRequest'), ${{ item.every_time }} ) - condition: ${{ item.every_time }} + condition: or( eq(variables['Build.Reason'], 'PullRequest'), ${{ item.every_time }} ) steps: - task: UsePythonVersion@0 @@ -122,30 +120,7 @@ stages: artifact: coverage_report - task: PublishCodeCoverageResults@2 + condition: ${{ item.every_time }} # should only be run once inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - # reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' - - # - task: PublishCodeCoverageResults@2 - # displayName: 'Publish coverage report' - # inputs: - # codeCoverageTool: Cobertura - # summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - # failIfCoverageEmpty: true - - # - task: PublishCodeCoverageResults@2 - # displayName: 'Install code coverage upload dependencies' - # # We only want the dependencies - this azure task is borked https://github.com/microsoft/azure-pipelines-tasks/issues/17756 - # # ref: https://github.com/microsoft/azure-pipelines-tasks/issues/17756#issuecomment-1585620675 - # condition: eq('true', 'false') # THIS WILL NEVER RUN ONCE TASK DECLARATION IS NEEDED TO DOWNLOAD IT SOURCES - # inputs: - # summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml' - - # - task: CmdLine@2 - # displayName: Publish Code Coverage - # env: { 'SYSTEM_ACCESSTOKEN': $(System.AccessToken) } #access token is needed to upload report to azure pipeline tabs - # inputs: - # script: | - # mkdir /home/vsts/work/_temp/cobertura - # "$(Dotnet_Root)/dotnet" `find /home/vsts/work/_tasks/ -name CoveragePublisher.Console.dll` '$(System.DefaultWorkingDirectory)/coverage.xml' --reportDirectory /home/vsts/work/_temp/cobertura From 1cc00203816b029a3594359df3126f237698a7f7 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 19:38:45 +0000 Subject: [PATCH 101/107] #2084: only upload copy of html report once --- .azure/azure-ci-build-pipeline.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 239369f5..221bedd5 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -117,9 +117,12 @@ stages: testRunTitle: 'Publish test results' - publish: $(System.DefaultWorkingDirectory)/htmlcov/ + # publish the html report - so we can debug the coverage if needed + condition: ${{ item.every_time }} # should only be run once artifact: coverage_report - task: PublishCodeCoverageResults@2 + # publish the code coverage so it can be viewed in the run coverage page condition: ${{ item.every_time }} # should only be run once inputs: codeCoverageTool: Cobertura From 1d5337153ba050b9a2900e75d4e465ae6b41d12d Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Sat, 2 Dec 2023 19:46:04 +0000 Subject: [PATCH 102/107] #2084: fix pr autocancel --- .azure/azure-ci-build-pipeline.yaml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 221bedd5..8a944c7f 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -7,17 +7,8 @@ trigger: - release/* pr: - autoCancel: true # automatically cancel PR if new push made - drafts: true # get triggered when doing drafts - branches: - include: - - main - - dev - - feature/* - - hotfix/* - - bugfix/* - - release/* - + autoCancel: true + drafts: false parameters: # https://stackoverflow.com/a/70046417 - name: matrix From 534d4f96f3b89b8ff6e5ed4930b93293108f9b27 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 4 Dec 2023 08:58:03 +0000 Subject: [PATCH 103/107] #2084: add coverage fail condition --- .azure/azure-ci-build-pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 8a944c7f..26559889 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -95,7 +95,7 @@ stages: version: '2.1.x' - script: | - coverage run -m --source=primaite pytest -v -o junit_family=xunit2 --junitxml=junit/test-results.xml + coverage run -m --source=primaite pytest -v -o junit_family=xunit2 --junitxml=junit/test-results.xml --cov-fail-under=80 coverage xml -o coverage.xml -i coverage html -d htmlcov -i displayName: 'Run tests and code coverage' From 8f063aa339cdc7567e4be36b9e23a2fadb0d99e4 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 4 Dec 2023 09:07:42 +0000 Subject: [PATCH 104/107] #2084: apply previous PR suggestions --- .../_primaite/_simulator/_network/test_container.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/_primaite/_simulator/_network/test_container.py b/tests/unit_tests/_primaite/_simulator/_network/test_container.py index 021d6777..e348838e 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/test_container.py +++ b/tests/unit_tests/_primaite/_simulator/_network/test_container.py @@ -53,7 +53,7 @@ def test_reset_network(network): server_1.power_off() assert server_1.operating_state is NodeOperatingState.SHUTTING_DOWN - assert network.describe_state() is not state_before + assert network.describe_state() != state_before network.reset_component_for_episode(episode=1) @@ -79,12 +79,16 @@ def test_apply_timestep_to_nodes(network): assert client_1.operating_state is NodeOperatingState.ON client_1.power_off() + assert client_1.operating_state is NodeOperatingState.SHUTTING_DOWN for i in range(client_1.shut_down_duration + 1): network.apply_timestep(timestep=i) assert client_1.operating_state is NodeOperatingState.OFF + network.apply_timestep(client_1.shut_down_duration + 2) + assert client_1.operating_state is NodeOperatingState.OFF + def test_removing_node_that_does_not_exist(network): """Node that does not exist on network should not affect existing nodes.""" From de5fead9a475ccfb99d775dfa3858cf871263206 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Dec 2023 09:14:20 +0000 Subject: [PATCH 105/107] Add docpage for config --- docs/source/config.rst | 79 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 0ce8b547..34147578 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,13 +1,80 @@ Primaite v3 config ****************** -PrimAITE uses a single configuration file to define a cybersecurity scenario. This includes the computer network and multiple agents. There are three main sections: training_config, game, and simulation. +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. -The simulation section describes the simulated network environment with which the agetns interact. +Configurable items +================== -The game section describes the agents and their capabilities. Each agent has a unique type and is associated with a team (GREEN, RED, or BLUE). Each agent has a configurable observation space, action space, and reward function. +``training_config`` +------------------- +This section allows selecting which training framework and algorithm to use, and set some training hyperparameters. -The training_config section describes the training parameters for the learning agents. This includes the number of episodes, the number of steps per episode, and the number of steps before the agents start learning. The training_config section also describes the learning algorithm used by the agents. The learning algorithm is specified by the name of the algorithm and the hyperparameters for the algorithm. The hyperparameters are specific to each algorithm and are described in the documentation for each algorithm. +``io_settings`` +--------------- +This section configures how the ``PrimaiteSession`` saves data. -.. only:: comment - This needs a bit of refactoring so I haven't written extensive documentation about the config yet. +``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. + +``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. + * ``nics`` (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 From ba3d37316b93e7c5815054db9bdd291f593511b3 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Dec 2023 09:21:47 +0000 Subject: [PATCH 106/107] Apply suggestions from review --- src/primaite/game/agent/rewards.py | 2 +- src/primaite/notebooks/training_example_ray_multi_agent.ipynb | 2 +- src/primaite/session/session.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primaite/game/agent/rewards.py b/src/primaite/game/agent/rewards.py index b7a5e9be..9b3dfb80 100644 --- a/src/primaite/game/agent/rewards.py +++ b/src/primaite/game/agent/rewards.py @@ -238,7 +238,7 @@ class RewardFunction: """Initialise the reward function object.""" self.reward_components: List[Tuple[AbstractReward, float]] = [] "attribute reward_components keeps track of reward components and the weights assigned to each." - self.current_reward: float + self.current_reward: float = 0.0 self.total_reward: float = 0.0 def regsiter_component(self, component: AbstractReward, weight: float = 1.0) -> None: diff --git a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb index cd9ecfe7..0d4b6d0e 100644 --- a/src/primaite/notebooks/training_example_ray_multi_agent.ipynb +++ b/src/primaite/notebooks/training_example_ray_multi_agent.ipynb @@ -83,7 +83,7 @@ "tune.Tuner(\n", " \"PPO\",\n", " run_config=air.RunConfig(\n", - " stop={\"timesteps_total\": 511},\n", + " stop={\"timesteps_total\": 512},\n", " ),\n", " param_space=config\n", ").fit()" diff --git a/src/primaite/session/session.py b/src/primaite/session/session.py index 3c8b40bd..ef462d83 100644 --- a/src/primaite/session/session.py +++ b/src/primaite/session/session.py @@ -62,7 +62,7 @@ class PrimaiteSession: def start_session(self) -> None: """Commence the training/eval session.""" - print("Staring Primaite Session") + print("Starting Primaite Session") self.mode = SessionMode.TRAIN n_learn_episodes = self.training_options.n_learn_episodes n_eval_episodes = self.training_options.n_eval_episodes From 9fa6f0b7aba2cb7432b46db9d95c144199eb76c1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 4 Dec 2023 10:16:29 +0000 Subject: [PATCH 107/107] Formatting improvements in cfg doc page --- docs/source/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 34147578..f4452c7e 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -25,7 +25,7 @@ Agents can be scripted (deterministic and stochastic), or controlled by a reinfo **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. +**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. @@ -40,7 +40,7 @@ The action space is configured to be made up of individual action types. Once co 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. + * ``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.