From ced45d427571cf8a8e78a5af44163c67fdabe8e4 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Wed, 16 Aug 2023 16:45:52 +0100 Subject: [PATCH 1/7] Connect actions of top-level sim components --- src/primaite/simulator/domain/controller.py | 16 +++++++++- src/primaite/simulator/network/container.py | 23 ++++++++++++++ src/primaite/simulator/sim_container.py | 34 +++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/primaite/simulator/network/container.py create mode 100644 src/primaite/simulator/sim_container.py diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index 887a065d..4e872531 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Dict, Final, List, Literal, Tuple -from primaite.simulator.core import ActionPermissionValidator, SimComponent +from primaite.simulator.core import Action, ActionManager, ActionPermissionValidator, SimComponent from primaite.simulator.domain.account import Account, AccountType @@ -82,6 +82,20 @@ class DomainController(SimComponent): folders: List[temp_folder] = {} files: List[temp_file] = {} + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.action_manager = ActionManager() + # Action 'account' matches requests like: + # ['account', '', *account_action] + self.action_manager.add_action( + "account", + Action( + func=lambda request, context: self.accounts[request.pop(0)].apply_action(request, context), + validator=GroupMembershipValidator([AccountGroup.DOMAIN_ADMIN]), + ), + ) + def _register_account(self, account: Account) -> None: """TODO.""" ... diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py new file mode 100644 index 00000000..346a089e --- /dev/null +++ b/src/primaite/simulator/network/container.py @@ -0,0 +1,23 @@ +from typing import Dict + +from primaite.simulator.core import Action, ActionManager, AllowAllValidator, SimComponent +from primaite.simulator.network.hardware.base import Link, Node + + +class NetworkContainer(SimComponent): + """TODO.""" + + nodes: Dict[str, Node] = {} + links: Dict[str, Link] = {} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.action_manager = ActionManager() + self.action_manager.add_action( + "node", + Action( + func=lambda request, context: self.nodes[request.pop(0)].apply_action(request, context), + validator=AllowAllValidator(), + ), + ) diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py new file mode 100644 index 00000000..6989d2b9 --- /dev/null +++ b/src/primaite/simulator/sim_container.py @@ -0,0 +1,34 @@ +from primaite.simulator.core import Action, ActionManager, AllowAllValidator, SimComponent +from primaite.simulator.domain.controller import DomainController + + +class __TempNetwork: + """TODO.""" + + pass + + +class SimulationContainer(SimComponent): + """TODO.""" + + network: __TempNetwork + domain: DomainController + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.action_manager = ActionManager() + # pass through network actions to the network objects + self.action_manager.add_action( + "network", + Action( + func=lambda request, context: self.network.apply_action(request, context), validator=AllowAllValidator() + ), + ) + # pass through domain actions to the domain object + self.action_manager.add_action( + "domain", + Action( + func=lambda request, context: self.domain.apply_action(request, context), validator=AllowAllValidator() + ), + ) From 6ca53803cd819334f505f7b2019efd7b67951747 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Thu, 17 Aug 2023 15:32:12 +0100 Subject: [PATCH 2/7] Describe state --- .../notebooks/create-simulation.ipynb | 140 +++++++++++++ src/primaite/simulator/core.py | 5 +- src/primaite/simulator/domain/account.py | 23 ++- src/primaite/simulator/domain/controller.py | 13 ++ .../simulator/file_system/file_system.py | 11 +- .../simulator/file_system/file_system_file.py | 14 +- .../file_system/file_system_folder.py | 25 ++- .../file_system/file_system_item_abc.py | 18 +- src/primaite/simulator/network/container.py | 18 ++ .../simulator/network/hardware/base.py | 187 +++++++++++------- src/primaite/simulator/sim_container.py | 37 +++- .../system/applications/application.py | 7 +- .../simulator/system/core/session_manager.py | 14 +- .../simulator/system/processes/process.py | 7 +- .../simulator/system/services/service.py | 7 +- src/primaite/simulator/system/software.py | 27 ++- .../_simulator/test_sim_conatiner.py | 16 ++ 17 files changed, 444 insertions(+), 125 deletions(-) create mode 100644 src/primaite/notebooks/create-simulation.ipynb create mode 100644 tests/unit_tests/_primaite/_simulator/test_sim_conatiner.py diff --git a/src/primaite/notebooks/create-simulation.ipynb b/src/primaite/notebooks/create-simulation.ipynb new file mode 100644 index 00000000..e5fd63b0 --- /dev/null +++ b/src/primaite/notebooks/create-simulation.ipynb @@ -0,0 +1,140 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Build a simulation using the Python API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import the Simulation class" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.sim_container import Simulation\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create an empty simulation. By default this has a network with no nodes or links, and a domain controller with no accounts.\n", + "\n", + "Let's use the simulation's `describe_state()` method to verify that it is empty." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'uuid': '8d5cbb1b-aa9b-4f66-8b23-80d47755df69',\n", + " 'network': {'uuid': 'd3569bc4-eeed-40b1-9c3c-0fe80b9bb11c',\n", + " 'nodes': {},\n", + " 'links': {}},\n", + " 'domain': {'uuid': '4d4024ae-5948-4f07-aed9-d2315891cddc', 'accounts': {}}}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_sim = Simulation()\n", + "my_sim.describe_state()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add nodes" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.network.hardware.base import Node\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'uuid': '8d5cbb1b-aa9b-4f66-8b23-80d47755df69',\n", + " 'network': {'uuid': 'd3569bc4-eeed-40b1-9c3c-0fe80b9bb11c',\n", + " 'nodes': {'5f596f4f-4d34-4d1c-9688-9a105e489444': {'uuid': '5f596f4f-4d34-4d1c-9688-9a105e489444',\n", + " 'hostname': 'primaite_pc',\n", + " 'operating_state': 0,\n", + " 'NICs': {},\n", + " 'file_system': {'uuid': 'dc1e7032-7dba-44d5-aedb-5da75ab1eccc',\n", + " 'folders': {}},\n", + " 'applications': {},\n", + " 'services': {},\n", + " 'process': {}}},\n", + " 'links': {}},\n", + " 'domain': {'uuid': '4d4024ae-5948-4f07-aed9-d2315891cddc', 'accounts': {}}}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_pc = Node(hostname=\"primaite_pc\",)\n", + "my_sim.network.nodes[my_pc.uuid] = my_pc\n", + "my_sim.describe_state()" + ] + }, + { + "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" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/primaite/simulator/core.py b/src/primaite/simulator/core.py index 7a183588..2c802c0f 100644 --- a/src/primaite/simulator/core.py +++ b/src/primaite/simulator/core.py @@ -147,7 +147,10 @@ class SimComponent(BaseModel): object. If there are objects referenced by this object that are owned by something else, it is not included in this output. """ - return {} + state = { + "uuid": self.uuid, + } + return state def apply_action(self, action: List[str], context: Dict = {}) -> None: """ diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index e8595afa..e30b7a27 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -43,8 +43,27 @@ class Account(SimComponent): enabled: bool = True def describe_state(self) -> Dict: - """Describe state for agent observations.""" - return super().describe_state() + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "num_logons": self.num_logons, + "num_logoffs": self.num_logoffs, + "num_group_changes": self.num_group_changes, + "username": self.username, + "password": self.password, + "account_type": self.account_type, + "enabled": self.enabled, + } + ) + return state def enable(self): """Set the status to enabled.""" diff --git a/src/primaite/simulator/domain/controller.py b/src/primaite/simulator/domain/controller.py index 4e872531..f772ab22 100644 --- a/src/primaite/simulator/domain/controller.py +++ b/src/primaite/simulator/domain/controller.py @@ -96,6 +96,19 @@ class DomainController(SimComponent): ), ) + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update({"accounts": {uuid: acct.describe_state() for uuid, acct in self.accounts.items()}}) + return state + def _register_account(self, account: Account) -> None: """TODO.""" ... diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index d42db3e0..a5f603fe 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -18,11 +18,16 @@ class FileSystem(SimComponent): def describe_state(self) -> Dict: """ - Get the current state of the FileSystem as a dict. + Produce a dictionary describing the current state of this object. - :return: A dict containing the current state of the FileSystemFile. + 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 """ - pass + state = super().describe_state() + state.update({"folders": {uuid: folder for uuid, folder in self.folders.items()}}) + return state def get_folders(self) -> Dict: """Returns the list of folders.""" diff --git a/src/primaite/simulator/file_system/file_system_file.py b/src/primaite/simulator/file_system/file_system_file.py index f9fc2e1f..4bb6e585 100644 --- a/src/primaite/simulator/file_system/file_system_file.py +++ b/src/primaite/simulator/file_system/file_system_file.py @@ -38,8 +38,16 @@ class FileSystemFile(FileSystemItem): def describe_state(self) -> Dict: """ - Get the current state of the FileSystemFile as a dict. + Produce a dictionary describing the current state of this object. - :return: A dict containing the current state of the FileSystemFile. + 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 """ - pass + return { + "uuid": self.uuid, + "name": self.name, + "size": self.size, + "file_type": self.file_type, + } diff --git a/src/primaite/simulator/file_system/file_system_folder.py b/src/primaite/simulator/file_system/file_system_folder.py index b0705804..463f3854 100644 --- a/src/primaite/simulator/file_system/file_system_folder.py +++ b/src/primaite/simulator/file_system/file_system_folder.py @@ -16,6 +16,23 @@ class FileSystemFolder(FileSystemItem): is_quarantined: bool = False """Flag that marks the folder as quarantined if true.""" + 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, + "name": self.name, + "size": self.size, + "files": {uuid: file for uuid, file in self.files.items()}, + "is_quarantined": self.is_quarantined, + } + def get_file_by_id(self, file_id: str) -> FileSystemFile: """Return a FileSystemFile with the matching id.""" return self.files.get(file_id) @@ -67,11 +84,3 @@ class FileSystemFolder(FileSystemItem): def quarantine_status(self) -> bool: """Returns true if the folder is being quarantined.""" return self.is_quarantined - - def describe_state(self) -> Dict: - """ - Get the current state of the FileSystemFolder as a dict. - - :return: A dict containing the current state of the FileSystemFile. - """ - pass 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 0594cc35..3b368819 100644 --- a/src/primaite/simulator/file_system/file_system_item_abc.py +++ b/src/primaite/simulator/file_system/file_system_item_abc.py @@ -13,5 +13,19 @@ class FileSystemItem(SimComponent): """The size the item takes up on disk.""" def describe_state(self) -> Dict: - """Returns the state of the FileSystemItem.""" - pass + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "name": self.name, + "size": self.size, + } + ) + return state diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 346a089e..463d5f91 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -21,3 +21,21 @@ class NetworkContainer(SimComponent): validator=AllowAllValidator(), ), ) + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "nodes": {uuid: node.describe_state() for uuid, node in self.nodes.items()}, + "links": {uuid: link.describe_state() for uuid, link in self.links.items()}, + } + ) + return state diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index ab5d4943..b731862b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -125,6 +125,31 @@ class NIC(SimComponent): _LOGGER.error(msg) raise ValueError(msg) + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "ip_adress": self.ip_address, + "subnet_mask": self.subnet_mask, + "gateway": self.gateway, + "mac_address": self.mac_address, + "speed": self.speed, + "mtu": self.mtu, + "wake_on_lan": self.wake_on_lan, + "dns_servers": self.dns_servers, + "enabled": self.enabled, + } + ) + return state + @property def ip_network(self) -> IPv4Network: """ @@ -241,23 +266,6 @@ class NIC(SimComponent): return True return False - def describe_state(self) -> Dict: - """ - Get the current state of the NIC as a dict. - - :return: A dict containing the current state of the NIC. - """ - pass - - def apply_action(self, action: str): - """ - Apply an action to the NIC. - - :param action: The action to be applied. - :type action: str - """ - pass - def __str__(self) -> str: return f"{self.mac_address}/{self.ip_address}" @@ -293,6 +301,25 @@ class SwitchPort(SimComponent): kwargs["mac_address"] = generate_mac_address() super().__init__(**kwargs) + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "mac_address": self.mac_address, + "speed": self.speed, + "mtu": self.mtu, + "enabled": self.enabled, + } + ) + def enable(self): """Attempt to enable the SwitchPort.""" if self.enabled: @@ -379,23 +406,6 @@ class SwitchPort(SimComponent): return True return False - def describe_state(self) -> Dict: - """ - Get the current state of the SwitchPort as a dict. - - :return: A dict containing the current state of the SwitchPort. - """ - pass - - def apply_action(self, action: str): - """ - Apply an action to the SwitchPort. - - :param action: The action to be applied. - :type action: str - """ - pass - def __str__(self) -> str: return f"{self.mac_address}" @@ -435,6 +445,26 @@ class Link(SimComponent): self.endpoint_b.connect_link(self) self.endpoint_up() + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "endpoint_a": self.endpoint_a.uuid, + "endpoint_b": self.endpoint_b.uuid, + "bandwidth": self.bandwidth, + "current_load": self.current_load, + } + ) + return state + @property def current_load_percent(self) -> str: """Get the current load formatted as a percentage string.""" @@ -504,23 +534,6 @@ class Link(SimComponent): """ self.current_load = 0 - def describe_state(self) -> Dict: - """ - Get the current state of the Link as a dict. - - :return: A dict containing the current state of the Link. - """ - pass - - def apply_action(self, action: str): - """ - Apply an action to the Link. - - :param action: The action to be applied. - :type action: str - """ - pass - def __str__(self) -> str: return f"{self.endpoint_a}<-->{self.endpoint_b}" @@ -832,6 +845,30 @@ class Node(SimComponent): super().__init__(**kwargs) self.arp.nics = self.nics + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "hostname": self.hostname, + "operating_state": self.operating_state.value, + "NICs": {uuid: nic.describe_state() for uuid, nic in self.nics.items()}, + # "switch_ports": {uuid, sp for uuid, sp in self.switch_ports.items()}, + "file_system": self.file_system.describe_state(), + "applications": {uuid: app for uuid, app in self.applications.items()}, + "services": {uuid: svc for uuid, svc in self.services.items()}, + "process": {uuid: proc for uuid, proc in self.processes.items()}, + } + ) + return state + def show(self): """Prints a table of the NICs on the Node..""" from prettytable import PrettyTable @@ -950,14 +987,6 @@ class Node(SimComponent): elif frame.ip.protocol == IPProtocol.ICMP: self.icmp.process_icmp(frame=frame) - def describe_state(self) -> Dict: - """ - Describe the state of the Node. - - :return: A dictionary representing the state of the node. - """ - pass - class Switch(Node): """A class representing a Layer 2 network switch.""" @@ -966,9 +995,17 @@ class Switch(Node): "The number of ports on the switch." switch_ports: Dict[int, SwitchPort] = {} "The SwitchPorts on the switch." - dst_mac_table: Dict[str, SwitchPort] = {} + 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.port_num = port_num + def show(self): """Prints a table of the SwitchPorts on the Switch.""" table = PrettyTable(["Port", "MAC Address", "Speed", "Status"]) @@ -978,25 +1015,29 @@ class Switch(Node): print(table) def describe_state(self) -> Dict: - """TODO.""" - pass + """ + Produce a dictionary describing the current state of this object. - 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.port_num = port_num + 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 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.dst_mac_table.get(mac_address) + mac_table_port = self.mac_address_table.get(mac_address) if not mac_table_port: - self.dst_mac_table[mac_address] = switch_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.dst_mac_table.pop(mac_address) + 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) @@ -1011,7 +1052,7 @@ class Switch(Node): dst_mac = frame.ethernet.dst_mac_addr self._add_mac_table_entry(src_mac, incoming_port) - outgoing_port = self.dst_mac_table.get(dst_mac) + 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: diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 6989d2b9..1a37dc18 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -1,20 +1,23 @@ +from typing import Dict + from primaite.simulator.core import Action, ActionManager, AllowAllValidator, SimComponent from primaite.simulator.domain.controller import DomainController +from primaite.simulator.network.container import NetworkContainer -class __TempNetwork: +class Simulation(SimComponent): """TODO.""" - pass - - -class SimulationContainer(SimComponent): - """TODO.""" - - network: __TempNetwork + network: NetworkContainer domain: DomainController def __init__(self, **kwargs): + if not kwargs.get("network"): + kwargs["network"] = NetworkContainer() + + if not kwargs.get("domain"): + kwargs["domain"] = DomainController() + super().__init__(**kwargs) self.action_manager = ActionManager() @@ -32,3 +35,21 @@ class SimulationContainer(SimComponent): func=lambda request, context: self.domain.apply_action(request, context), validator=AllowAllValidator() ), ) + + def describe_state(self) -> Dict: + """ + Produce a dictionary describing the current state of this object. + + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. + + :return: Current state of this object and child objects. + :rtype: Dict + """ + state = super().describe_state() + state.update( + { + "network": self.network.describe_state(), + "domain": self.domain.describe_state(), + } + ) + return state diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 36a7bc85..c61afae6 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -36,12 +36,11 @@ class Application(IOSoftware): @abstractmethod def describe_state(self) -> Dict: """ - Describes the current state of the software. + Produce a dictionary describing the current state of this object. - The specifics of the software's state, including its health, criticality, - and any other pertinent information, should be implemented in subclasses. + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - :return: A dictionary containing key-value pairs representing the current state of the software. + :return: Current state of this object and child objects. :rtype: Dict """ pass diff --git a/src/primaite/simulator/system/core/session_manager.py b/src/primaite/simulator/system/core/session_manager.py index 96d6251d..fe7b06b2 100644 --- a/src/primaite/simulator/system/core/session_manager.py +++ b/src/primaite/simulator/system/core/session_manager.py @@ -51,9 +51,12 @@ class Session(SimComponent): def describe_state(self) -> Dict: """ - Describes the current state of the session as a dictionary. + Produce a dictionary describing the current state of this object. - :return: A dictionary containing the current state of the session. + 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 """ pass @@ -77,9 +80,12 @@ class SessionManager: def describe_state(self) -> Dict: """ - Describes the current state of the session manager as a dictionary. + Produce a dictionary describing the current state of this object. - :return: A dictionary containing the current state of the session manager. + 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 """ pass diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index bbd94345..8e278aa3 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -27,12 +27,11 @@ class Process(Software): @abstractmethod def describe_state(self) -> Dict: """ - Describes the current state of the software. + Produce a dictionary describing the current state of this object. - The specifics of the software's state, including its health, criticality, - and any other pertinent information, should be implemented in subclasses. + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - :return: A dictionary containing key-value pairs representing the current state of the software. + :return: Current state of this object and child objects. :rtype: Dict """ pass diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 7be5cb78..29a787c5 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -35,12 +35,11 @@ class Service(IOSoftware): @abstractmethod def describe_state(self) -> Dict: """ - Describes the current state of the software. + Produce a dictionary describing the current state of this object. - The specifics of the software's state, including its health, criticality, - and any other pertinent information, should be implemented in subclasses. + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - :return: A dictionary containing key-value pairs representing the current state of the software. + :return: Current state of this object and child objects. :rtype: Dict """ pass diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 854e7e2b..5bc08178 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -78,15 +78,25 @@ class Software(SimComponent): @abstractmethod def describe_state(self) -> Dict: """ - Describes the current state of the software. + Produce a dictionary describing the current state of this object. - The specifics of the software's state, including its health, criticality, - and any other pertinent information, should be implemented in subclasses. + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - :return: A dictionary containing key-value pairs representing the current state of the software. + :return: Current state of this object and child objects. :rtype: Dict """ - pass + state = super().describe_state() + state.update( + { + "health_state": self.health_state_actual.name, + "health_state_red_view": self.health_state_visible.name, + "criticality": self.criticality.name, + "patching_count": self.patching_count, + "scanning_count": self.scanning_count, + "revealed_to_red": self.revealed_to_red, + } + ) + return state def apply_action(self, action: List[str]) -> None: """ @@ -134,12 +144,11 @@ class IOSoftware(Software): @abstractmethod def describe_state(self) -> Dict: """ - Describes the current state of the software. + Produce a dictionary describing the current state of this object. - The specifics of the software's state, including its health, criticality, - and any other pertinent information, should be implemented in subclasses. + Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation. - :return: A dictionary containing key-value pairs representing the current state of the software. + :return: Current state of this object and child objects. :rtype: Dict """ pass diff --git a/tests/unit_tests/_primaite/_simulator/test_sim_conatiner.py b/tests/unit_tests/_primaite/_simulator/test_sim_conatiner.py new file mode 100644 index 00000000..4543259d --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/test_sim_conatiner.py @@ -0,0 +1,16 @@ +from primaite.simulator.sim_container import Simulation + + +def test_creating_empty_simulation(): + """Check that no errors occur when trying to setup a simulation without providing parameters""" + empty_sim = Simulation() + + +def test_empty_sim_state(): + """Check that describe_state has the right subcomponents.""" + empty_sim = Simulation() + sim_state = empty_sim.describe_state() + network_state = empty_sim.network.describe_state() + domain_state = empty_sim.domain.describe_state() + assert sim_state["network"] == network_state + assert sim_state["domain"] == domain_state From 01c912c094cfcfdf27609db77cff2a841b64dd17 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 20 Aug 2023 18:38:02 +0100 Subject: [PATCH 3/7] fix type hints and describe state functions --- .../notebooks/create-simulation.ipynb | 400 +++++++++++++++++- src/primaite/simulator/domain/account.py | 2 +- .../simulator/file_system/file_system.py | 4 +- .../simulator/file_system/file_system_file.py | 14 +- .../file_system/file_system_folder.py | 17 +- .../simulator/network/hardware/base.py | 27 +- .../system/applications/application.py | 24 +- .../simulator/system/processes/process.py | 4 +- .../simulator/system/services/service.py | 4 +- src/primaite/simulator/system/software.py | 12 +- 10 files changed, 459 insertions(+), 49 deletions(-) diff --git a/src/primaite/notebooks/create-simulation.ipynb b/src/primaite/notebooks/create-simulation.ipynb index e5fd63b0..86a7f6a2 100644 --- a/src/primaite/notebooks/create-simulation.ipynb +++ b/src/primaite/notebooks/create-simulation.ipynb @@ -40,11 +40,11 @@ { "data": { "text/plain": [ - "{'uuid': '8d5cbb1b-aa9b-4f66-8b23-80d47755df69',\n", - " 'network': {'uuid': 'd3569bc4-eeed-40b1-9c3c-0fe80b9bb11c',\n", + "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", + " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", " 'nodes': {},\n", " 'links': {}},\n", - " 'domain': {'uuid': '4d4024ae-5948-4f07-aed9-d2315891cddc', 'accounts': {}}}" + " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912', 'accounts': {}}}" ] }, "execution_count": 2, @@ -81,19 +81,28 @@ { "data": { "text/plain": [ - "{'uuid': '8d5cbb1b-aa9b-4f66-8b23-80d47755df69',\n", - " 'network': {'uuid': 'd3569bc4-eeed-40b1-9c3c-0fe80b9bb11c',\n", - " 'nodes': {'5f596f4f-4d34-4d1c-9688-9a105e489444': {'uuid': '5f596f4f-4d34-4d1c-9688-9a105e489444',\n", + "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", + " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", + " 'nodes': {'c7c91f06-f128-4891-84a2-83beceea3908': {'uuid': 'c7c91f06-f128-4891-84a2-83beceea3908',\n", " 'hostname': 'primaite_pc',\n", " 'operating_state': 0,\n", " 'NICs': {},\n", - " 'file_system': {'uuid': 'dc1e7032-7dba-44d5-aedb-5da75ab1eccc',\n", + " 'file_system': {'uuid': '04ffd1e8-dea7-47ad-a088-4856df055ed1',\n", + " 'folders': {}},\n", + " 'applications': {},\n", + " 'services': {},\n", + " 'process': {}},\n", + " 'dfcc395a-93ff-4dd5-9684-c80c5885d827': {'uuid': 'dfcc395a-93ff-4dd5-9684-c80c5885d827',\n", + " 'hostname': 'google_server',\n", + " 'operating_state': 0,\n", + " 'NICs': {},\n", + " 'file_system': {'uuid': 'aea8f406-05de-4a02-b65f-972aa1fed70e',\n", " 'folders': {}},\n", " 'applications': {},\n", " 'services': {},\n", " 'process': {}}},\n", " 'links': {}},\n", - " 'domain': {'uuid': '4d4024ae-5948-4f07-aed9-d2315891cddc', 'accounts': {}}}" + " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912', 'accounts': {}}}" ] }, "execution_count": 4, @@ -103,16 +112,387 @@ ], "source": [ "my_pc = Node(hostname=\"primaite_pc\",)\n", + "my_server = Node(hostname=\"google_server\")\n", + "\n", + "# TODO: when there is a proper function for adding nodes, use it instead of manually adding.\n", + "\n", "my_sim.network.nodes[my_pc.uuid] = my_pc\n", + "my_sim.network.nodes[my_server.uuid] = my_server\n", + "\n", + "my_sim.describe_state()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connect the nodes" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.network.hardware.base import NIC, Link, Switch\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-08-20 18:34:59,328: NIC c3:08:90:23:29:cb/130.1.1.1 connected to Link c3:08:90:23:29:cb/130.1.1.1<-->40:4a:3f:2e:ee:2e\n", + "2023-08-20 18:34:59,329: SwitchPort 40:4a:3f:2e:ee:2e connected to Link c3:08:90:23:29:cb/130.1.1.1<-->40:4a:3f:2e:ee:2e\n", + "2023-08-20 18:34:59,331: NIC 69:50:cb:76:22:10/130.1.1.2 connected to Link 69:50:cb:76:22:10/130.1.1.2<-->18:5e:49:ed:21:55\n", + "2023-08-20 18:34:59,331: SwitchPort 18:5e:49:ed:21:55 connected to Link 69:50:cb:76:22:10/130.1.1.2<-->18:5e:49:ed:21:55\n" + ] + } + ], + "source": [ + "my_swtich = Switch(hostname=\"switch1\", num_ports=12)\n", + "\n", + "pc_nic = NIC(ip_address=\"130.1.1.1\", gateway=\"130.1.1.255\", subnet_mask=\"255.255.255.0\")\n", + "my_pc.connect_nic(pc_nic)\n", + "\n", + "\n", + "server_nic = NIC(ip_address=\"130.1.1.2\", gateway=\"130.1.1.255\", subnet_mask=\"255.255.255.0\")\n", + "my_server.connect_nic(server_nic)\n", + "\n", + "\n", + "pc_to_switch = Link(endpoint_a=pc_nic, endpoint_b=my_swtich.switch_ports[1])\n", + "server_to_swtich = Link(endpoint_a=server_nic, endpoint_b=my_swtich.switch_ports[2])\n", + "\n", + "my_sim.network.links[pc_to_switch.uuid] = pc_to_switch\n", + "my_sim.network.links[server_to_swtich.uuid] = server_to_swtich" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", + " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", + " 'nodes': {'c7c91f06-f128-4891-84a2-83beceea3908': {'uuid': 'c7c91f06-f128-4891-84a2-83beceea3908',\n", + " 'hostname': 'primaite_pc',\n", + " 'operating_state': 0,\n", + " 'NICs': {'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2': {'uuid': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", + " 'ip_adress': '130.1.1.1',\n", + " 'subnet_mask': '255.255.255.0',\n", + " 'gateway': '130.1.1.255',\n", + " 'mac_address': 'c3:08:90:23:29:cb',\n", + " 'speed': 100,\n", + " 'mtu': 1500,\n", + " 'wake_on_lan': False,\n", + " 'dns_servers': [],\n", + " 'enabled': False}},\n", + " 'file_system': {'uuid': '04ffd1e8-dea7-47ad-a088-4856df055ed1',\n", + " 'folders': {}},\n", + " 'applications': {},\n", + " 'services': {},\n", + " 'process': {}},\n", + " 'dfcc395a-93ff-4dd5-9684-c80c5885d827': {'uuid': 'dfcc395a-93ff-4dd5-9684-c80c5885d827',\n", + " 'hostname': 'google_server',\n", + " 'operating_state': 0,\n", + " 'NICs': {'1fd281a0-83ae-49d9-9b40-6aae7b465cab': {'uuid': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", + " 'ip_adress': '130.1.1.2',\n", + " 'subnet_mask': '255.255.255.0',\n", + " 'gateway': '130.1.1.255',\n", + " 'mac_address': '69:50:cb:76:22:10',\n", + " 'speed': 100,\n", + " 'mtu': 1500,\n", + " 'wake_on_lan': False,\n", + " 'dns_servers': [],\n", + " 'enabled': False}},\n", + " 'file_system': {'uuid': 'aea8f406-05de-4a02-b65f-972aa1fed70e',\n", + " 'folders': {}},\n", + " 'applications': {},\n", + " 'services': {},\n", + " 'process': {}}},\n", + " 'links': {'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9': {'uuid': 'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9',\n", + " 'endpoint_a': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", + " 'endpoint_b': '4e6abc87-b4b9-4f95-a9a9-59cac130c6ff',\n", + " 'bandwidth': 100.0,\n", + " 'current_load': 0.0},\n", + " '2dab7fc3-470d-44d2-8593-feb8e96d71ae': {'uuid': '2dab7fc3-470d-44d2-8593-feb8e96d71ae',\n", + " 'endpoint_a': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", + " 'endpoint_b': 'e136553f-333e-4abf-b1f3-ce352ffa4630',\n", + " 'bandwidth': 100.0,\n", + " 'current_load': 0.0}}},\n", + " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912', 'accounts': {}}}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_sim.describe_state()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add files and folders to nodes\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.file_system.file_system_file_type import FileSystemFileType\n", + "from primaite.simulator.file_system.file_system_file import FileSystemFile" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "my_pc_downloads_folder = my_pc.file_system.create_folder(\"downloads\")\n", + "my_pc_downloads_folder.add_file(FileSystemFile(name=\"firefox_installer.zip\",file_type=FileSystemFileType.ZIP))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FileSystemFile(uuid='3ecf7223-dafd-4973-8c3b-b85af4e177da', name='favicon.ico', size=40.0, file_type=, action_manager=None)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_server_folder = my_server.file_system.create_folder(\"static\")\n", + "my_server.file_system.create_file(\"favicon.ico\", file_type=FileSystemFileType.PNG)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add applications to nodes" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.system.applications.application import Application, ApplicationOperatingState\n", + "from primaite.simulator.system.software import SoftwareHealthState, SoftwareCriticality\n", + "from primaite.simulator.network.transmission.transport_layer import Port\n", + "\n", + "class MSPaint(Application):\n", + " def describe_state(self):\n", + " return super().describe_state()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "mspaint = MSPaint(name = \"mspaint\", health_state_actual=SoftwareHealthState.GOOD, health_state_visible=SoftwareHealthState.GOOD, criticality=SoftwareCriticality.MEDIUM, ports={Port.HTTP}, operating_state=ApplicationOperatingState.RUNNING,execution_control_status='manual')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "my_pc.applications[mspaint.uuid] = mspaint" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create a domain account" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from primaite.simulator.domain.account import Account, AccountType\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "acct = Account(username=\"admin\", password=\"admin12\", account_type=AccountType.USER)\n", + "my_sim.domain.accounts[acct.uuid] = acct" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", + " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", + " 'nodes': {'c7c91f06-f128-4891-84a2-83beceea3908': {'uuid': 'c7c91f06-f128-4891-84a2-83beceea3908',\n", + " 'hostname': 'primaite_pc',\n", + " 'operating_state': 0,\n", + " 'NICs': {'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2': {'uuid': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", + " 'ip_adress': '130.1.1.1',\n", + " 'subnet_mask': '255.255.255.0',\n", + " 'gateway': '130.1.1.255',\n", + " 'mac_address': 'c3:08:90:23:29:cb',\n", + " 'speed': 100,\n", + " 'mtu': 1500,\n", + " 'wake_on_lan': False,\n", + " 'dns_servers': [],\n", + " 'enabled': False}},\n", + " 'file_system': {'uuid': '04ffd1e8-dea7-47ad-a088-4856df055ed1',\n", + " 'folders': {'f1fdf2ae-6377-4417-a28a-3edb4058712d': {'uuid': 'f1fdf2ae-6377-4417-a28a-3edb4058712d',\n", + " 'name': 'downloads',\n", + " 'size': 1000.0,\n", + " 'files': {'409b09a3-0d98-4c03-adf2-09190539be45': {'uuid': '409b09a3-0d98-4c03-adf2-09190539be45',\n", + " 'name': 'firefox_installer.zip',\n", + " 'size': 1000.0,\n", + " 'file_type': 'ZIP'}},\n", + " 'is_quarantined': False}}},\n", + " 'applications': {'cddee888-d1b9-4289-8512-bc0a6672c880': {'uuid': 'cddee888-d1b9-4289-8512-bc0a6672c880',\n", + " 'health_state': 'GOOD',\n", + " 'health_state_red_view': 'GOOD',\n", + " 'criticality': 'MEDIUM',\n", + " 'patching_count': 0,\n", + " 'scanning_count': 0,\n", + " 'revealed_to_red': False,\n", + " 'installing_count': 0,\n", + " 'max_sessions': 1,\n", + " 'tcp': True,\n", + " 'udp': True,\n", + " 'ports': ['HTTP'],\n", + " 'opearting_state': 'RUNNING',\n", + " 'execution_control_status': 'manual',\n", + " 'num_executions': 0,\n", + " 'groups': []}},\n", + " 'services': {},\n", + " 'process': {}},\n", + " 'dfcc395a-93ff-4dd5-9684-c80c5885d827': {'uuid': 'dfcc395a-93ff-4dd5-9684-c80c5885d827',\n", + " 'hostname': 'google_server',\n", + " 'operating_state': 0,\n", + " 'NICs': {'1fd281a0-83ae-49d9-9b40-6aae7b465cab': {'uuid': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", + " 'ip_adress': '130.1.1.2',\n", + " 'subnet_mask': '255.255.255.0',\n", + " 'gateway': '130.1.1.255',\n", + " 'mac_address': '69:50:cb:76:22:10',\n", + " 'speed': 100,\n", + " 'mtu': 1500,\n", + " 'wake_on_lan': False,\n", + " 'dns_servers': [],\n", + " 'enabled': False}},\n", + " 'file_system': {'uuid': 'aea8f406-05de-4a02-b65f-972aa1fed70e',\n", + " 'folders': {'beb5b535-cf6c-431d-94f6-d1097910130d': {'uuid': 'beb5b535-cf6c-431d-94f6-d1097910130d',\n", + " 'name': 'static',\n", + " 'size': 0,\n", + " 'files': {},\n", + " 'is_quarantined': False},\n", + " '6644cd6c-1eca-4fe4-9313-e3481abb895e': {'uuid': '6644cd6c-1eca-4fe4-9313-e3481abb895e',\n", + " 'name': 'root',\n", + " 'size': 40.0,\n", + " 'files': {'3ecf7223-dafd-4973-8c3b-b85af4e177da': {'uuid': '3ecf7223-dafd-4973-8c3b-b85af4e177da',\n", + " 'name': 'favicon.ico',\n", + " 'size': 40.0,\n", + " 'file_type': 'PNG'}},\n", + " 'is_quarantined': False}}},\n", + " 'applications': {},\n", + " 'services': {},\n", + " 'process': {}}},\n", + " 'links': {'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9': {'uuid': 'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9',\n", + " 'endpoint_a': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", + " 'endpoint_b': '4e6abc87-b4b9-4f95-a9a9-59cac130c6ff',\n", + " 'bandwidth': 100.0,\n", + " 'current_load': 0.0},\n", + " '2dab7fc3-470d-44d2-8593-feb8e96d71ae': {'uuid': '2dab7fc3-470d-44d2-8593-feb8e96d71ae',\n", + " 'endpoint_a': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", + " 'endpoint_b': 'e136553f-333e-4abf-b1f3-ce352ffa4630',\n", + " 'bandwidth': 100.0,\n", + " 'current_load': 0.0}}},\n", + " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912',\n", + " 'accounts': {'563a1805-0b32-4ba6-9551-e127e5eb57a8': {'uuid': '563a1805-0b32-4ba6-9551-e127e5eb57a8',\n", + " 'num_logons': 0,\n", + " 'num_logoffs': 0,\n", + " 'num_group_changes': 0,\n", + " 'username': 'admin',\n", + " 'password': 'admin12',\n", + " 'account_type': 'USER',\n", + " 'enabled': True}}}}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ "my_sim.describe_state()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"uuid\": \"f0c06262-1bd9-49ee-81f8-793fb4a5e58e\", \"network\": {\"uuid\": \"455d6a1a-ca23-4135-b326-3ebf75022a45\", \"nodes\": {\"c7c91f06-f128-4891-84a2-83beceea3908\": {\"uuid\": \"c7c91f06-f128-4891-84a2-83beceea3908\", \"hostname\": \"primaite_pc\", \"operating_state\": 0, \"NICs\": {\"f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2\": {\"uuid\": \"f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2\", \"ip_adress\": \"130.1.1.1\", \"subnet_mask\": \"255.255.255.0\", \"gateway\": \"130.1.1.255\", \"mac_address\": \"c3:08:90:23:29:cb\", \"speed\": 100, \"mtu\": 1500, \"wake_on_lan\": false, \"dns_servers\": [], \"enabled\": false}}, \"file_system\": {\"uuid\": \"04ffd1e8-dea7-47ad-a088-4856df055ed1\", \"folders\": {\"f1fdf2ae-6377-4417-a28a-3edb4058712d\": {\"uuid\": \"f1fdf2ae-6377-4417-a28a-3edb4058712d\", \"name\": \"downloads\", \"size\": 1000.0, \"files\": {\"409b09a3-0d98-4c03-adf2-09190539be45\": {\"uuid\": \"409b09a3-0d98-4c03-adf2-09190539be45\", \"name\": \"firefox_installer.zip\", \"size\": 1000.0, \"file_type\": \"ZIP\"}}, \"is_quarantined\": false}}}, \"applications\": {\"cddee888-d1b9-4289-8512-bc0a6672c880\": {\"uuid\": \"cddee888-d1b9-4289-8512-bc0a6672c880\", \"health_state\": \"GOOD\", \"health_state_red_view\": \"GOOD\", \"criticality\": \"MEDIUM\", \"patching_count\": 0, \"scanning_count\": 0, \"revealed_to_red\": false, \"installing_count\": 0, \"max_sessions\": 1, \"tcp\": true, \"udp\": true, \"ports\": [\"HTTP\"], \"opearting_state\": \"RUNNING\", \"execution_control_status\": \"manual\", \"num_executions\": 0, \"groups\": []}}, \"services\": {}, \"process\": {}}, \"dfcc395a-93ff-4dd5-9684-c80c5885d827\": {\"uuid\": \"dfcc395a-93ff-4dd5-9684-c80c5885d827\", \"hostname\": \"google_server\", \"operating_state\": 0, \"NICs\": {\"1fd281a0-83ae-49d9-9b40-6aae7b465cab\": {\"uuid\": \"1fd281a0-83ae-49d9-9b40-6aae7b465cab\", \"ip_adress\": \"130.1.1.2\", \"subnet_mask\": \"255.255.255.0\", \"gateway\": \"130.1.1.255\", \"mac_address\": \"69:50:cb:76:22:10\", \"speed\": 100, \"mtu\": 1500, \"wake_on_lan\": false, \"dns_servers\": [], \"enabled\": false}}, \"file_system\": {\"uuid\": \"aea8f406-05de-4a02-b65f-972aa1fed70e\", \"folders\": {\"beb5b535-cf6c-431d-94f6-d1097910130d\": {\"uuid\": \"beb5b535-cf6c-431d-94f6-d1097910130d\", \"name\": \"static\", \"size\": 0, \"files\": {}, \"is_quarantined\": false}, \"6644cd6c-1eca-4fe4-9313-e3481abb895e\": {\"uuid\": \"6644cd6c-1eca-4fe4-9313-e3481abb895e\", \"name\": \"root\", \"size\": 40.0, \"files\": {\"3ecf7223-dafd-4973-8c3b-b85af4e177da\": {\"uuid\": \"3ecf7223-dafd-4973-8c3b-b85af4e177da\", \"name\": \"favicon.ico\", \"size\": 40.0, \"file_type\": \"PNG\"}}, \"is_quarantined\": false}}}, \"applications\": {}, \"services\": {}, \"process\": {}}}, \"links\": {\"cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9\": {\"uuid\": \"cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9\", \"endpoint_a\": \"f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2\", \"endpoint_b\": \"4e6abc87-b4b9-4f95-a9a9-59cac130c6ff\", \"bandwidth\": 100.0, \"current_load\": 0.0}, \"2dab7fc3-470d-44d2-8593-feb8e96d71ae\": {\"uuid\": \"2dab7fc3-470d-44d2-8593-feb8e96d71ae\", \"endpoint_a\": \"1fd281a0-83ae-49d9-9b40-6aae7b465cab\", \"endpoint_b\": \"e136553f-333e-4abf-b1f3-ce352ffa4630\", \"bandwidth\": 100.0, \"current_load\": 0.0}}}, \"domain\": {\"uuid\": \"9da912d5-4c07-4df6-94c2-b3630e178912\", \"accounts\": {\"563a1805-0b32-4ba6-9551-e127e5eb57a8\": {\"uuid\": \"563a1805-0b32-4ba6-9551-e127e5eb57a8\", \"num_logons\": 0, \"num_logoffs\": 0, \"num_group_changes\": 0, \"username\": \"admin\", \"password\": \"admin12\", \"account_type\": \"USER\", \"enabled\": true}}}}'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d = my_sim.describe_state()\n", + "json.dumps(d)" + ] } ], "metadata": { diff --git a/src/primaite/simulator/domain/account.py b/src/primaite/simulator/domain/account.py index e30b7a27..d235c00e 100644 --- a/src/primaite/simulator/domain/account.py +++ b/src/primaite/simulator/domain/account.py @@ -59,7 +59,7 @@ class Account(SimComponent): "num_group_changes": self.num_group_changes, "username": self.username, "password": self.password, - "account_type": self.account_type, + "account_type": self.account_type.name, "enabled": self.enabled, } ) diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index a5f603fe..440b7dc5 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -13,7 +13,7 @@ _LOGGER = getLogger(__name__) class FileSystem(SimComponent): """Class that contains all the simulation File System.""" - folders: Dict = {} + folders: Dict[str, FileSystemFolder] = {} """List containing all the folders in the file system.""" def describe_state(self) -> Dict: @@ -26,7 +26,7 @@ class FileSystem(SimComponent): :rtype: Dict """ state = super().describe_state() - state.update({"folders": {uuid: folder for uuid, folder in self.folders.items()}}) + state.update({"folders": {uuid: folder.describe_state() for uuid, folder in self.folders.items()}}) return state def get_folders(self) -> Dict: diff --git a/src/primaite/simulator/file_system/file_system_file.py b/src/primaite/simulator/file_system/file_system_file.py index 4bb6e585..c25f5973 100644 --- a/src/primaite/simulator/file_system/file_system_file.py +++ b/src/primaite/simulator/file_system/file_system_file.py @@ -45,9 +45,11 @@ class FileSystemFile(FileSystemItem): :return: Current state of this object and child objects. :rtype: Dict """ - return { - "uuid": self.uuid, - "name": self.name, - "size": self.size, - "file_type": self.file_type, - } + state = super().describe_state() + state.update( + { + "uuid": self.uuid, + "file_type": self.file_type.name, + } + ) + return state diff --git a/src/primaite/simulator/file_system/file_system_folder.py b/src/primaite/simulator/file_system/file_system_folder.py index 463f3854..4e461a3a 100644 --- a/src/primaite/simulator/file_system/file_system_folder.py +++ b/src/primaite/simulator/file_system/file_system_folder.py @@ -10,7 +10,7 @@ _LOGGER = getLogger(__name__) class FileSystemFolder(FileSystemItem): """Simulation FileSystemFolder.""" - files: Dict = {} + files: Dict[str, FileSystemFile] = {} """List of files stored in the folder.""" is_quarantined: bool = False @@ -25,13 +25,14 @@ class FileSystemFolder(FileSystemItem): :return: Current state of this object and child objects. :rtype: Dict """ - return { - "uuid": self.uuid, - "name": self.name, - "size": self.size, - "files": {uuid: file for uuid, file in self.files.items()}, - "is_quarantined": self.is_quarantined, - } + state = super().describe_state() + state.update( + { + "files": {uuid: file.describe_state() for uuid, file in self.files.items()}, + "is_quarantined": self.is_quarantined, + } + ) + return state def get_file_by_id(self, file_id: str) -> FileSystemFile: """Return a FileSystemFile with the matching id.""" diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index b731862b..28e7693a 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -11,15 +11,19 @@ from prettytable import PrettyTable from primaite import getLogger from primaite.exceptions import NetworkError from primaite.simulator.core import SimComponent +from primaite.simulator.domain.account import Account from primaite.simulator.file_system.file_system import FileSystem 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 from primaite.simulator.network.transmission.transport_layer import Port, TCPHeader +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.packet_capture import PacketCapture from primaite.simulator.system.core.session_manager import SessionManager 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 _LOGGER = getLogger(__name__) @@ -137,9 +141,9 @@ class NIC(SimComponent): state = super().describe_state() state.update( { - "ip_adress": self.ip_address, - "subnet_mask": self.subnet_mask, - "gateway": self.gateway, + "ip_adress": str(self.ip_address), + "subnet_mask": str(self.subnet_mask), + "gateway": str(self.gateway), "mac_address": self.mac_address, "speed": self.speed, "mtu": self.mtu, @@ -319,6 +323,7 @@ class SwitchPort(SimComponent): "enabled": self.enabled, } ) + return state def enable(self): """Attempt to enable the SwitchPort.""" @@ -802,13 +807,13 @@ class Node(SimComponent): nics: Dict[str, NIC] = {} "The NICs on the node." - accounts: Dict = {} + accounts: Dict[str, Account] = {} "All accounts on the node." - applications: Dict = {} + applications: Dict[str, Application] = {} "All applications on the node." - services: Dict = {} + services: Dict[str, Service] = {} "All services on the node." - processes: Dict = {} + processes: Dict[str, Process] = {} "All processes on the node." file_system: FileSystem "The nodes file system." @@ -862,9 +867,9 @@ class Node(SimComponent): "NICs": {uuid: nic.describe_state() for uuid, nic in self.nics.items()}, # "switch_ports": {uuid, sp for uuid, sp in self.switch_ports.items()}, "file_system": self.file_system.describe_state(), - "applications": {uuid: app for uuid, app in self.applications.items()}, - "services": {uuid: svc for uuid, svc in self.services.items()}, - "process": {uuid: proc for uuid, proc in self.processes.items()}, + "applications": {uuid: app.describe_state() for uuid, app in self.applications.items()}, + "services": {uuid: svc.describe_state() for uuid, svc in self.services.items()}, + "process": {uuid: proc.describe_state() for uuid, proc in self.processes.items()}, } ) return state @@ -1026,7 +1031,7 @@ class Switch(Node): return { "uuid": self.uuid, "num_ports": self.num_ports, # redundant? - "ports": {port_num: port for port_num, port in self.switch_ports.items()}, + "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()}, } diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index c61afae6..37748560 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -8,13 +8,12 @@ from primaite.simulator.system.software import IOSoftware class ApplicationOperatingState(Enum): """Enumeration of Application Operating States.""" - -RUNNING = 1 -"The application is running." -CLOSED = 2 -"The application is closed or not running." -INSTALLING = 3 -"The application is being installed or updated." + RUNNING = 1 + "The application is running." + CLOSED = 2 + "The application is closed or not running." + INSTALLING = 3 + "The application is being installed or updated." class Application(IOSoftware): @@ -43,7 +42,16 @@ class Application(IOSoftware): :return: Current state of this object and child objects. :rtype: Dict """ - pass + state = super().describe_state() + state.update( + { + "opearting_state": self.operating_state.name, + "execution_control_status": self.execution_control_status, + "num_executions": self.num_executions, + "groups": list(self.groups), + } + ) + return state def apply_action(self, action: List[str]) -> None: """ diff --git a/src/primaite/simulator/system/processes/process.py b/src/primaite/simulator/system/processes/process.py index 8e278aa3..c4e94845 100644 --- a/src/primaite/simulator/system/processes/process.py +++ b/src/primaite/simulator/system/processes/process.py @@ -34,4 +34,6 @@ class Process(Software): :return: Current state of this object and child objects. :rtype: Dict """ - pass + state = super().describe_state() + state.update({"operating_state": self.operating_state.name}) + return state diff --git a/src/primaite/simulator/system/services/service.py b/src/primaite/simulator/system/services/service.py index 29a787c5..eafff3f0 100644 --- a/src/primaite/simulator/system/services/service.py +++ b/src/primaite/simulator/system/services/service.py @@ -42,7 +42,9 @@ class Service(IOSoftware): :return: Current state of this object and child objects. :rtype: Dict """ - pass + state = super().describe_state() + state.update({"operating_state": self.operating_state.name}) + return state def apply_action(self, action: List[str]) -> None: """ diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index 5bc08178..a2acd9fb 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -151,7 +151,17 @@ class IOSoftware(Software): :return: Current state of this object and child objects. :rtype: Dict """ - pass + state = super().describe_state() + state.update( + { + "installing_count": self.installing_count, + "max_sessions": self.max_sessions, + "tcp": self.tcp, + "udp": self.udp, + "ports": [port.name for port in self.ports], # TODO: not sure if this should be port.name or port.value + } + ) + return state def send(self, payload: Any, session_id: str, **kwargs) -> bool: """ From 3911010777ad09451f8da4b2ac1a72b947f0ff61 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 20 Aug 2023 18:42:58 +0100 Subject: [PATCH 4/7] update notebook --- src/primaite/notebooks/create-simulation.ipynb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/primaite/notebooks/create-simulation.ipynb b/src/primaite/notebooks/create-simulation.ipynb index 86a7f6a2..a3e2d92c 100644 --- a/src/primaite/notebooks/create-simulation.ipynb +++ b/src/primaite/notebooks/create-simulation.ipynb @@ -464,6 +464,13 @@ "my_sim.describe_state()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Verify that the state dictionary contains no non-serialisable objects." + ] + }, { "cell_type": "code", "execution_count": 17, From 7c16a9cdde818093c518844868aecdbb4c38f2e1 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Sun, 20 Aug 2023 18:43:21 +0100 Subject: [PATCH 5/7] Update notebook --- .../notebooks/create-simulation.ipynb | 230 +++++------------- 1 file changed, 61 insertions(+), 169 deletions(-) diff --git a/src/primaite/notebooks/create-simulation.ipynb b/src/primaite/notebooks/create-simulation.ipynb index a3e2d92c..b0a140a1 100644 --- a/src/primaite/notebooks/create-simulation.ipynb +++ b/src/primaite/notebooks/create-simulation.ipynb @@ -4,7 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Build a simulation using the Python API\n" + "# Build a simulation using the Python API\n", + "\n", + "Currently, this notbook manipulates the simulation by directly placing objects inside of the attributes of the network and domain. It should be refactored when proper methods exist for adding these objects.\n" ] }, { @@ -40,11 +42,11 @@ { "data": { "text/plain": [ - "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", - " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", + "{'uuid': '5304ed6d-de4c-408c-ae24-ada32852d196',\n", + " 'network': {'uuid': 'fa17dfe8-81a1-4c7f-8c5b-8c2d3b1e8756',\n", " 'nodes': {},\n", " 'links': {}},\n", - " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912', 'accounts': {}}}" + " 'domain': {'uuid': '320cbb83-eb1b-4911-a4f0-fc46d8038a8a', 'accounts': {}}}" ] }, "execution_count": 2, @@ -77,39 +79,7 @@ "cell_type": "code", "execution_count": 4, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", - " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", - " 'nodes': {'c7c91f06-f128-4891-84a2-83beceea3908': {'uuid': 'c7c91f06-f128-4891-84a2-83beceea3908',\n", - " 'hostname': 'primaite_pc',\n", - " 'operating_state': 0,\n", - " 'NICs': {},\n", - " 'file_system': {'uuid': '04ffd1e8-dea7-47ad-a088-4856df055ed1',\n", - " 'folders': {}},\n", - " 'applications': {},\n", - " 'services': {},\n", - " 'process': {}},\n", - " 'dfcc395a-93ff-4dd5-9684-c80c5885d827': {'uuid': 'dfcc395a-93ff-4dd5-9684-c80c5885d827',\n", - " 'hostname': 'google_server',\n", - " 'operating_state': 0,\n", - " 'NICs': {},\n", - " 'file_system': {'uuid': 'aea8f406-05de-4a02-b65f-972aa1fed70e',\n", - " 'folders': {}},\n", - " 'applications': {},\n", - " 'services': {},\n", - " 'process': {}}},\n", - " 'links': {}},\n", - " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912', 'accounts': {}}}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "my_pc = Node(hostname=\"primaite_pc\",)\n", "my_server = Node(hostname=\"google_server\")\n", @@ -117,9 +87,7 @@ "# TODO: when there is a proper function for adding nodes, use it instead of manually adding.\n", "\n", "my_sim.network.nodes[my_pc.uuid] = my_pc\n", - "my_sim.network.nodes[my_server.uuid] = my_server\n", - "\n", - "my_sim.describe_state()" + "my_sim.network.nodes[my_server.uuid] = my_server\n" ] }, { @@ -147,10 +115,10 @@ "name": "stderr", "output_type": "stream", "text": [ - "2023-08-20 18:34:59,328: NIC c3:08:90:23:29:cb/130.1.1.1 connected to Link c3:08:90:23:29:cb/130.1.1.1<-->40:4a:3f:2e:ee:2e\n", - "2023-08-20 18:34:59,329: SwitchPort 40:4a:3f:2e:ee:2e connected to Link c3:08:90:23:29:cb/130.1.1.1<-->40:4a:3f:2e:ee:2e\n", - "2023-08-20 18:34:59,331: NIC 69:50:cb:76:22:10/130.1.1.2 connected to Link 69:50:cb:76:22:10/130.1.1.2<-->18:5e:49:ed:21:55\n", - "2023-08-20 18:34:59,331: SwitchPort 18:5e:49:ed:21:55 connected to Link 69:50:cb:76:22:10/130.1.1.2<-->18:5e:49:ed:21:55\n" + "2023-08-20 18:42:51,310: NIC 5c:b6:26:c0:86:61/130.1.1.1 connected to Link 5c:b6:26:c0:86:61/130.1.1.1<-->01:ef:b1:a3:24:72\n", + "2023-08-20 18:42:51,311: SwitchPort 01:ef:b1:a3:24:72 connected to Link 5c:b6:26:c0:86:61/130.1.1.1<-->01:ef:b1:a3:24:72\n", + "2023-08-20 18:42:51,314: NIC f6:de:1e:63:8e:7f/130.1.1.2 connected to Link f6:de:1e:63:8e:7f/130.1.1.2<-->30:9e:c8:d4:5d:f3\n", + "2023-08-20 18:42:51,315: SwitchPort 30:9e:c8:d4:5d:f3 connected to Link f6:de:1e:63:8e:7f/130.1.1.2<-->30:9e:c8:d4:5d:f3\n" ] } ], @@ -172,74 +140,6 @@ "my_sim.network.links[server_to_swtich.uuid] = server_to_swtich" ] }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", - " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", - " 'nodes': {'c7c91f06-f128-4891-84a2-83beceea3908': {'uuid': 'c7c91f06-f128-4891-84a2-83beceea3908',\n", - " 'hostname': 'primaite_pc',\n", - " 'operating_state': 0,\n", - " 'NICs': {'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2': {'uuid': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", - " 'ip_adress': '130.1.1.1',\n", - " 'subnet_mask': '255.255.255.0',\n", - " 'gateway': '130.1.1.255',\n", - " 'mac_address': 'c3:08:90:23:29:cb',\n", - " 'speed': 100,\n", - " 'mtu': 1500,\n", - " 'wake_on_lan': False,\n", - " 'dns_servers': [],\n", - " 'enabled': False}},\n", - " 'file_system': {'uuid': '04ffd1e8-dea7-47ad-a088-4856df055ed1',\n", - " 'folders': {}},\n", - " 'applications': {},\n", - " 'services': {},\n", - " 'process': {}},\n", - " 'dfcc395a-93ff-4dd5-9684-c80c5885d827': {'uuid': 'dfcc395a-93ff-4dd5-9684-c80c5885d827',\n", - " 'hostname': 'google_server',\n", - " 'operating_state': 0,\n", - " 'NICs': {'1fd281a0-83ae-49d9-9b40-6aae7b465cab': {'uuid': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", - " 'ip_adress': '130.1.1.2',\n", - " 'subnet_mask': '255.255.255.0',\n", - " 'gateway': '130.1.1.255',\n", - " 'mac_address': '69:50:cb:76:22:10',\n", - " 'speed': 100,\n", - " 'mtu': 1500,\n", - " 'wake_on_lan': False,\n", - " 'dns_servers': [],\n", - " 'enabled': False}},\n", - " 'file_system': {'uuid': 'aea8f406-05de-4a02-b65f-972aa1fed70e',\n", - " 'folders': {}},\n", - " 'applications': {},\n", - " 'services': {},\n", - " 'process': {}}},\n", - " 'links': {'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9': {'uuid': 'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9',\n", - " 'endpoint_a': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", - " 'endpoint_b': '4e6abc87-b4b9-4f95-a9a9-59cac130c6ff',\n", - " 'bandwidth': 100.0,\n", - " 'current_load': 0.0},\n", - " '2dab7fc3-470d-44d2-8593-feb8e96d71ae': {'uuid': '2dab7fc3-470d-44d2-8593-feb8e96d71ae',\n", - " 'endpoint_a': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", - " 'endpoint_b': 'e136553f-333e-4abf-b1f3-ce352ffa4630',\n", - " 'bandwidth': 100.0,\n", - " 'current_load': 0.0}}},\n", - " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912', 'accounts': {}}}" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "my_sim.describe_state()" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -249,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -259,7 +159,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -269,16 +169,16 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "FileSystemFile(uuid='3ecf7223-dafd-4973-8c3b-b85af4e177da', name='favicon.ico', size=40.0, file_type=, action_manager=None)" + "FileSystemFile(uuid='253e4606-0f6d-4e57-8db0-6fa7e331ecea', name='favicon.ico', size=40.0, file_type=, action_manager=None)" ] }, - "execution_count": 10, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -297,7 +197,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -305,6 +205,7 @@ "from primaite.simulator.system.software import SoftwareHealthState, SoftwareCriticality\n", "from primaite.simulator.network.transmission.transport_layer import Port\n", "\n", + "# no applications exist yet so we will create our own.\n", "class MSPaint(Application):\n", " def describe_state(self):\n", " return super().describe_state()" @@ -312,7 +213,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -321,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -337,7 +238,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -346,7 +247,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -354,39 +255,46 @@ "my_sim.domain.accounts[acct.uuid] = acct" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Verify that the state dictionary contains no non-serialisable objects." + ] + }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'uuid': 'f0c06262-1bd9-49ee-81f8-793fb4a5e58e',\n", - " 'network': {'uuid': '455d6a1a-ca23-4135-b326-3ebf75022a45',\n", - " 'nodes': {'c7c91f06-f128-4891-84a2-83beceea3908': {'uuid': 'c7c91f06-f128-4891-84a2-83beceea3908',\n", + "{'uuid': '5304ed6d-de4c-408c-ae24-ada32852d196',\n", + " 'network': {'uuid': 'fa17dfe8-81a1-4c7f-8c5b-8c2d3b1e8756',\n", + " 'nodes': {'1fa46446-6681-4e25-a3ba-c4c2cc564630': {'uuid': '1fa46446-6681-4e25-a3ba-c4c2cc564630',\n", " 'hostname': 'primaite_pc',\n", " 'operating_state': 0,\n", - " 'NICs': {'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2': {'uuid': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", + " 'NICs': {'09ca02eb-7733-492c-9eff-f0d6b6ebeeda': {'uuid': '09ca02eb-7733-492c-9eff-f0d6b6ebeeda',\n", " 'ip_adress': '130.1.1.1',\n", " 'subnet_mask': '255.255.255.0',\n", " 'gateway': '130.1.1.255',\n", - " 'mac_address': 'c3:08:90:23:29:cb',\n", + " 'mac_address': '5c:b6:26:c0:86:61',\n", " 'speed': 100,\n", " 'mtu': 1500,\n", " 'wake_on_lan': False,\n", " 'dns_servers': [],\n", " 'enabled': False}},\n", - " 'file_system': {'uuid': '04ffd1e8-dea7-47ad-a088-4856df055ed1',\n", - " 'folders': {'f1fdf2ae-6377-4417-a28a-3edb4058712d': {'uuid': 'f1fdf2ae-6377-4417-a28a-3edb4058712d',\n", + " 'file_system': {'uuid': '8b533e31-04e9-4838-839d-0656ace3e57a',\n", + " 'folders': {'b450c223-872c-4fe0-90cc-9da80973eaad': {'uuid': 'b450c223-872c-4fe0-90cc-9da80973eaad',\n", " 'name': 'downloads',\n", " 'size': 1000.0,\n", - " 'files': {'409b09a3-0d98-4c03-adf2-09190539be45': {'uuid': '409b09a3-0d98-4c03-adf2-09190539be45',\n", + " 'files': {'8160e685-a76f-4171-8a12-3d6b32a9ea16': {'uuid': '8160e685-a76f-4171-8a12-3d6b32a9ea16',\n", " 'name': 'firefox_installer.zip',\n", " 'size': 1000.0,\n", " 'file_type': 'ZIP'}},\n", " 'is_quarantined': False}}},\n", - " 'applications': {'cddee888-d1b9-4289-8512-bc0a6672c880': {'uuid': 'cddee888-d1b9-4289-8512-bc0a6672c880',\n", + " 'applications': {'c82f1064-f35e-466b-88ae-3f61ba0e5161': {'uuid': 'c82f1064-f35e-466b-88ae-3f61ba0e5161',\n", " 'health_state': 'GOOD',\n", " 'health_state_red_view': 'GOOD',\n", " 'criticality': 'MEDIUM',\n", @@ -404,29 +312,29 @@ " 'groups': []}},\n", " 'services': {},\n", " 'process': {}},\n", - " 'dfcc395a-93ff-4dd5-9684-c80c5885d827': {'uuid': 'dfcc395a-93ff-4dd5-9684-c80c5885d827',\n", + " '7f637689-6f91-4026-a685-48a9067f03e8': {'uuid': '7f637689-6f91-4026-a685-48a9067f03e8',\n", " 'hostname': 'google_server',\n", " 'operating_state': 0,\n", - " 'NICs': {'1fd281a0-83ae-49d9-9b40-6aae7b465cab': {'uuid': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", + " 'NICs': {'1abc7272-c516-4463-bd07-1a3cefe39313': {'uuid': '1abc7272-c516-4463-bd07-1a3cefe39313',\n", " 'ip_adress': '130.1.1.2',\n", " 'subnet_mask': '255.255.255.0',\n", " 'gateway': '130.1.1.255',\n", - " 'mac_address': '69:50:cb:76:22:10',\n", + " 'mac_address': 'f6:de:1e:63:8e:7f',\n", " 'speed': 100,\n", " 'mtu': 1500,\n", " 'wake_on_lan': False,\n", " 'dns_servers': [],\n", " 'enabled': False}},\n", - " 'file_system': {'uuid': 'aea8f406-05de-4a02-b65f-972aa1fed70e',\n", - " 'folders': {'beb5b535-cf6c-431d-94f6-d1097910130d': {'uuid': 'beb5b535-cf6c-431d-94f6-d1097910130d',\n", + " 'file_system': {'uuid': 'ac9a6643-8349-4f7a-98c7-a1a9f97ce123',\n", + " 'folders': {'befa5d92-0878-4da2-9dac-f993c0b4a554': {'uuid': 'befa5d92-0878-4da2-9dac-f993c0b4a554',\n", " 'name': 'static',\n", " 'size': 0,\n", " 'files': {},\n", " 'is_quarantined': False},\n", - " '6644cd6c-1eca-4fe4-9313-e3481abb895e': {'uuid': '6644cd6c-1eca-4fe4-9313-e3481abb895e',\n", + " '27383b5e-8884-4ec0-bb50-a5d43e460dfa': {'uuid': '27383b5e-8884-4ec0-bb50-a5d43e460dfa',\n", " 'name': 'root',\n", " 'size': 40.0,\n", - " 'files': {'3ecf7223-dafd-4973-8c3b-b85af4e177da': {'uuid': '3ecf7223-dafd-4973-8c3b-b85af4e177da',\n", + " 'files': {'253e4606-0f6d-4e57-8db0-6fa7e331ecea': {'uuid': '253e4606-0f6d-4e57-8db0-6fa7e331ecea',\n", " 'name': 'favicon.ico',\n", " 'size': 40.0,\n", " 'file_type': 'PNG'}},\n", @@ -434,18 +342,18 @@ " 'applications': {},\n", " 'services': {},\n", " 'process': {}}},\n", - " 'links': {'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9': {'uuid': 'cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9',\n", - " 'endpoint_a': 'f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2',\n", - " 'endpoint_b': '4e6abc87-b4b9-4f95-a9a9-59cac130c6ff',\n", + " 'links': {'a449b1ff-50d9-4342-861e-44f2d4dfef37': {'uuid': 'a449b1ff-50d9-4342-861e-44f2d4dfef37',\n", + " 'endpoint_a': '09ca02eb-7733-492c-9eff-f0d6b6ebeeda',\n", + " 'endpoint_b': 'ee4557d9-a309-45dd-a6e0-5b572cc70ee5',\n", " 'bandwidth': 100.0,\n", " 'current_load': 0.0},\n", - " '2dab7fc3-470d-44d2-8593-feb8e96d71ae': {'uuid': '2dab7fc3-470d-44d2-8593-feb8e96d71ae',\n", - " 'endpoint_a': '1fd281a0-83ae-49d9-9b40-6aae7b465cab',\n", - " 'endpoint_b': 'e136553f-333e-4abf-b1f3-ce352ffa4630',\n", + " 'ebd7687b-ec69-4f1b-b2ba-86669aa95723': {'uuid': 'ebd7687b-ec69-4f1b-b2ba-86669aa95723',\n", + " 'endpoint_a': '1abc7272-c516-4463-bd07-1a3cefe39313',\n", + " 'endpoint_b': 'dc26b764-a07e-486a-99a4-798c8e0c187a',\n", " 'bandwidth': 100.0,\n", " 'current_load': 0.0}}},\n", - " 'domain': {'uuid': '9da912d5-4c07-4df6-94c2-b3630e178912',\n", - " 'accounts': {'563a1805-0b32-4ba6-9551-e127e5eb57a8': {'uuid': '563a1805-0b32-4ba6-9551-e127e5eb57a8',\n", + " 'domain': {'uuid': '320cbb83-eb1b-4911-a4f0-fc46d8038a8a',\n", + " 'accounts': {'5fdcfb66-84f3-4f0f-a3a7-d0cb0e1a5d51': {'uuid': '5fdcfb66-84f3-4f0f-a3a7-d0cb0e1a5d51',\n", " 'num_logons': 0,\n", " 'num_logoffs': 0,\n", " 'num_group_changes': 0,\n", @@ -455,7 +363,7 @@ " 'enabled': True}}}}" ] }, - "execution_count": 16, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -464,41 +372,25 @@ "my_sim.describe_state()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Verify that the state dictionary contains no non-serialisable objects." - ] - }, { "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "import json" - ] - }, - { - "cell_type": "code", - "execution_count": 22, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'{\"uuid\": \"f0c06262-1bd9-49ee-81f8-793fb4a5e58e\", \"network\": {\"uuid\": \"455d6a1a-ca23-4135-b326-3ebf75022a45\", \"nodes\": {\"c7c91f06-f128-4891-84a2-83beceea3908\": {\"uuid\": \"c7c91f06-f128-4891-84a2-83beceea3908\", \"hostname\": \"primaite_pc\", \"operating_state\": 0, \"NICs\": {\"f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2\": {\"uuid\": \"f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2\", \"ip_adress\": \"130.1.1.1\", \"subnet_mask\": \"255.255.255.0\", \"gateway\": \"130.1.1.255\", \"mac_address\": \"c3:08:90:23:29:cb\", \"speed\": 100, \"mtu\": 1500, \"wake_on_lan\": false, \"dns_servers\": [], \"enabled\": false}}, \"file_system\": {\"uuid\": \"04ffd1e8-dea7-47ad-a088-4856df055ed1\", \"folders\": {\"f1fdf2ae-6377-4417-a28a-3edb4058712d\": {\"uuid\": \"f1fdf2ae-6377-4417-a28a-3edb4058712d\", \"name\": \"downloads\", \"size\": 1000.0, \"files\": {\"409b09a3-0d98-4c03-adf2-09190539be45\": {\"uuid\": \"409b09a3-0d98-4c03-adf2-09190539be45\", \"name\": \"firefox_installer.zip\", \"size\": 1000.0, \"file_type\": \"ZIP\"}}, \"is_quarantined\": false}}}, \"applications\": {\"cddee888-d1b9-4289-8512-bc0a6672c880\": {\"uuid\": \"cddee888-d1b9-4289-8512-bc0a6672c880\", \"health_state\": \"GOOD\", \"health_state_red_view\": \"GOOD\", \"criticality\": \"MEDIUM\", \"patching_count\": 0, \"scanning_count\": 0, \"revealed_to_red\": false, \"installing_count\": 0, \"max_sessions\": 1, \"tcp\": true, \"udp\": true, \"ports\": [\"HTTP\"], \"opearting_state\": \"RUNNING\", \"execution_control_status\": \"manual\", \"num_executions\": 0, \"groups\": []}}, \"services\": {}, \"process\": {}}, \"dfcc395a-93ff-4dd5-9684-c80c5885d827\": {\"uuid\": \"dfcc395a-93ff-4dd5-9684-c80c5885d827\", \"hostname\": \"google_server\", \"operating_state\": 0, \"NICs\": {\"1fd281a0-83ae-49d9-9b40-6aae7b465cab\": {\"uuid\": \"1fd281a0-83ae-49d9-9b40-6aae7b465cab\", \"ip_adress\": \"130.1.1.2\", \"subnet_mask\": \"255.255.255.0\", \"gateway\": \"130.1.1.255\", \"mac_address\": \"69:50:cb:76:22:10\", \"speed\": 100, \"mtu\": 1500, \"wake_on_lan\": false, \"dns_servers\": [], \"enabled\": false}}, \"file_system\": {\"uuid\": \"aea8f406-05de-4a02-b65f-972aa1fed70e\", \"folders\": {\"beb5b535-cf6c-431d-94f6-d1097910130d\": {\"uuid\": \"beb5b535-cf6c-431d-94f6-d1097910130d\", \"name\": \"static\", \"size\": 0, \"files\": {}, \"is_quarantined\": false}, \"6644cd6c-1eca-4fe4-9313-e3481abb895e\": {\"uuid\": \"6644cd6c-1eca-4fe4-9313-e3481abb895e\", \"name\": \"root\", \"size\": 40.0, \"files\": {\"3ecf7223-dafd-4973-8c3b-b85af4e177da\": {\"uuid\": \"3ecf7223-dafd-4973-8c3b-b85af4e177da\", \"name\": \"favicon.ico\", \"size\": 40.0, \"file_type\": \"PNG\"}}, \"is_quarantined\": false}}}, \"applications\": {}, \"services\": {}, \"process\": {}}}, \"links\": {\"cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9\": {\"uuid\": \"cfbf3c88-c2ac-40f2-9466-a92b99f7cfc9\", \"endpoint_a\": \"f181ea2b-59b6-4724-acf1-a8a7d4e2a1b2\", \"endpoint_b\": \"4e6abc87-b4b9-4f95-a9a9-59cac130c6ff\", \"bandwidth\": 100.0, \"current_load\": 0.0}, \"2dab7fc3-470d-44d2-8593-feb8e96d71ae\": {\"uuid\": \"2dab7fc3-470d-44d2-8593-feb8e96d71ae\", \"endpoint_a\": \"1fd281a0-83ae-49d9-9b40-6aae7b465cab\", \"endpoint_b\": \"e136553f-333e-4abf-b1f3-ce352ffa4630\", \"bandwidth\": 100.0, \"current_load\": 0.0}}}, \"domain\": {\"uuid\": \"9da912d5-4c07-4df6-94c2-b3630e178912\", \"accounts\": {\"563a1805-0b32-4ba6-9551-e127e5eb57a8\": {\"uuid\": \"563a1805-0b32-4ba6-9551-e127e5eb57a8\", \"num_logons\": 0, \"num_logoffs\": 0, \"num_group_changes\": 0, \"username\": \"admin\", \"password\": \"admin12\", \"account_type\": \"USER\", \"enabled\": true}}}}'" + "'{\"uuid\": \"5304ed6d-de4c-408c-ae24-ada32852d196\", \"network\": {\"uuid\": \"fa17dfe8-81a1-4c7f-8c5b-8c2d3b1e8756\", \"nodes\": {\"1fa46446-6681-4e25-a3ba-c4c2cc564630\": {\"uuid\": \"1fa46446-6681-4e25-a3ba-c4c2cc564630\", \"hostname\": \"primaite_pc\", \"operating_state\": 0, \"NICs\": {\"09ca02eb-7733-492c-9eff-f0d6b6ebeeda\": {\"uuid\": \"09ca02eb-7733-492c-9eff-f0d6b6ebeeda\", \"ip_adress\": \"130.1.1.1\", \"subnet_mask\": \"255.255.255.0\", \"gateway\": \"130.1.1.255\", \"mac_address\": \"5c:b6:26:c0:86:61\", \"speed\": 100, \"mtu\": 1500, \"wake_on_lan\": false, \"dns_servers\": [], \"enabled\": false}}, \"file_system\": {\"uuid\": \"8b533e31-04e9-4838-839d-0656ace3e57a\", \"folders\": {\"b450c223-872c-4fe0-90cc-9da80973eaad\": {\"uuid\": \"b450c223-872c-4fe0-90cc-9da80973eaad\", \"name\": \"downloads\", \"size\": 1000.0, \"files\": {\"8160e685-a76f-4171-8a12-3d6b32a9ea16\": {\"uuid\": \"8160e685-a76f-4171-8a12-3d6b32a9ea16\", \"name\": \"firefox_installer.zip\", \"size\": 1000.0, \"file_type\": \"ZIP\"}}, \"is_quarantined\": false}}}, \"applications\": {\"c82f1064-f35e-466b-88ae-3f61ba0e5161\": {\"uuid\": \"c82f1064-f35e-466b-88ae-3f61ba0e5161\", \"health_state\": \"GOOD\", \"health_state_red_view\": \"GOOD\", \"criticality\": \"MEDIUM\", \"patching_count\": 0, \"scanning_count\": 0, \"revealed_to_red\": false, \"installing_count\": 0, \"max_sessions\": 1, \"tcp\": true, \"udp\": true, \"ports\": [\"HTTP\"], \"opearting_state\": \"RUNNING\", \"execution_control_status\": \"manual\", \"num_executions\": 0, \"groups\": []}}, \"services\": {}, \"process\": {}}, \"7f637689-6f91-4026-a685-48a9067f03e8\": {\"uuid\": \"7f637689-6f91-4026-a685-48a9067f03e8\", \"hostname\": \"google_server\", \"operating_state\": 0, \"NICs\": {\"1abc7272-c516-4463-bd07-1a3cefe39313\": {\"uuid\": \"1abc7272-c516-4463-bd07-1a3cefe39313\", \"ip_adress\": \"130.1.1.2\", \"subnet_mask\": \"255.255.255.0\", \"gateway\": \"130.1.1.255\", \"mac_address\": \"f6:de:1e:63:8e:7f\", \"speed\": 100, \"mtu\": 1500, \"wake_on_lan\": false, \"dns_servers\": [], \"enabled\": false}}, \"file_system\": {\"uuid\": \"ac9a6643-8349-4f7a-98c7-a1a9f97ce123\", \"folders\": {\"befa5d92-0878-4da2-9dac-f993c0b4a554\": {\"uuid\": \"befa5d92-0878-4da2-9dac-f993c0b4a554\", \"name\": \"static\", \"size\": 0, \"files\": {}, \"is_quarantined\": false}, \"27383b5e-8884-4ec0-bb50-a5d43e460dfa\": {\"uuid\": \"27383b5e-8884-4ec0-bb50-a5d43e460dfa\", \"name\": \"root\", \"size\": 40.0, \"files\": {\"253e4606-0f6d-4e57-8db0-6fa7e331ecea\": {\"uuid\": \"253e4606-0f6d-4e57-8db0-6fa7e331ecea\", \"name\": \"favicon.ico\", \"size\": 40.0, \"file_type\": \"PNG\"}}, \"is_quarantined\": false}}}, \"applications\": {}, \"services\": {}, \"process\": {}}}, \"links\": {\"a449b1ff-50d9-4342-861e-44f2d4dfef37\": {\"uuid\": \"a449b1ff-50d9-4342-861e-44f2d4dfef37\", \"endpoint_a\": \"09ca02eb-7733-492c-9eff-f0d6b6ebeeda\", \"endpoint_b\": \"ee4557d9-a309-45dd-a6e0-5b572cc70ee5\", \"bandwidth\": 100.0, \"current_load\": 0.0}, \"ebd7687b-ec69-4f1b-b2ba-86669aa95723\": {\"uuid\": \"ebd7687b-ec69-4f1b-b2ba-86669aa95723\", \"endpoint_a\": \"1abc7272-c516-4463-bd07-1a3cefe39313\", \"endpoint_b\": \"dc26b764-a07e-486a-99a4-798c8e0c187a\", \"bandwidth\": 100.0, \"current_load\": 0.0}}}, \"domain\": {\"uuid\": \"320cbb83-eb1b-4911-a4f0-fc46d8038a8a\", \"accounts\": {\"5fdcfb66-84f3-4f0f-a3a7-d0cb0e1a5d51\": {\"uuid\": \"5fdcfb66-84f3-4f0f-a3a7-d0cb0e1a5d51\", \"num_logons\": 0, \"num_logoffs\": 0, \"num_group_changes\": 0, \"username\": \"admin\", \"password\": \"admin12\", \"account_type\": \"USER\", \"enabled\": true}}}}'" ] }, - "execution_count": 22, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "d = my_sim.describe_state()\n", - "json.dumps(d)" + "import json\n", + "json.dumps(my_sim.describe_state())" ] } ], From 07b740a81e9872a62aa3326520fa50421c2c6186 Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 21 Aug 2023 09:49:31 +0100 Subject: [PATCH 6/7] Update docs and changelog. --- CHANGELOG.md | 2 ++ docs/_static/component_relationship.png | Bin 0 -> 82446 bytes docs/source/simulation_structure.rst | 14 +++++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 docs/_static/component_relationship.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3f57fb..2b495c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ a Service/Application another machine. SessionManager. - Permission System - each action can define criteria that will be used to permit or deny agent actions. - File System - ability to emulate a node's file system during a simulation +- Example notebooks - There is currently 1 jupyter notebook which walks through using PrimAITE + 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) ## [2.0.0] - 2023-07-26 diff --git a/docs/_static/component_relationship.png b/docs/_static/component_relationship.png new file mode 100644 index 0000000000000000000000000000000000000000..c2dd1102d203e7f99081dc1e0f953729aaa2e314 GIT binary patch literal 82446 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 =|845Vs4X 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 literal 0 HcmV?d00001 diff --git a/docs/source/simulation_structure.rst b/docs/source/simulation_structure.rst index 2b213f16..7630ae0f 100644 --- a/docs/source/simulation_structure.rst +++ b/docs/source/simulation_structure.rst @@ -7,11 +7,19 @@ Simulation Structure ==================== The simulation is made up of many smaller components which are related to each other in a tree-like structure. At the -top level, there is an object called the ``SimulationController`` _(doesn't exist yet)_, which has a physical network -and a software controller for managing software and users. +top level, there is the :py:meth:`primaite.simulator.sim_container.Simulation`, which keeps track of the physical network +and a domain controller for managing software and users. -Each node of the simulation 'tree' has responsibility for creating, deleting, and updating its direct descendants. +Each node of the simulation 'tree' has responsibility for creating, deleting, and updating its direct descendants. Also, +when a component's ``describe_state()`` method is called, it will include the state of its descendants. The +``apply_action()`` method can be used to act on a component or one of its descendatnts. The diagram below shows the +relationship between components. +.. image:: _static/component_relationship.png + :width: 500 + :alt: The top level simulation object owns a NetworkContainer and a DomainController. The DomainController has a + list of accounts. The network container has links and nodes. Nodes can own switchports, NICs, FileSystem, + Application, Service, and Process. Actions From 7e64acd36840363be507ea36924d4eeb93f2a07e Mon Sep 17 00:00:00 2001 From: Marek Wolan Date: Mon, 21 Aug 2023 10:04:23 +0100 Subject: [PATCH 7/7] Update container docstrings --- src/primaite/simulator/network/container.py | 3 ++- src/primaite/simulator/sim_container.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/primaite/simulator/network/container.py b/src/primaite/simulator/network/container.py index 463d5f91..f89ed2d3 100644 --- a/src/primaite/simulator/network/container.py +++ b/src/primaite/simulator/network/container.py @@ -5,12 +5,13 @@ from primaite.simulator.network.hardware.base import Link, Node class NetworkContainer(SimComponent): - """TODO.""" + """Top level container object representing the physical network.""" nodes: Dict[str, Node] = {} links: Dict[str, Link] = {} def __init__(self, **kwargs): + """Initialise the network.""" super().__init__(**kwargs) self.action_manager = ActionManager() diff --git a/src/primaite/simulator/sim_container.py b/src/primaite/simulator/sim_container.py index 1a37dc18..50fe412c 100644 --- a/src/primaite/simulator/sim_container.py +++ b/src/primaite/simulator/sim_container.py @@ -6,12 +6,13 @@ from primaite.simulator.network.container import NetworkContainer class Simulation(SimComponent): - """TODO.""" + """Top-level simulation object which holds a reference to all other parts of the simulation.""" network: NetworkContainer domain: DomainController def __init__(self, **kwargs): + """Initialise the Simulation.""" if not kwargs.get("network"): kwargs["network"] = NetworkContainer()