From a0cfe8cdfa740be50b61a95034cd8eb2fc5e0e9d Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Mon, 29 Jul 2024 08:52:16 +0100 Subject: [PATCH 01/10] #2778 - fixed the mis-merge that was trying to call the old latex function instead of the new md function. removed the old threshold leftover stuff in the report too --- benchmark/primaite_benchmark.py | 4 ++-- benchmark/report.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/benchmark/primaite_benchmark.py b/benchmark/primaite_benchmark.py index 27e25a0c..542f46d7 100644 --- a/benchmark/primaite_benchmark.py +++ b/benchmark/primaite_benchmark.py @@ -5,12 +5,12 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, Final, Tuple -from report import build_benchmark_latex_report from stable_baselines3 import PPO import primaite from benchmark import BenchmarkPrimaiteGymEnv from primaite.config.load import data_manipulation_config_path +from report import build_benchmark_md_report _LOGGER = primaite.getLogger(__name__) @@ -188,7 +188,7 @@ def run( with open(_SESSION_METADATA_ROOT / f"{i}.json", "r") as file: session_metadata_dict[i] = json.load(file) # generate report - build_benchmark_latex_report( + build_benchmark_md_report( benchmark_start_time=benchmark_start_time, session_metadata=session_metadata_dict, config_path=data_manipulation_config_path(), diff --git a/benchmark/report.py b/benchmark/report.py index 5eaaab9f..e1ff46b9 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -234,10 +234,7 @@ def _plot_av_s_per_100_steps_10_nodes( """ major_v = primaite.__version__.split(".")[0] title = f"Performance of Minor and Bugfix Releases for Major Version {major_v}" - subtitle = ( - f"Average Training Time per 100 Steps on 10 Nodes " - f"(target: <= {PLOT_CONFIG['av_s_per_100_steps_10_nodes_benchmark_threshold']} seconds)" - ) + subtitle = "Average Training Time per 100 Steps on 10 Nodes " title = f"{title}
{subtitle}" layout = go.Layout( @@ -250,10 +247,6 @@ def _plot_av_s_per_100_steps_10_nodes( versions = sorted(list(version_times_dict.keys())) times = [version_times_dict[version] for version in versions] - av_s_per_100_steps_10_nodes_benchmark_threshold = PLOT_CONFIG["av_s_per_100_steps_10_nodes_benchmark_threshold"] - - # Calculate the appropriate maximum y-axis value - max_y_axis_value = max(max(times), av_s_per_100_steps_10_nodes_benchmark_threshold) + 1 fig.add_trace( go.Bar( @@ -267,7 +260,6 @@ def _plot_av_s_per_100_steps_10_nodes( fig.update_layout( xaxis_title="PrimAITE Version", yaxis_title="Avg Time per 100 Steps on 10 Nodes (seconds)", - yaxis=dict(range=[0, max_y_axis_value]), title=title, ) From 8af7fc0ecd94e5f75805c059013c1aa5cfcba43d Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Mon, 29 Jul 2024 09:31:50 +0100 Subject: [PATCH 02/10] #2778 - ran pre-commit --- benchmark/primaite_benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/primaite_benchmark.py b/benchmark/primaite_benchmark.py index 542f46d7..0e6c2acc 100644 --- a/benchmark/primaite_benchmark.py +++ b/benchmark/primaite_benchmark.py @@ -5,12 +5,12 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, Final, Tuple +from report import build_benchmark_md_report from stable_baselines3 import PPO import primaite from benchmark import BenchmarkPrimaiteGymEnv from primaite.config.load import data_manipulation_config_path -from report import build_benchmark_md_report _LOGGER = primaite.getLogger(__name__) From 3d13669671403dbda1a60c07a65aab9f1e755328 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 29 Jul 2024 15:12:24 +0100 Subject: [PATCH 03/10] #2735: fixes to broken items --- src/primaite/simulator/network/hardware/base.py | 17 ++++++++++------- .../network/hardware/nodes/network/switch.py | 3 +++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 0a561707..08f14b7e 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -850,7 +850,6 @@ class UserManager(Service): kwargs["port"] = Port.NONE kwargs["protocol"] = IPProtocol.NONE super().__init__(**kwargs) - self._request_manager = None self.start() @@ -1499,20 +1498,28 @@ class Node(SimComponent): super().__init__(**kwargs) self.session_manager.node = self self.session_manager.software_manager = self.software_manager + self.software_manager.install(UserSessionManager, node=self) + self._request_manager.add_request( + "sessions", RequestType(func=self.user_session_manager._request_manager) + ) # noqa + self.software_manager.install(UserManager) + self._request_manager.add_request("accounts", RequestType(func=self.user_manager._request_manager)) # noqa + self.user_manager.add_user(username="admin", password="admin", is_admin=True, bypass_can_perform_action=True) + self._install_system_software() @property def user_manager(self) -> UserManager: """The Nodes User Manager.""" - return self.software_manager.software["UserManager"] # noqa + return self.software_manager.software.get("UserManager") # noqa @property def user_session_manager(self) -> UserSessionManager: """The Nodes User Session Manager.""" - return self.software_manager.software["UserSessionManager"] # noqa + return self.software_manager.software.get("UserSessionManager") # noqa def local_login(self, username: str, password: str) -> Optional[str]: """ @@ -1735,10 +1742,6 @@ class Node(SimComponent): self._application_manager.add_request(name="install", request_type=RequestType(func=_install_application)) self._application_manager.add_request(name="uninstall", request_type=RequestType(func=_uninstall_application)) - rm.add_request("accounts", RequestType(func=self.user_manager._request_manager)) # noqa - - rm.add_request("sessions", RequestType(func=self.user_session_manager._request_manager)) # noqa - return rm def describe_state(self) -> Dict: diff --git a/src/primaite/simulator/network/hardware/nodes/network/switch.py b/src/primaite/simulator/network/hardware/nodes/network/switch.py index 1a7da2e7..4324ac94 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/switch.py +++ b/src/primaite/simulator/network/hardware/nodes/network/switch.py @@ -108,6 +108,9 @@ class Switch(NetworkNode): for i in range(1, self.num_ports + 1): self.connect_nic(SwitchPort()) + def _install_system_software(self): + pass + def show(self, markdown: bool = False): """ Prints a table of the SwitchPorts on the Switch. From 0fad61eaea2d1d39c94fe5241125292c5686fc71 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 29 Jul 2024 15:15:15 +0100 Subject: [PATCH 04/10] #2735: pipeline build fail if test fails --- .azure/azure-ci-build-pipeline.yaml | 4 +--- run_test_and_coverage.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 run_test_and_coverage.py diff --git a/.azure/azure-ci-build-pipeline.yaml b/.azure/azure-ci-build-pipeline.yaml index 01111290..2375a391 100644 --- a/.azure/azure-ci-build-pipeline.yaml +++ b/.azure/azure-ci-build-pipeline.yaml @@ -102,9 +102,7 @@ stages: version: '2.1.x' - script: | - 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 + python run_test_and_coverage.py displayName: 'Run tests and code coverage' # Run the notebooks diff --git a/run_test_and_coverage.py b/run_test_and_coverage.py new file mode 100644 index 00000000..3bd9072d --- /dev/null +++ b/run_test_and_coverage.py @@ -0,0 +1,22 @@ +# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK +import subprocess +import sys +from typing import Any + + +def run_command(command: Any): + """Runs a command and returns the exit code.""" + result = subprocess.run(command, shell=True) + if result.returncode != 0: + sys.exit(result.returncode) + + +# Run pytest with coverage +run_command( + "coverage run -m --source=primaite pytest -v -o junit_family=xunit2 " + "--junitxml=junit/test-results.xml --cov-fail-under=80" +) + +# Generate coverage reports if tests passed +run_command("coverage xml -o coverage.xml -i") +run_command("coverage html -d htmlcov -i") From c984d695cca3b2ac53d8ce7eff3fcce34aa43b94 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Mon, 29 Jul 2024 23:03:26 +0100 Subject: [PATCH 05/10] #2735: use ray version 2.32 until 2.33 is fixed --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e919604..01be8d52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ license-files = ["LICENSE"] [project.optional-dependencies] rl = [ - "ray[rllib] >= 2.20.0, < 3", + "ray[rllib] == 2.32.0, < 3", "tensorflow==2.12.0", "stable-baselines3[extra]==2.1.0", "sb3-contrib==2.1.0", From 2e1d6222286a885ae48c498c8a7d691b23a7de32 Mon Sep 17 00:00:00 2001 From: Chris McCarthy Date: Tue, 30 Jul 2024 09:57:48 +0100 Subject: [PATCH 06/10] #2778 - pinned Ray version to <2.33 until they fix their bug --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e919604..c9b7c062 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ license-files = ["LICENSE"] [project.optional-dependencies] rl = [ - "ray[rllib] >= 2.20.0, < 3", + "ray[rllib] >= 2.20.0, <2.33", "tensorflow==2.12.0", "stable-baselines3[extra]==2.1.0", "sb3-contrib==2.1.0", From 5e3a16999952aab47983f99175937da94a577826 Mon Sep 17 00:00:00 2001 From: Czar Echavez Date: Tue, 30 Jul 2024 12:48:11 +0100 Subject: [PATCH 07/10] #2735: add usermanager and usersessionmanager into describe_state --- src/primaite/simulator/network/hardware/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 08f14b7e..05e52e32 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1767,6 +1767,8 @@ class Node(SimComponent): "services": {svc.name: svc.describe_state() for svc in self.services.values()}, "process": {proc.name: proc.describe_state() for proc in self.processes.values()}, "revealed_to_red": self.revealed_to_red, + "user_manager": self.user_manager.describe_state(), + "user_session_manager": self.user_session_manager.describe_state(), } ) return state From 2abd1969fe618160df7e77b2899c7e0ab0c4f5bd Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 31 Jul 2024 16:41:59 +0100 Subject: [PATCH 08/10] #2800 - Consolidate software install and uninstall to a single method --- .../simulator/network/hardware/base.py | 68 ------------------ .../simulator/system/core/software_manager.py | 70 ++++++++++--------- tests/conftest.py | 12 ++-- .../test_action_integration.py | 3 +- .../system/test_service_on_node.py | 4 +- .../test_simulation/test_request_response.py | 6 +- .../_network/_hardware/test_node_actions.py | 17 +++-- 7 files changed, 61 insertions(+), 119 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 15c44821..fd3f369d 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -1455,74 +1455,6 @@ class Node(SimComponent): else: return - def install_service(self, service: Service) -> None: - """ - Install a service on this node. - - :param service: Service instance that has not been installed on any node yet. - :type service: Service - """ - if service in self: - _LOGGER.warning(f"Can't add service {service.name} to node {self.hostname}. It's already installed.") - return - self.services[service.uuid] = service - service.parent = self - service.install() # Perform any additional setup, such as creating files for this service on the node. - self.sys_log.info(f"Installed service {service.name}") - _LOGGER.debug(f"Added service {service.name} to node {self.hostname}") - self._service_request_manager.add_request(service.name, RequestType(func=service._request_manager)) - - def uninstall_service(self, service: Service) -> None: - """ - Uninstall and completely remove service from this node. - - :param service: Service object that is currently associated with this node. - :type service: Service - """ - if service not in self: - _LOGGER.warning(f"Can't remove service {service.name} from node {self.hostname}. It's not installed.") - return - service.uninstall() # Perform additional teardown, such as removing files or restarting the machine. - self.services.pop(service.uuid) - service.parent = None - self.sys_log.info(f"Uninstalled service {service.name}") - self._service_request_manager.remove_request(service.name) - - def install_application(self, application: Application) -> None: - """ - Install an application on this node. - - :param application: Application instance that has not been installed on any node yet. - :type application: Application - """ - if application in self: - _LOGGER.warning( - f"Can't add application {application.name} to node {self.hostname}. It's already installed." - ) - return - self.applications[application.uuid] = application - application.parent = self - self.sys_log.info(f"Installed application {application.name}") - _LOGGER.debug(f"Added application {application.name} to node {self.hostname}") - self._application_request_manager.add_request(application.name, RequestType(func=application._request_manager)) - - def uninstall_application(self, application: Application) -> None: - """ - Uninstall and completely remove application from this node. - - :param application: Application object that is currently associated with this node. - :type application: Application - """ - if application not in self: - _LOGGER.warning( - f"Can't remove application {application.name} from node {self.hostname}. It's not installed." - ) - return - self.applications.pop(application.uuid) - application.parent = None - self.sys_log.info(f"Uninstalled application {application.name}") - self._application_request_manager.remove_request(application.name) - def _shut_down_actions(self): """Actions to perform when the node is shut down.""" # Turn off all the services in the node diff --git a/src/primaite/simulator/system/core/software_manager.py b/src/primaite/simulator/system/core/software_manager.py index e2266c2d..9c4d7cf6 100644 --- a/src/primaite/simulator/system/core/software_manager.py +++ b/src/primaite/simulator/system/core/software_manager.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union from prettytable import MARKDOWN, PrettyTable +from primaite.simulator.core import RequestType from primaite.simulator.file_system.file_system import FileSystem from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.network.transmission.network_layer import IPProtocol @@ -20,9 +21,7 @@ if TYPE_CHECKING: from primaite.simulator.system.services.arp.arp import ARP from primaite.simulator.system.services.icmp.icmp import ICMP -from typing import Type, TypeVar - -IOSoftwareClass = TypeVar("IOSoftwareClass", bound=IOSoftware) +from typing import Type class SoftwareManager: @@ -51,7 +50,7 @@ class SoftwareManager: self.node = parent_node self.session_manager = session_manager self.software: Dict[str, Union[Service, Application]] = {} - self._software_class_to_name_map: Dict[Type[IOSoftwareClass], str] = {} + self._software_class_to_name_map: Dict[Type[IOSoftware], str] = {} self.port_protocol_mapping: Dict[Tuple[Port, IPProtocol], Union[Service, Application]] = {} self.sys_log: SysLog = sys_log self.file_system: FileSystem = file_system @@ -104,33 +103,34 @@ class SoftwareManager: return True return False - def install(self, software_class: Type[IOSoftwareClass]): + def install(self, software_class: Type[IOSoftware]): """ Install an Application or Service. :param software_class: The software class. """ - # TODO: Software manager and node itself both have an install method. Need to refactor to have more logical - # separation of concerns. if software_class in self._software_class_to_name_map: self.sys_log.warning(f"Cannot install {software_class} as it is already installed") return software = software_class( software_manager=self, sys_log=self.sys_log, file_system=self.file_system, dns_server=self.dns_server ) + software.parent = self.node if isinstance(software, Application): - software.install() + self.node.applications[software.uuid] = software + self.node._application_request_manager.add_request( + software.name, RequestType(func=software._request_manager) + ) + elif isinstance(software, Service): + self.node.services[software.uuid] = software + self.node._service_request_manager.add_request(software.name, RequestType(func=software._request_manager)) + software.install() software.software_manager = self self.software[software.name] = software self.port_protocol_mapping[(software.port, software.protocol)] = software if isinstance(software, Application): software.operating_state = ApplicationOperatingState.CLOSED - - # add the software to the node's registry after it has been fully initialized - if isinstance(software, Service): - self.node.install_service(software) - elif isinstance(software, Application): - self.node.install_application(software) + self.node.sys_log.info(f"Installed {software.name}") def uninstall(self, software_name: str): """ @@ -138,25 +138,31 @@ class SoftwareManager: :param software_name: The software name. """ - if software_name in self.software: - self.software[software_name].uninstall() - software = self.software.pop(software_name) # noqa - if isinstance(software, Application): - self.node.uninstall_application(software) - elif isinstance(software, Service): - self.node.uninstall_service(software) - for key, value in self.port_protocol_mapping.items(): - if value.name == software_name: - self.port_protocol_mapping.pop(key) - break - for key, value in self._software_class_to_name_map.items(): - if value == software_name: - self._software_class_to_name_map.pop(key) - break - del software - self.sys_log.info(f"Uninstalled {software_name}") + if software_name not in self.software: + self.sys_log.error(f"Cannot uninstall {software_name} as it is not installed") return - self.sys_log.error(f"Cannot uninstall {software_name} as it is not installed") + + self.software[software_name].uninstall() + software = self.software.pop(software_name) # noqa + if isinstance(software, Application): + self.node.applications.pop(software.uuid) + self.node._application_request_manager.remove_request(software.name) + elif isinstance(software, Service): + self.node.services.pop(software.uuid) + software.uninstall() + self.node._service_request_manager.remove_request(software.name) + software.parent = None + for key, value in self.port_protocol_mapping.items(): + if value.name == software_name: + self.port_protocol_mapping.pop(key) + break + for key, value in self._software_class_to_name_map.items(): + if value == software_name: + self._software_class_to_name_map.pop(key) + break + del software + self.sys_log.info(f"Uninstalled {software_name}") + return def send_internal_payload(self, target_software: str, payload: Any): """ diff --git a/tests/conftest.py b/tests/conftest.py index 54519e2b..ca704461 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,14 +37,14 @@ ACTION_SPACE_NODE_ACTION_VALUES = 1 _LOGGER = getLogger(__name__) -class TestService(Service): +class DummyService(Service): """Test Service class""" def describe_state(self) -> Dict: return super().describe_state() def __init__(self, **kwargs): - kwargs["name"] = "TestService" + kwargs["name"] = "DummyService" kwargs["port"] = Port.HTTP kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) @@ -75,15 +75,15 @@ def uc2_network() -> Network: @pytest.fixture(scope="function") -def service(file_system) -> TestService: - return TestService( - name="TestService", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="test_service") +def service(file_system) -> DummyService: + return DummyService( + name="DummyService", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="dummy_service") ) @pytest.fixture(scope="function") def service_class(): - return TestService + return DummyService @pytest.fixture(scope="function") diff --git a/tests/integration_tests/component_creation/test_action_integration.py b/tests/integration_tests/component_creation/test_action_integration.py index a6f09436..7bdc80fc 100644 --- a/tests/integration_tests/component_creation/test_action_integration.py +++ b/tests/integration_tests/component_creation/test_action_integration.py @@ -22,8 +22,7 @@ def test_passing_actions_down(monkeypatch) -> None: for n in [pc1, pc2, srv, s1]: sim.network.add_node(n) - database_service = DatabaseService(file_system=srv.file_system) - srv.install_service(database_service) + srv.software_manager.install(DatabaseService) downloads_folder = pc1.file_system.create_folder("downloads") pc1.file_system.create_file("bermuda_triangle.png", folder_name="downloads") diff --git a/tests/integration_tests/system/test_service_on_node.py b/tests/integration_tests/system/test_service_on_node.py index 15dbaf1d..cf9728ce 100644 --- a/tests/integration_tests/system/test_service_on_node.py +++ b/tests/integration_tests/system/test_service_on_node.py @@ -23,7 +23,7 @@ def populated_node( server.power_on() server.software_manager.install(service_class) - service = server.software_manager.software.get("TestService") + service = server.software_manager.software.get("DummyService") service.start() return server, service @@ -42,7 +42,7 @@ def test_service_on_offline_node(service_class): computer.power_on() computer.software_manager.install(service_class) - service: Service = computer.software_manager.software.get("TestService") + service: Service = computer.software_manager.software.get("DummyService") computer.power_off() diff --git a/tests/integration_tests/test_simulation/test_request_response.py b/tests/integration_tests/test_simulation/test_request_response.py index a9f0b58d..95634cf1 100644 --- a/tests/integration_tests/test_simulation/test_request_response.py +++ b/tests/integration_tests/test_simulation/test_request_response.py @@ -13,7 +13,7 @@ from primaite.simulator.network.hardware.node_operating_state import NodeOperati from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.transmission.transport_layer import Port -from tests.conftest import DummyApplication, TestService +from tests.conftest import DummyApplication, DummyService def test_successful_node_file_system_creation_request(example_network): @@ -61,7 +61,7 @@ def test_successful_application_requests(example_network): def test_successful_service_requests(example_network): net = example_network server_1 = net.get_node_by_hostname("server_1") - server_1.software_manager.install(TestService) + server_1.software_manager.install(DummyService) # Careful: the order here is important, for example we cannot run "stop" unless we run "start" first for verb in [ @@ -77,7 +77,7 @@ def test_successful_service_requests(example_network): "scan", "fix", ]: - resp_1 = net.apply_request(["node", "server_1", "service", "TestService", verb]) + resp_1 = net.apply_request(["node", "server_1", "service", "DummyService", verb]) assert resp_1 == RequestResponse(status="success", data={}) server_1.apply_timestep(timestep=1) server_1.apply_timestep(timestep=1) diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index 9b37ac80..44c5c781 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -7,6 +7,7 @@ from primaite.simulator.file_system.folder import Folder from primaite.simulator.network.hardware.base import Node, NodeOperatingState from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.system.software import SoftwareHealthState +from tests.conftest import DummyApplication, DummyService @pytest.fixture @@ -47,7 +48,7 @@ def test_node_shutdown(node): assert node.operating_state == NodeOperatingState.OFF -def test_node_os_scan(node, service, application): +def test_node_os_scan(node): """Test OS Scanning.""" node.operating_state = NodeOperatingState.ON @@ -55,13 +56,15 @@ def test_node_os_scan(node, service, application): # TODO implement processes # add services to node + node.software_manager.install(DummyService) + service = node.software_manager.software.get("DummyService") service.set_health_state(SoftwareHealthState.COMPROMISED) - node.install_service(service=service) assert service.health_state_visible == SoftwareHealthState.UNUSED # add application to node + node.software_manager.install(DummyApplication) + application = node.software_manager.software.get("DummyApplication") application.set_health_state(SoftwareHealthState.COMPROMISED) - node.install_application(application=application) assert application.health_state_visible == SoftwareHealthState.UNUSED # add folder and file to node @@ -91,7 +94,7 @@ def test_node_os_scan(node, service, application): assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT -def test_node_red_scan(node, service, application): +def test_node_red_scan(node): """Test revealing to red""" node.operating_state = NodeOperatingState.ON @@ -99,12 +102,14 @@ def test_node_red_scan(node, service, application): # TODO implement processes # add services to node - node.install_service(service=service) + node.software_manager.install(DummyService) + service = node.software_manager.software.get("DummyService") assert service.revealed_to_red is False # add application to node + node.software_manager.install(DummyApplication) + application = node.software_manager.software.get("DummyApplication") application.set_health_state(SoftwareHealthState.COMPROMISED) - node.install_application(application=application) assert application.revealed_to_red is False # add folder and file to node From 2648614f97d2424c31e4fc1c208ebe0ce12dbd69 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 31 Jul 2024 16:44:25 +0100 Subject: [PATCH 09/10] 2800 update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 515be435..cebc2569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Transmission Feasibility Check**: Updated `_can_transmit` function in `Link` to account for current load and total bandwidth capacity, ensuring transmissions do not exceed limits. - **Frame Size Details**: Frame `size` attribute now includes both core size and payload size in bytes. - **Transmission Blocking**: Enhanced `AirSpace` logic to block transmissions that would exceed the available capacity. +- **Software (un)install refactored**: Removed the install/uninstall methods in the node class and made the software manager install/uninstall handle all of their functionality. ### Fixed From 78ad95fcef835b2b62f03a3ab724cf564a2e400f Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 1 Aug 2024 13:58:35 +0100 Subject: [PATCH 10/10] #2735 - fix up node request manager and system software --- .../simulator/network/hardware/base.py | 36 +++++++++---------- .../network/hardware/nodes/host/host_node.py | 22 +++++------- .../network/hardware/nodes/network/router.py | 10 ++++-- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index cbe8db64..d2aa4604 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -6,7 +6,7 @@ import secrets from abc import ABC, abstractmethod from ipaddress import IPv4Address, IPv4Network from pathlib import Path -from typing import Any, Dict, List, Optional, TypeVar, Union +from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union from prettytable import MARKDOWN, PrettyTable from pydantic import BaseModel, Field, validate_call @@ -39,7 +39,7 @@ from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.processes.process import Process from primaite.simulator.system.services.service import Service -from primaite.simulator.system.software import IOSoftware +from primaite.simulator.system.software import IOSoftware, Software from primaite.utils.converters import convert_dict_enum_keys_to_enum_values from primaite.utils.validators import IPV4Address @@ -897,6 +897,10 @@ class UserManager(Service): table.add_row([user.username, user.is_admin, user.disabled]) print(table.get_string(sortby="Username")) + def install(self) -> None: + """Setup default user during first-time installation.""" + self.add_user(username="admin", password="admin", is_admin=True, bypass_can_perform_action=True) + def _is_last_admin(self, username: str) -> bool: return username in self.admins and len(self.admins) == 1 @@ -1100,9 +1104,6 @@ class UserSessionManager(Service): This class handles authentication, session management, and session timeouts for users interacting with the Node. """ - node: Node - """The node associated with this UserSessionManager.""" - local_session: Optional[UserSession] = None """The current local user session, if any.""" @@ -1183,7 +1184,7 @@ class UserSessionManager(Service): if markdown: table.set_style(MARKDOWN) table.align = "l" - table.title = f"{self.node.hostname} User Sessions" + table.title = f"{self.parent.hostname} User Sessions" def _add_session_to_table(user_session: UserSession): """ @@ -1472,6 +1473,9 @@ class Node(SimComponent): red_scan_countdown: int = 0 "Time steps until reveal to red scan is complete." + SYSTEM_SOFTWARE: ClassVar[Dict[str, Type[Software]]] = {} + "Base system software that must be preinstalled." + def __init__(self, **kwargs): """ Initialize the Node with various components and managers. @@ -1496,21 +1500,10 @@ class Node(SimComponent): dns_server=kwargs.get("dns_server"), ) super().__init__(**kwargs) + self._install_system_software() self.session_manager.node = self self.session_manager.software_manager = self.software_manager - self.software_manager.install(UserSessionManager, node=self) - self._request_manager.add_request( - "sessions", RequestType(func=self.user_session_manager._request_manager) - ) # noqa - - self.software_manager.install(UserManager) - self._request_manager.add_request("accounts", RequestType(func=self.user_manager._request_manager)) # noqa - - self.user_manager.add_user(username="admin", password="admin", is_admin=True, bypass_can_perform_action=True) - - self._install_system_software() - @property def user_manager(self) -> UserManager: """The Nodes User Manager.""" @@ -1767,8 +1760,6 @@ class Node(SimComponent): "services": {svc.name: svc.describe_state() for svc in self.services.values()}, "process": {proc.name: proc.describe_state() for proc in self.processes.values()}, "revealed_to_red": self.revealed_to_red, - "user_manager": self.user_manager.describe_state(), - "user_session_manager": self.user_session_manager.describe_state(), } ) return state @@ -2134,6 +2125,11 @@ class Node(SimComponent): # for process_id in self.processes: # self.processes[process_id] + def _install_system_software(self) -> None: + """Preinstall required software.""" + for _, software_class in self.SYSTEM_SOFTWARE.items(): + self.software_manager.install(software_class) + def __contains__(self, item: Any) -> bool: if isinstance(item, Service): return item.uuid in self.services diff --git a/src/primaite/simulator/network/hardware/nodes/host/host_node.py b/src/primaite/simulator/network/hardware/nodes/host/host_node.py index aac57e95..22c50bef 100644 --- a/src/primaite/simulator/network/hardware/nodes/host/host_node.py +++ b/src/primaite/simulator/network/hardware/nodes/host/host_node.py @@ -5,7 +5,13 @@ from ipaddress import IPv4Address from typing import Any, ClassVar, Dict, Optional from primaite import getLogger -from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, Link, Node +from primaite.simulator.network.hardware.base import ( + IPWiredNetworkInterface, + Link, + Node, + UserManager, + UserSessionManager, +) from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.transmission.data_link_layer import Frame from primaite.simulator.system.applications.application import ApplicationOperatingState @@ -306,8 +312,8 @@ class HostNode(Node): "NTPClient": NTPClient, "WebBrowser": WebBrowser, "NMAP": NMAP, - # "UserSessionManager": UserSessionManager, - # "UserManager": UserManager, + "UserSessionManager": UserSessionManager, + "UserManager": UserManager, } """List of system software that is automatically installed on nodes.""" @@ -340,16 +346,6 @@ class HostNode(Node): """ return self.software_manager.software.get("ARP") - def _install_system_software(self): - """ - Installs the system software and network services typically found on an operating system. - - This method equips the host with essential network services and applications, preparing it for various - network-related tasks and operations. - """ - for _, software_class in self.SYSTEM_SOFTWARE.items(): - self.software_manager.install(software_class) - def default_gateway_hello(self): """ Sends a hello message to the default gateway to establish connectivity and resolve the gateway's MAC address. diff --git a/src/primaite/simulator/network/hardware/nodes/network/router.py b/src/primaite/simulator/network/hardware/nodes/network/router.py index 61b7b96a..42821120 100644 --- a/src/primaite/simulator/network/hardware/nodes/network/router.py +++ b/src/primaite/simulator/network/hardware/nodes/network/router.py @@ -4,14 +4,14 @@ from __future__ import annotations import secrets from enum import Enum from ipaddress import IPv4Address, IPv4Network -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, ClassVar, Dict, List, Optional, Tuple, Union from prettytable import MARKDOWN, PrettyTable from pydantic import validate_call from primaite.interface.request import RequestResponse from primaite.simulator.core import RequestManager, RequestType, SimComponent -from primaite.simulator.network.hardware.base import IPWiredNetworkInterface +from primaite.simulator.network.hardware.base import IPWiredNetworkInterface, UserManager, UserSessionManager from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.nodes.network.network_node import NetworkNode from primaite.simulator.network.protocols.arp import ARPPacket @@ -1200,6 +1200,11 @@ class Router(NetworkNode): RouteTable, RouterARP, and RouterICMP services. """ + SYSTEM_SOFTWARE: ClassVar[Dict] = { + "UserSessionManager": UserSessionManager, + "UserManager": UserManager, + } + num_ports: int network_interfaces: Dict[str, RouterInterface] = {} "The Router Interfaces on the node." @@ -1235,6 +1240,7 @@ class Router(NetworkNode): resolution within the network. These services are crucial for the router's operation, enabling it to manage network traffic efficiently. """ + super()._install_system_software() self.software_manager.install(RouterICMP) icmp: RouterICMP = self.software_manager.icmp # noqa icmp.router = self