Merge branch 'feature/#3110-userguide-fixes' of ssh.dev.azure.com:v3/ma-dev-uk/PrimAITE/PrimAITE into feature/#3110-userguide-fixes

This commit is contained in:
Nick Todd
2025-03-13 14:56:20 +00:00
9 changed files with 60 additions and 71 deletions

View File

@@ -8,16 +8,18 @@ SOURCEDIR = .
BUILDDIR = _build BUILDDIR = _build
AUTOSUMMARY="source/_autosummary" AUTOSUMMARY="source/_autosummary"
NOTEBOOKS="source/notebooks/notebooks"
# Remove command is different depending on OS # Remove command is different depending on OS
ifdef OS ifdef OS
RM = IF exist $(AUTOSUMMARY) ( RMDIR $(AUTOSUMMARY) /s /q ) RM = IF exist $(AUTOSUMMARY) (RMDIR $(AUTOSUMMARY) /s /q) & IF exist $(NOTEBOOKS) (RMDIR $(NOTEBOOKS) /s /q)
else else
ifeq ($(shell uname), Linux) ifeq ($(shell uname), Linux)
RM = rm -rf $(AUTOSUMMARY) RM = rm -rf $(AUTOSUMMARY) $(NOTEBOOKS)
endif endif
endif endif
# Put it first so that "make" without argument is like "make help". # Put it first so that "make" without argument is like "make help".
help: help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -43,7 +43,6 @@ What is PrimAITE?
source/simulation source/simulation
source/config source/config
source/rewards source/rewards
source/customising_scenarios
source/varying_config_files source/varying_config_files
source/environment source/environment
source/action_masking source/action_masking

View File

@@ -1,8 +0,0 @@
.. only:: comment
© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK
Customising Agents
******************
For an example of how to customise red agent behaviour in the Data Manipulation scenario, please refer to the notebook ``Data-Manipulation-Customising-Red-Agent.ipynb``.

View File

@@ -9,8 +9,8 @@ Rewards
Rewards in PrimAITE are based on a system of individual components that react to events in the simulation. An agent's reward function is calculated as the weighted sum of several reward components. Rewards in PrimAITE are based on a system of individual components that react to events in the simulation. An agent's reward function is calculated as the weighted sum of several reward components.
Some rewards, such as the ``GreenAdminDatabaseUnreachablePenalty``, can be marked as 'sticky' in their configuration. Setting this to ``True`` will mean that they continue to output the same value after an event until another event of that type. Some rewards, such as the ``green-admin-database-unreachable-penalty``, can be marked as 'sticky' in their configuration. Setting this to ``True`` will mean that they continue to output the same value after an event until another event of that type.
In the instance of the ``GreenAdminDatabaseUnreachablePenalty``, the database admin reward will stay negative until the next successful database request is made, even if the database admin agents do nothing and the database returns a good state. In the instance of the ``green-admin-database-unreachable-penalty``, the database admin reward will stay negative until the next successful database request is made, even if the database admin agents do nothing and the database returns a good state.
Components Components
********** **********

View File

@@ -64,7 +64,7 @@ other wireless devices within the same frequency band.
Example Scenario Example Scenario
---------------- ----------------
This example sets up a network with two PCs (PC A and PC B), each connected to their own `WirelessRouter` This example sets up a network with two PCs (PC A and PC B), each connected to their own ``WirelessRouter``
(Router 1 and Router 2). These routers are then wirelessly connected to each other, enabling communication between the (Router 1 and Router 2). These routers are then wirelessly connected to each other, enabling communication between the
PCs through the routers over the airspace. Access Control Lists (ACLs) are configured on the routers to permit ARP and PCs through the routers over the airspace. Access Control Lists (ACLs) are configured on the routers to permit ARP and
ICMP traffic, ensuring basic network connectivity and ping functionality. ICMP traffic, ensuring basic network connectivity and ping functionality.
@@ -160,7 +160,7 @@ network segments.
Viewing Wireless Network Configuration Viewing Wireless Network Configuration
-------------------------------------- --------------------------------------
The `AirSpace.show()` function is an invaluable tool for inspecting the current wireless network configuration within The :py:meth:`AirSpace.show() <primaite.simulator.network.airspace.AirSpace.show()>` function is an invaluable tool for inspecting the current wireless network configuration within
the PrimAITE environment. It presents a table summarising all wireless interfaces, including routers and access points, the PrimAITE environment. It presents a table summarising all wireless interfaces, including routers and access points,
that are active within the airspace. The table outlines each device's connected node name, MAC address, IP address, that are active within the airspace. The table outlines each device's connected node name, MAC address, IP address,
subnet mask, operating frequency, and status, providing a comprehensive view of the wireless network topology. subnet mask, operating frequency, and status, providing a comprehensive view of the wireless network topology.
@@ -168,7 +168,7 @@ subnet mask, operating frequency, and status, providing a comprehensive view of
Example Output Example Output
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
Below is an example output of the `AirSpace.show()` function, demonstrating the visibility it provides into the Below is an example output of the :py:meth:`AirSpace.show() <primaite.simulator.network.airspace.AirSpace.show()>` function, demonstrating the visibility it provides into the
wireless network: wireless network:
.. code-block:: none .. code-block:: none
@@ -182,10 +182,10 @@ wireless network:
This table aids in verifying that wireless devices are correctly configured and operational. It also helps in This table aids in verifying that wireless devices are correctly configured and operational. It also helps in
diagnosing connectivity issues by ensuring that devices are on the correct frequency and have the appropriate network diagnosing connectivity issues by ensuring that devices are on the correct frequency and have the appropriate network
settings. The `Status` column, indicating whether a device is enabled or disabled, further assists in troubleshooting settings. The ``Status`` column, indicating whether a device is enabled or disabled, further assists in troubleshooting
by quickly identifying any devices that are not actively participating in the network. by quickly identifying any devices that are not actively participating in the network.
Utilising the `AirSpace.show()` function is particularly beneficial in complex network simulations where multiple Utilising the :py:meth:`AirSpace.show() <primaite.simulator.network.airspace.AirSpace.show()>` function is particularly beneficial in complex network simulations where multiple
wireless devices are in use. It provides a snapshot of the wireless landscape, facilitating the understanding of how wireless devices are in use. It provides a snapshot of the wireless landscape, facilitating the understanding of how
devices interact within the network and ensuring that configurations are aligned with the intended network architecture. devices interact within the network and ensuring that configurations are aligned with the intended network architecture.

View File

@@ -14,9 +14,11 @@ Transport Layer (Layer 4)
**UDPHeader:** Represents a UDP header for the transport layer of a Network Frame. It includes source and destination **UDPHeader:** Represents a UDP header for the transport layer of a Network Frame. It includes source and destination
ports. UDP (User Datagram Protocol) is a connectionless and unreliable transport protocol used for data transmission. ports. UDP (User Datagram Protocol) is a connectionless and unreliable transport protocol used for data transmission.
**TCPFlags:** Enum representing TCP control flags used in a TCP connection, such as SYN, ACK, FIN, and RST. TCP ..
(Transmission Control Protocol) is a connection-oriented and reliable transport protocol used for establishing and **TCPFlags:** Enum representing TCP control flags used in a TCP connection, such as SYN, ACK, FIN, and RST. TCP
maintaining data streams. (Transmission Control Protocol) is a connection-oriented and reliable transport protocol used for establishing and
maintaining data streams.
.. not currently used
**TCPHeader:** Represents a TCP header for the transport layer of a Network Frame. It includes source and destination **TCPHeader:** Represents a TCP header for the transport layer of a Network Frame. It includes source and destination
ports and TCP flags. This header is used for establishing and managing TCP connections. ports and TCP flags. This header is used for establishing and managing TCP connections.

View File

@@ -77,9 +77,9 @@ Adding to this, the following behaviour of the C2 beacon can be configured by us
+---------------------+---------------------------------------------------------------------------+ +---------------------+---------------------------------------------------------------------------+
|keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. | |keep_alive_frequency | How often should the C2 Beacon confirm it's connection in timesteps. |
+---------------------+---------------------------------------------------------------------------+ +---------------------+---------------------------------------------------------------------------+
|masquerade_protocol | What protocol should the C2 traffic masquerade as? (HTTP, FTP or DNS) | |masquerade_protocol | What protocol should the C2 traffic masquerade as? (TCP opr UDP) |
+---------------------+---------------------------------------------------------------------------+ +---------------------+---------------------------------------------------------------------------+
|masquerade_port | What port should the C2 traffic use? (TCP or UDP) | |masquerade_port | What port should the C2 traffic use? (HTTP, FTP, or DNS) |
+---------------------+---------------------------------------------------------------------------+ +---------------------+---------------------------------------------------------------------------+
@@ -115,38 +115,30 @@ Python
"""""" """"""
.. code-block:: python .. code-block:: python
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.network.hardware.nodes.network.switch import Switch
from primaite.simulator.system.applications.database_client import DatabaseClient
from primaite.simulator.system.applications.red_applications.ransomware_script import RansomwareScript
from primaite.simulator.system.services.database.database_service import DatabaseService
from primaite.simulator.system.applications.red_applications.c2.c2_server import C2Command, C2Server
from primaite.simulator.system.applications.red_applications.c2.c2_beacon import C2Beacon
# Network Setup # Network Setup
network = Network() network = Network()
switch = Switch(config={"hostname":"switch", "start_up_duration":0, "num_ports":4}) switch = Switch(config=Switch.ConfigSchema(hostname="switch", start_up_duration=0, num_ports=4))
switch.power_on() switch.power_on()
node_a = Computer(config={"hostname":"node_a", "ip_address":"192.168.0.10", "subnet_mask":"255.255.255.0", "start_up_duration":0}) node_a = Computer(config=Computer.ConfigSchema(hostname="node_a", ip_address="192.168.0.10", subnet_mask="255.255.255.0", start_up_duration=0))
node_a.power_on() node_a.power_on()
network.connect(node_a.network_interface[1], switch.network_interface[1]) network.connect(node_a.network_interface[1], switch.network_interface[1])
node_b = Computer(config={"hostname":"node_b", "ip_address":"192.168.0.11", "subnet_mask":"255.255.255.0", "start_up_duration":0}) node_b = Computer(config=Computer.ConfigSchema(hostname="node_b", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0))
node_b.power_on() node_b.power_on()
network.connect(node_b.network_interface[1], switch.network_interface[2]) network.connect(node_b.network_interface[1], switch.network_interface[2])
node_c = Computer(config={"hostname":"node_c", "ip_address":"192.168.0.12", "subnet_mask":"255.255.255.0", "start_up_duration":0}) node_c = Computer(config=Computer.ConfigSchema(hostname="node_c", ip_address="192.168.0.12", subnet_mask="255.255.255.0", start_up_duration=0))
node_c.power_on() node_c.power_on()
network.connect(node_c.network_interface[1], switch.network_interface[3]) network.connect(node_c.network_interface[1], switch.network_interface[3])
node_c.software_manager.install(software_class=DatabaseService) node_c.software_manager.install(software_class=DatabaseService)
node_b.software_manager.install(software_class=DatabaseClient) node_b.software_manager.install(software_class=DatabaseClient)
node_b.software_manager.install(software_class=RansomwareScript) node_b.software_manager.install(software_class=RansomwareScript)
node_b.software_manager.install(software_class=C2Beacon)
node_a.software_manager.install(software_class=C2Server) node_a.software_manager.install(software_class=C2Server)
# C2 Application objects # C2 Application objects
@@ -154,8 +146,8 @@ Python
c2_server_host: Computer = network.get_node_by_hostname("node_a") c2_server_host: Computer = network.get_node_by_hostname("node_a")
c2_beacon_host: Computer = network.get_node_by_hostname("node_b") c2_beacon_host: Computer = network.get_node_by_hostname("node_b")
c2_server: C2Server = c2_server_host.software_manager.software["C2Server"] c2_server: C2Server = c2_server_host.software_manager.software["c2-server"]
c2_beacon: C2Beacon = c2_beacon_host.software_manager.software["C2Beacon"] c2_beacon: C2Beacon = c2_beacon_host.software_manager.software["c2-beacon"]
# Configuring the C2 Beacon # Configuring the C2 Beacon
c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=5) c2_beacon.configure(c2_server_ip_address="192.168.0.10", keep_alive_frequency=5)
@@ -287,8 +279,6 @@ It's worth noting that this may be a useful option to bypass ACL rules.
This must be a string i.e *UDP*. Defaults to ``TCP``. This must be a string i.e *UDP*. Defaults to ``TCP``.
*Please refer to the ``IPProtocol`` class for further reference.*
``Masquerade Port`` ``Masquerade Port``
""""""""""""""""""" """""""""""""""""""
@@ -300,8 +290,6 @@ It's worth noting that this may be a useful option to bypass ACL rules.
This must be a string i.e ``DNS``. Defaults to ``HTTP``. This must be a string i.e ``DNS``. Defaults to ``HTTP``.
*Please refer to the ``IPProtocol`` class for further reference.*
``Common Attributes`` ``Common Attributes``
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^

View File

@@ -7,8 +7,7 @@ Simulation Structure
==================== ====================
The simulation is made up of many smaller components which are related to each other in a tree-like structure. At the 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 the :py:meth:`primaite.simulator.sim_container.Simulation`, which keeps track of the physical network 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.
and a domain controller for managing software and users.
Each node of the simulation 'tree' has responsibility for creating, deleting, and updating its direct descendants. Also, 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 when a component's ``describe_state()`` method is called, it will include the state of its descendants. The
@@ -25,48 +24,55 @@ relationship between components.
Actions Actions
======= =======
Agents can interact with the simulation by using actions. Actions are standardised with the Agents can interact with the simulation by using actions. Actions adhere to the Common Action and Observation Space (CAOS) specification, and are converted into Requests for use by the simulation. Requests are standardised via the :py:class:`primaite.simulation.core.RequestType` class, which just holds a reference to two special functions.
:py:class:`primaite.simulation.core.RequestType` class, which just holds a reference to two special functions.
1. The request function itself, it must accept a `request` parameters which is a list of strings that describe what the 1. The function that actions the request, it must accept a `request` parameters which is a list of strings that describe what the action should do. It must also accept a `context` dict which can house additional information surrounding the action.
action should do. It must also accept a `context` dict which can house additional information surrounding the action. 2. A validator function. This function should return a boolean value that decides if the request is permitted or not. It uses the same paramters as the action function.
For example, the context will typically include information about which entity intiated the action.
2. A validator function. This function should return a boolean value that decides if the request is permitted or not.
It uses the same paramters as the action function.
Action Permissions Action Validation
------------------ ------------------
When an agent tries to perform an action on a simulation component, that action will only be executed if the request is When an agent tries to perform an action on a simulation component, that action will only be executed if the request is validated. For example, some actions can require that the target network node is powered on. Each action defines its
validated. For example, some actions can require that an agent is logged into an admin account. Each action defines its
own permissions using an instance of :py:class:`primaite.simulation.core.ActionPermissionValidator`. The below code own permissions using an instance of :py:class:`primaite.simulation.core.ActionPermissionValidator`. The below code
snippet demonstrates usage of the ``ActionPermissionValidator``. snippet demonstrates usage of the ``ActionPermissionValidator``.
.. code:: python .. code:: python
from primaite.simulator.core import Action, RequestManager, SimComponent from primaite.simulator.core import Action, RequestManager, SimComponent, ActionPermissionValidator
from primaite.simulator.domain.controller import AccountGroup, GroupMembershipValidator from primaite.interface.request import RequestResponse
class Smartphone(SimComponent): class Smartphone(SimComponent):
name: str name: str
apps = [] connected: bool
apps: List = []
class _ConnectedToNetworkValidator(ActionPermissionValidator):
smartphone: Smarphone
"""A reference to the smartphone object."""
def __call__(self, request: RequestFormat, context: Dict) -> bool:
return self.smartphone.connected
def _init_request_manager(self) -> RequestManager: def _init_request_manager(self) -> RequestManager:
am = super()._init_request_manager() am = super()._init_request_manager()
am.add_request( am.add_request(
"reset_factory_settings", "reset_factory_settings",
Action( ReqeustType(
func = lambda request, context: self.reset_factory_settings(), func = lambda request, context: RequestResponse.from_bool(self.reset_factory_settings()),
validator = GroupMembershipValidator([AccountGroup.DOMAIN_ADMIN]), validator = Smartphone._ConnectedToNetworkValidator(smartphone=self),
) )
) )
def reset_factory_settings(self): def reset_factory_settings(self):
self.apps = [] self.apps = []
return True
phone = Smartphone(name="phone1") phone = Smartphone(name="phone1", connected=False)
phone.apply_request(request=["reset_factory_settings"])
# >>> False
# try to wipe the phone as a domain user, this will have no effect phone2 = Smartphone(name="phone2", connected=True)
phone.apply_action(["reset_factory_settings"], context={"request_source":{"groups":["DOMAIN_USER"]}) phone.apply_request(request=["reset_factory_settings"])
# >>> True
# try to wipe the phone as an admin user, this will wipe the phone
phone.apply_action(["reset_factory_settings"], context={"request_source":{"groups":["DOMAIN_ADMIN"]})

View File

@@ -4,7 +4,7 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# UC7 Demonstration\n", "# UC7 Network Connectivity\n",
"\n", "\n",
"© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n" "© Crown-owned copyright 2025, Defence Science and Technology Laboratory UK\n"
] ]
@@ -521,7 +521,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Some tech intranet router DR 1 --> Public DNS \n", "# Some tech intranet router DR 1 --> Public DNS\n",
"\n", "\n",
"st_intra_prv_rt_dr_1.ping(isp_pub_srv_dns.network_interface[1].ip_address)" "st_intra_prv_rt_dr_1.ping(isp_pub_srv_dns.network_interface[1].ip_address)"
] ]
@@ -583,7 +583,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# ST Home office PC 3 --> ST Router DR 2 \n", "# ST Home office PC 3 --> ST Router DR 2\n",
"\n", "\n",
"st_head_office_private_pc_1.ping(st_intra_prv_rt_dr_2.network_interface[1].ip_address)" "st_head_office_private_pc_1.ping(st_intra_prv_rt_dr_2.network_interface[1].ip_address)"
] ]
@@ -623,7 +623,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# ST Human Resources PC 1 --> ST Human Resources PC 2 \n", "# ST Human Resources PC 1 --> ST Human Resources PC 2\n",
"\n", "\n",
"st_human_resources_private_pc_1.ping(st_human_resources_private_pc_2.network_interface[1].ip_address)" "st_human_resources_private_pc_1.ping(st_human_resources_private_pc_2.network_interface[1].ip_address)"
] ]
@@ -645,7 +645,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# ST Human Resources PC 1 --> ST Intranet Router DR 2 \n", "# ST Human Resources PC 1 --> ST Intranet Router DR 2\n",
"\n", "\n",
"st_human_resources_private_pc_1.ping(st_intra_prv_rt_dr_2.network_interface[1].ip_address)" "st_human_resources_private_pc_1.ping(st_intra_prv_rt_dr_2.network_interface[1].ip_address)"
] ]
@@ -656,7 +656,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# ST Human Resources PC 1 --> Public DNS \n", "# ST Human Resources PC 1 --> Public DNS\n",
"\n", "\n",
"st_human_resources_private_pc_1.ping(isp_pub_srv_dns.network_interface[1].ip_address)" "st_human_resources_private_pc_1.ping(isp_pub_srv_dns.network_interface[1].ip_address)"
] ]