Merge remote-tracking branch 'origin/dev' into feature/2533-improve-dev-tools-in-primaite-cli
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added the ability for a DatabaseService to terminate a connection.
|
||||
- Added active_connection to DatabaseClientConnection so that if the connection is terminated active_connection is set to False and the object can no longer be used.
|
||||
- Added additional show functions to enable connection inspection.
|
||||
- Updates to agent logging, to include the reward both per step and per episode.
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
@@ -52,7 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Order of service health state
|
||||
- Starting a node didn't start the services on it
|
||||
- Fixed an issue where the services were still able to run even though the node the service is installed on is turned off
|
||||
|
||||
- The use of NODE_FILE_CHECKHASH and NODE_FOLDER_CHECKHASH in the current release is marked as 'Not Implemented'.
|
||||
|
||||
|
||||
### Added
|
||||
|
||||
@@ -31,7 +31,13 @@ To see the configuration for these nodes, refer to the following:
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
simulation/nodes/*
|
||||
simulation/nodes/computer
|
||||
simulation/nodes/firewall
|
||||
simulation/nodes/router
|
||||
simulation/nodes/server
|
||||
simulation/nodes/switch
|
||||
simulation/nodes/wireless_router
|
||||
simulation/nodes/network_examples
|
||||
|
||||
``links``
|
||||
---------
|
||||
@@ -54,15 +60,13 @@ this results in:
|
||||
.. code-block:: yaml
|
||||
|
||||
links:
|
||||
- ref: computer_1___switch
|
||||
endpoint_a_ref: computer_1
|
||||
- endpoint_a_hostname: computer_1
|
||||
endpoint_a_port: 1 # port 1 on computer_1
|
||||
endpoint_b_ref: switch
|
||||
endpoint_b_hostname: switch
|
||||
endpoint_b_port: 1 # port 1 on switch
|
||||
- ref: computer_2___switch
|
||||
endpoint_a_ref: computer_2
|
||||
- endpoint_a_hostname: computer_2
|
||||
endpoint_a_port: 1 # port 1 on computer_2
|
||||
endpoint_b_ref: switch
|
||||
endpoint_b_hostname: switch
|
||||
endpoint_b_port: 2 # port 2 on switch
|
||||
|
||||
``ref``
|
||||
@@ -70,7 +74,7 @@ this results in:
|
||||
|
||||
The human readable name for the link. Not used in code, however is useful for a human to understand what the link is for.
|
||||
|
||||
``endpoint_a_ref``
|
||||
``endpoint_a_hostname``
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``hostname`` of the node which must be connected.
|
||||
@@ -78,10 +82,10 @@ The ``hostname`` of the node which must be connected.
|
||||
``endpoint_a_port``
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The port on ``endpoint_a_ref`` which is to be connected to ``endpoint_b_port``.
|
||||
The port on ``endpoint_a_hostname`` which is to be connected to ``endpoint_b_port``.
|
||||
This accepts an integer value e.g. if port 1 is to be connected, the configuration should be ``endpoint_a_port: 1``
|
||||
|
||||
``endpoint_b_ref``
|
||||
``endpoint_b_hostname``
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``hostname`` of the node which must be connected.
|
||||
@@ -89,5 +93,5 @@ The ``hostname`` of the node which must be connected.
|
||||
``endpoint_b_port``
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The port on ``endpoint_b_ref`` which is to be connected to ``endpoint_a_port``.
|
||||
The port on ``endpoint_b_hostname`` which is to be connected to ``endpoint_a_port``.
|
||||
This accepts an integer value e.g. if port 1 is to be connected, the configuration should be ``endpoint_b_port: 1``
|
||||
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 65 KiB |
1348
docs/source/configuration/simulation/nodes/network_examples.rst
Normal file
@@ -41,6 +41,20 @@ Just like other aspects of SimComponent, the request types are not managed centr
|
||||
- ``context`` detail
|
||||
The context is not used by any of the currently implemented components or requests.
|
||||
|
||||
- Request response
|
||||
When the simulator receives a request, it returns a response with a success status. The possible statuses are:
|
||||
|
||||
* **success**: The request was received and successfully executed.
|
||||
* For example, the agent tries to add an ACL rule and specifies correct parameters, and the ACL rule is added successfully.
|
||||
|
||||
* **failure**: The request was received, but it could not be executed, or it failed while executing.
|
||||
* For example, the agent tries to execute the ``WebBrowser`` application, but the webpage wasn't retrieved because the DNS server is not setup on the node.
|
||||
|
||||
* **unreachable**: The request was sent to a simulation component that does not exist.
|
||||
* For example, the agent tries to scan a file that has not been created yet.
|
||||
|
||||
For more information, please refer to the ``Requests-and-Responses.ipynb`` jupyter notebook
|
||||
|
||||
Technical Detail
|
||||
================
|
||||
|
||||
@@ -64,9 +78,9 @@ A simple example without chaining can be seen in the :py:class:`primaite.simulat
|
||||
...
|
||||
def _init_request_manager(self):
|
||||
...
|
||||
request_manager.add_request("scan", RequestType(func=lambda request, context: self.scan()))
|
||||
request_manager.add_request("repair", RequestType(func=lambda request, context: self.repair()))
|
||||
request_manager.add_request("restore", RequestType(func=lambda request, context: self.restore()))
|
||||
request_manager.add_request("scan", RequestType(func=lambda request, context: RequestResponse.from_bool(self.scan())))
|
||||
request_manager.add_request("repair", RequestType(func=lambda request, context: RequestResponse.from_bool(self.repair())))
|
||||
request_manager.add_request("restore", RequestType(func=lambda request, context: RequestResponse.from_bool(self.restore())))
|
||||
|
||||
*ellipses (``...``) used to omit code impertinent to this explanation*
|
||||
|
||||
@@ -115,8 +129,8 @@ Requests that are specified without a validator automatically get assigned an ``
|
||||
Request Response
|
||||
----------------
|
||||
|
||||
The :py:class:`primaite.interface.request.RequestResponse<RequestResponse>` is a data transfer object that carries response data between the simulator and the game layer. The ``status`` field reports on the success or failure, and the ``data`` field is for any additional data. The most common way that this class is initiated is by its ``from_bool`` method. This way, given a True or False, a successful or failed request response is generated, respectively (with an empty data field).
|
||||
The :py:class:`primaite.interface.request.RequestResponse<RequestResponse>` carries response data between the simulator and the game layer. The ``status`` field reports on the success or failure, and the ``data`` field is for any additional data. The most common way that this class is used is by the ``from_bool`` method. This way, given a True or False, a successful or failed request response is generated, respectively (with an empty data field).
|
||||
|
||||
For instance, the ``execute`` action on a :py:class:`primaite.simulator.system.applications.web_browser.WebBrowser<WebBrowser>` calls the ``get_webpage()`` method of the ``WebBrowser``. ``get_webpage()`` returns a True if the webpage was successfully retrieved, and False if unsuccessful for any reason, such as being blocked by an ACL, or if the database server is unresponsive. The boolean returned from ``get_webpage()`` is used to create the request response.
|
||||
For instance, the ``execute`` action on a :py:class:`primaite.simulator.system.applications.web_browser.WebBrowser<WebBrowser>` calls the ``get_webpage()`` method. ``get_webpage()`` returns a True if the webpage was successfully retrieved, and False if unsuccessful for any reason, such as being blocked by an ACL, or if the database server is unresponsive. The boolean returned from ``get_webpage()`` is used to create the request response with ``from_bool()``.
|
||||
|
||||
Just as the requests themselves were passed from owner to component, the request response is bubbled back up from component to owner until it arrives at the game layer.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
game:
|
||||
ports:
|
||||
- ARP
|
||||
protocols:
|
||||
- ICMP
|
||||
- TCP
|
||||
- UDP
|
||||
|
||||
simulation:
|
||||
network:
|
||||
nodes:
|
||||
- hostname: pc_1
|
||||
type: computer
|
||||
ip_address: 192.168.1.11
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
|
||||
- hostname: pc_2
|
||||
type: computer
|
||||
ip_address: 192.168.1.12
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
|
||||
- hostname: server_1
|
||||
type: server
|
||||
ip_address: 192.168.1.13
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
|
||||
- hostname: switch_1
|
||||
type: switch
|
||||
num_ports: 4
|
||||
|
||||
- hostname: router_1
|
||||
type: router
|
||||
num_ports: 1
|
||||
ports:
|
||||
1:
|
||||
ip_address: 192.168.1.1
|
||||
subnet_mask: 255.255.255.0
|
||||
acl:
|
||||
10:
|
||||
action: PERMIT
|
||||
src_ip: 192.168.1.0
|
||||
src_wildcard_mask: 0.0.0.255
|
||||
dst_ip: 192.168.1.1
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
|
||||
links:
|
||||
- endpoint_a_hostname: pc_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: pc_2
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 2
|
||||
- endpoint_a_hostname: server_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 3
|
||||
- endpoint_a_hostname: router_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 4
|
||||
@@ -0,0 +1,26 @@
|
||||
game:
|
||||
ports:
|
||||
- ARP
|
||||
protocols:
|
||||
- ICMP
|
||||
- TCP
|
||||
- UDP
|
||||
|
||||
simulation:
|
||||
network:
|
||||
nodes:
|
||||
- hostname: pc_1
|
||||
type: computer
|
||||
ip_address: 192.168.1.11
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
- hostname: server_1
|
||||
type: server
|
||||
ip_address: 192.168.1.13
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
links:
|
||||
- endpoint_a_hostname: pc_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: server_1
|
||||
endpoint_b_port: 1
|
||||
@@ -313,7 +313,7 @@ agents:
|
||||
folder_id: 0
|
||||
file_id: 0
|
||||
10:
|
||||
action: "NODE_FILE_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context.
|
||||
action: "NODE_FILE_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context.
|
||||
options:
|
||||
node_id: 2
|
||||
folder_id: 0
|
||||
@@ -341,7 +341,7 @@ agents:
|
||||
node_id: 2
|
||||
folder_id: 0
|
||||
15:
|
||||
action: "NODE_FOLDER_SCAN" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context.
|
||||
action: "NODE_FOLDER_CHECKHASH" # CHECKHASH replaced by SCAN - but the behaviour is the same in this context.
|
||||
options:
|
||||
node_id: 2
|
||||
folder_id: 0
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
game:
|
||||
ports:
|
||||
- ARP
|
||||
- DNS
|
||||
- HTTP
|
||||
- POSTGRES_SERVER
|
||||
protocols:
|
||||
- ICMP
|
||||
- TCP
|
||||
- UDP
|
||||
|
||||
simulation:
|
||||
network:
|
||||
nodes:
|
||||
# Home/Office Network
|
||||
- hostname: pc_1
|
||||
type: computer
|
||||
ip_address: 192.168.1.11
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 8.8.8.2
|
||||
applications:
|
||||
- type: DatabaseClient
|
||||
options:
|
||||
db_server_ip: 10.10.1.11
|
||||
- type: WebBrowser
|
||||
options:
|
||||
target_url: http://sometech.ai
|
||||
|
||||
- hostname: pc_2
|
||||
type: computer
|
||||
ip_address: 192.168.1.12
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 8.8.8.2
|
||||
applications:
|
||||
- type: DatabaseClient
|
||||
options:
|
||||
db_server_ip: 10.10.1.11
|
||||
- type: WebBrowser
|
||||
options:
|
||||
target_url: http://sometech.ai
|
||||
|
||||
- hostname: server_1
|
||||
type: server
|
||||
ip_address: 192.168.1.13
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 192.168.1.1
|
||||
dns_server: 8.8.8.2
|
||||
|
||||
- hostname: switch_1
|
||||
type: switch
|
||||
num_ports: 4
|
||||
|
||||
- hostname: router_1
|
||||
type: router
|
||||
num_ports: 2
|
||||
ports:
|
||||
1:
|
||||
ip_address: 192.168.1.1
|
||||
subnet_mask: 255.255.255.0
|
||||
2:
|
||||
ip_address: 43.35.240.2
|
||||
subnet_mask: 255.255.255.252
|
||||
acl:
|
||||
10:
|
||||
action: PERMIT
|
||||
default_route: # Default route to all external networks
|
||||
next_hop_ip_address: 43.35.240.1 # NI 1 on icp_router
|
||||
|
||||
# ISP Network
|
||||
- hostname: isp_rt
|
||||
type: router
|
||||
num_ports: 3
|
||||
ports:
|
||||
1:
|
||||
ip_address: 43.35.240.1
|
||||
subnet_mask: 255.255.255.252
|
||||
2:
|
||||
ip_address: 94.10.180.1
|
||||
subnet_mask: 255.255.255.252
|
||||
3:
|
||||
ip_address: 8.8.8.1
|
||||
subnet_mask: 255.255.255.252
|
||||
acl:
|
||||
10:
|
||||
action: PERMIT
|
||||
routes:
|
||||
- address: 192.168.1.0 # Route to the Home/Office LAN
|
||||
subnet_mask: 255.255.255.0
|
||||
next_hop_ip_address: 43.35.240.2 # NI 2 on router_1
|
||||
- address: 10.10.0.0 # Route to the SomeTech internal network
|
||||
subnet_mask: 255.255.0.0
|
||||
next_hop_ip_address: 94.10.180.2 # NI ext on some_tech_fw
|
||||
- address: 94.10.180.6 # Route to the Web Server in the SomeTech DMZ
|
||||
subnet_mask: 255.255.255.255
|
||||
next_hop_ip_address: 94.10.180.2 # NI ext on some_tech_fw
|
||||
|
||||
- hostname: isp_dns_srv
|
||||
type: server
|
||||
ip_address: 8.8.8.2
|
||||
subnet_mask: 255.255.255.252
|
||||
default_gateway: 8.8.8.1
|
||||
services:
|
||||
- ref: dns_server
|
||||
type: DNSServer
|
||||
options:
|
||||
domain_mapping:
|
||||
sometech.ai: 94.10.180.6
|
||||
|
||||
# SomeTech Network
|
||||
- hostname: some_tech_fw
|
||||
type: firewall
|
||||
ports:
|
||||
external_port: # port 1
|
||||
ip_address: 94.10.180.2
|
||||
subnet_mask: 255.255.255.252
|
||||
internal_port: # port 2
|
||||
ip_address: 10.10.4.2
|
||||
subnet_mask: 255.255.255.252
|
||||
dmz_port: # port 3
|
||||
ip_address: 94.10.180.5
|
||||
subnet_mask: 255.255.255.252
|
||||
acl:
|
||||
internal_inbound_acl:
|
||||
8: # Permit some_tech_web_srv to connect to Database service on some_tech_db_srv
|
||||
action: PERMIT
|
||||
src_ip: 94.10.180.6
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
src_port: POSTGRES_SERVER
|
||||
dst_ip: 10.10.1.11
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
dst_port: POSTGRES_SERVER
|
||||
9: # Permit SomeTech to use HTTP
|
||||
action: PERMIT
|
||||
src_port: HTTP
|
||||
10: # Permit SomeTech to use DNS
|
||||
action: PERMIT
|
||||
src_port: DNS
|
||||
dst_port: DNS
|
||||
internal_outbound_acl:
|
||||
10: # Permit all internal outbound traffic
|
||||
action: PERMIT
|
||||
dmz_inbound_acl:
|
||||
7: # Permit Database service on some_tech_db_srv to respond to some_tech_web_srv
|
||||
action: PERMIT
|
||||
src_ip: 10.10.1.11
|
||||
src_port: POSTGRES_SERVER
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
dst_ip: 94.10.180.6
|
||||
dst_port: POSTGRES_SERVER
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
8: # Permit SomeTech DMZ to use ARP
|
||||
action: PERMIT
|
||||
src_port: ARP
|
||||
dst_port: ARP
|
||||
9: # Permit SomeTech DMZ to use DNS
|
||||
action: PERMIT
|
||||
src_port: DNS
|
||||
dst_port: DNS
|
||||
10: # Permit all inbound HTTP requests
|
||||
action: PERMIT
|
||||
dst_port: HTTP
|
||||
dmz_outbound_acl:
|
||||
7: # Permit some_tech_web_srv to connect to Database service on some_tech_db_srv
|
||||
action: PERMIT
|
||||
src_ip: 94.10.180.6
|
||||
src_port: POSTGRES_SERVER
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
dst_ip: 10.10.1.11
|
||||
dst_port: POSTGRES_SERVER
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
8: # Permit SomeTech DMZ to use ARP
|
||||
action: PERMIT
|
||||
src_port: ARP
|
||||
dst_port: ARP
|
||||
9: # Permit SomeTech DMZ to use DNS
|
||||
action: PERMIT
|
||||
src_port: DNS
|
||||
dst_port: DNS
|
||||
10: # Permit all outbound HTTP requests
|
||||
action: PERMIT
|
||||
src_port: HTTP
|
||||
default_route: # Default route to all external networks
|
||||
next_hop_ip_address: 94.10.180.1 # NI 2 on isp_rt
|
||||
routes:
|
||||
- address: 10.10.0.0 # Route to the SomeTech internal LAN
|
||||
subnet_mask: 255.255.0.0
|
||||
next_hop_ip_address: 10.10.4.1 # NI 1 on some_tech_rt
|
||||
|
||||
|
||||
- hostname: some_tech_web_srv
|
||||
type: server
|
||||
ip_address: 94.10.180.6
|
||||
subnet_mask: 255.255.255.252
|
||||
default_gateway: 94.10.180.5
|
||||
dns_server: 8.8.8.2
|
||||
services:
|
||||
- ref: web_server
|
||||
type: WebServer
|
||||
applications:
|
||||
- type: DatabaseClient
|
||||
options:
|
||||
db_server_ip: 10.10.1.11
|
||||
|
||||
- hostname: some_tech_rt
|
||||
type: router
|
||||
num_ports: 4
|
||||
ports:
|
||||
1:
|
||||
ip_address: 10.10.1.1
|
||||
subnet_mask: 255.255.255.0
|
||||
2:
|
||||
ip_address: 10.10.4.1
|
||||
subnet_mask: 255.255.255.252
|
||||
3:
|
||||
ip_address: 10.10.3.1
|
||||
subnet_mask: 255.255.255.0
|
||||
4:
|
||||
ip_address: 10.10.2.1
|
||||
subnet_mask: 255.255.255.0
|
||||
|
||||
acl:
|
||||
2: # Allow the some_tech_web_srv to connect to the Database Service on some_tech_db_srv
|
||||
action: PERMIT
|
||||
src_ip: 94.10.180.6
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
src_port: POSTGRES_SERVER
|
||||
dst_ip: 10.10.1.11
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
dst_port: POSTGRES_SERVER
|
||||
3: # Allow the Database Service on some_tech_db_srv to respond to some_tech_web_srv
|
||||
action: PERMIT
|
||||
src_ip: 10.10.1.11
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
src_port: POSTGRES_SERVER
|
||||
dst_ip: 94.10.180.6
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
dst_port: POSTGRES_SERVER
|
||||
4: # Prevent the Junior engineer from downloading files from the some_tech_storage_srv over FTP
|
||||
action: DENY
|
||||
src_ip: 10.10.2.12
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
src_port: FTP
|
||||
dst_ip: 10.10.1.12
|
||||
dst_wildcard_mask: 0.0.0.0
|
||||
dst_port: FTP
|
||||
5: # Allow communication between Engineering and the DB & Storage subnet
|
||||
action: PERMIT
|
||||
src_ip: 10.10.2.0
|
||||
src_wildcard_mask: 0.0.0.255
|
||||
dst_ip: 10.10.1.0
|
||||
dst_wildcard_mask: 0.0.0.255
|
||||
6: # Allow communication between the DB & Storage subnet and Engineering
|
||||
action: PERMIT
|
||||
src_ip: 10.10.1.0
|
||||
src_wildcard_mask: 0.0.0.255
|
||||
dst_ip: 10.10.2.0
|
||||
dst_wildcard_mask: 0.0.0.255
|
||||
7: # Allow the SomeTech network to use HTTP
|
||||
action: PERMIT
|
||||
src_port: HTTP
|
||||
dst_port: HTTP
|
||||
8: # Allow the SomeTech internal network to use ARP
|
||||
action: PERMIT
|
||||
src_ip: 10.10.0.0
|
||||
src_wildcard_mask: 0.0.255.255
|
||||
src_port: ARP
|
||||
9: # Allow the SomeTech internal network to use ICMP
|
||||
action: PERMIT
|
||||
src_ip: 10.10.0.0
|
||||
src_wildcard_mask: 0.0.255.255
|
||||
protocol: ICMP
|
||||
10:
|
||||
action: PERMIT
|
||||
src_ip: 94.10.180.6
|
||||
src_wildcard_mask: 0.0.0.0
|
||||
src_port: HTTP
|
||||
dst_ip: 10.10.0.0
|
||||
dst_wildcard_mask: 0.0.255.255
|
||||
dst_port: HTTP
|
||||
11: # Permit SomeTech to use DNS
|
||||
action: PERMIT
|
||||
src_port: DNS
|
||||
dst_port: DNS
|
||||
default_route: # Default route to all external networks
|
||||
next_hop_ip_address: 10.10.4.2 # NI int on some_tech_fw
|
||||
|
||||
|
||||
- hostname: some_tech_data_sw
|
||||
type: switch
|
||||
num_ports: 3
|
||||
|
||||
- hostname: some_tech_hr_sw
|
||||
type: switch
|
||||
num_ports: 2
|
||||
|
||||
- hostname: some_tech_eng_sw
|
||||
type: switch
|
||||
num_ports: 3
|
||||
|
||||
- hostname: some_tech_db_srv
|
||||
type: server
|
||||
ip_address: 10.10.1.11
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 10.10.1.1
|
||||
dns_server: 8.8.8.2
|
||||
services:
|
||||
- type: DatabaseService
|
||||
options:
|
||||
backup_server_ip: 10.10.1.12 # The some_tech_storage_srv server
|
||||
- type: FTPClient
|
||||
|
||||
- hostname: some_tech_storage_srv
|
||||
type: server
|
||||
ip_address: 10.10.1.12
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 10.10.1.1
|
||||
dns_server: 8.8.8.2
|
||||
services:
|
||||
- type: FTPServer
|
||||
|
||||
- hostname: some_tech_hr_1
|
||||
type: computer
|
||||
ip_address: 10.10.3.11
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 10.10.3.1
|
||||
dns_server: 8.8.8.2
|
||||
applications:
|
||||
- type: DatabaseClient
|
||||
options:
|
||||
db_server_ip: 10.10.1.11
|
||||
- type: WebBrowser
|
||||
options:
|
||||
target_url: http://sometech.ai
|
||||
|
||||
- hostname: some_tech_snr_dev_pc
|
||||
type: computer
|
||||
ip_address: 10.10.2.11
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 10.10.2.1
|
||||
dns_server: 8.8.8.2
|
||||
applications:
|
||||
- type: DatabaseClient
|
||||
options:
|
||||
db_server_ip: 10.10.1.11
|
||||
- type: WebBrowser
|
||||
options:
|
||||
target_url: http://sometech.ai
|
||||
|
||||
- hostname: some_tech_jnr_dev_pc
|
||||
type: computer
|
||||
ip_address: 10.10.2.12
|
||||
subnet_mask: 255.255.255.0
|
||||
default_gateway: 10.10.2.1
|
||||
dns_server: 8.8.8.2
|
||||
applications:
|
||||
- type: DatabaseClient
|
||||
options:
|
||||
db_server_ip: 10.10.1.11
|
||||
- type: WebBrowser
|
||||
options:
|
||||
target_url: http://sometech.ai
|
||||
|
||||
links:
|
||||
# Home/Office Lan Links
|
||||
- endpoint_a_hostname: pc_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: pc_2
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 2
|
||||
- endpoint_a_hostname: server_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 3
|
||||
- endpoint_a_hostname: router_1
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: switch_1
|
||||
endpoint_b_port: 4
|
||||
|
||||
# ISP Links
|
||||
- endpoint_a_hostname: isp_rt
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: router_1
|
||||
endpoint_b_port: 2
|
||||
- endpoint_a_hostname: isp_rt
|
||||
endpoint_a_port: 2
|
||||
endpoint_b_hostname: some_tech_fw
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: isp_rt
|
||||
endpoint_a_port: 3
|
||||
endpoint_b_hostname: isp_dns_srv
|
||||
endpoint_b_port: 1
|
||||
|
||||
|
||||
# SomeTech LAN Links
|
||||
- endpoint_a_hostname: some_tech_fw
|
||||
endpoint_a_port: 3
|
||||
endpoint_b_hostname: some_tech_web_srv
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: some_tech_fw
|
||||
endpoint_a_port: 2
|
||||
endpoint_b_hostname: some_tech_rt
|
||||
endpoint_b_port: 2
|
||||
- endpoint_a_hostname: some_tech_rt
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: some_tech_data_sw
|
||||
endpoint_b_port: 3
|
||||
- endpoint_a_hostname: some_tech_rt
|
||||
endpoint_a_port: 3
|
||||
endpoint_b_hostname: some_tech_hr_sw
|
||||
endpoint_b_port: 2
|
||||
- endpoint_a_hostname: some_tech_rt
|
||||
endpoint_a_port: 4
|
||||
endpoint_b_hostname: some_tech_eng_sw
|
||||
endpoint_b_port: 3
|
||||
- endpoint_a_hostname: some_tech_data_sw
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: some_tech_db_srv
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: some_tech_data_sw
|
||||
endpoint_a_port: 2
|
||||
endpoint_b_hostname: some_tech_storage_srv
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: some_tech_hr_sw
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: some_tech_hr_1
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: some_tech_eng_sw
|
||||
endpoint_a_port: 1
|
||||
endpoint_b_hostname: some_tech_snr_dev_pc
|
||||
endpoint_b_port: 1
|
||||
- endpoint_a_hostname: some_tech_eng_sw
|
||||
endpoint_a_port: 2
|
||||
endpoint_b_hostname: some_tech_jnr_dev_pc
|
||||
endpoint_b_port: 1
|
||||
@@ -244,7 +244,7 @@ class PrimaiteGame:
|
||||
hostname=node_cfg["hostname"],
|
||||
ip_address=node_cfg["ip_address"],
|
||||
subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")),
|
||||
default_gateway=node_cfg["default_gateway"],
|
||||
default_gateway=node_cfg.get("default_gateway"),
|
||||
dns_server=node_cfg.get("dns_server", None),
|
||||
operating_state=NodeOperatingState.ON
|
||||
if not (p := node_cfg.get("operating_state"))
|
||||
@@ -255,7 +255,7 @@ class PrimaiteGame:
|
||||
hostname=node_cfg["hostname"],
|
||||
ip_address=node_cfg["ip_address"],
|
||||
subnet_mask=IPv4Address(node_cfg.get("subnet_mask", "255.255.255.0")),
|
||||
default_gateway=node_cfg["default_gateway"],
|
||||
default_gateway=node_cfg.get("default_gateway"),
|
||||
dns_server=node_cfg.get("dns_server", None),
|
||||
operating_state=NodeOperatingState.ON
|
||||
if not (p := node_cfg.get("operating_state"))
|
||||
|
||||
@@ -404,7 +404,7 @@
|
||||
" # don't flatten observations so that we can see what is going on\n",
|
||||
" cfg['agents'][3]['agent_settings']['flatten_obs'] = False\n",
|
||||
"\n",
|
||||
"env = PrimaiteGymEnv(game_config = cfg)\n",
|
||||
"env = PrimaiteGymEnv(env_config = cfg)\n",
|
||||
"obs, info = env.reset()\n",
|
||||
"print('env created successfully')\n",
|
||||
"pprint(obs)"
|
||||
@@ -477,7 +477,8 @@
|
||||
"source": [
|
||||
"obs, reward, terminated, truncated, info = env.step(9) # scan database file\n",
|
||||
"obs, reward, terminated, truncated, info = env.step(1) # scan webapp service\n",
|
||||
"pprint(obs['NODES'])"
|
||||
"\n",
|
||||
"pprint(obs['NODES'])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -492,7 +493,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Also, the NMNE outbound of either client 1 (node 6) or client 2 (node 7) increased from 0 to 1, but only right after the red attack, so we probably cannot see it now."
|
||||
"Also, the NMNE outbound of either client 1 (node 6) or client 2 (node 7) has increased from 0 to 1. This tells us which client is being used by the red agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -647,7 +648,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"\n",
|
||||
"for step in range(40):\n",
|
||||
" obs, reward, terminated, truncated, info = env.step(0) # do nothing\n",
|
||||
" print(f\"step: {env.game.step_counter}, Red action: {info['agent_actions']['data_manipulation_attacker'].action}, Blue reward:{reward:.2f}\" )"
|
||||
@@ -668,13 +668,6 @@
|
||||
"source": [
|
||||
"env.reset()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -693,7 +686,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
"version": "3.10.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
221
src/primaite/notebooks/Requests-and-Responses.ipynb
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Requests and Responses\n",
|
||||
"\n",
|
||||
"Agents interact with the PrimAITE simulation via the Request system.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Sending a request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's set up a minimal network simulation and send some requests to see how it works."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState\n",
|
||||
"from primaite.simulator.network.hardware.nodes.host.host_node import HostNode\n",
|
||||
"from primaite.simulator.sim_container import Simulation\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"sim = Simulation()\n",
|
||||
"sim.network.add_node(\n",
|
||||
" HostNode(\n",
|
||||
" hostname=\"client\",\n",
|
||||
" ip_address='10.0.0.1',\n",
|
||||
" subnet_mask='255.255.255.0',\n",
|
||||
" operating_state=NodeOperatingState.ON)\n",
|
||||
")\n",
|
||||
"client = sim.network.get_node_by_hostname('client')\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A request is structured in a similar way to a command line interface - a list of strings with positional args. It's also possible to supply an optional `context` dictionary. We will craft a request that stops the pre-installed DNSClient service on the client node.\n",
|
||||
"\n",
|
||||
"First let's verify that the DNS Client is running on the client.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"client.software_manager.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Send a request to the simulator to stop the DNSClient."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request=[\"network\", \"node\", \"client\", \"service\", \"DNSClient\", \"stop\"],\n",
|
||||
" context={}\n",
|
||||
" )\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"\n",
|
||||
"The request returns a `RequestResponse` object which tells us that the request was successfully executed. Let's verify that the DNS client is in a stopped state now."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(f\"DNS Client state: {client.software_manager.software.get('DNSClient').operating_state.name}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Unreachable requests"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we attempt to send a request to something that doesn't exist, we will get an unreachable request status."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request=[\"network\", \"node\", \"client\", \"service\", \"NonExistentApplication\", \"stop\"],\n",
|
||||
" context={}\n",
|
||||
" )\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Failed requests\n",
|
||||
"\n",
|
||||
"Sometimes requests cannot be executed by the simulation. For example if we turn off the client node, we cannot execute the software that is running on it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request = [\"network\", \"node\", \"client\", \"shutdown\"],\n",
|
||||
" context = {}\n",
|
||||
")\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We need to apply timestep a few times for the client to go from `SHUTTING_DOWN` to `OFF` state."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(f\"client is in state: {client.operating_state.name}\")\n",
|
||||
"sim.apply_timestep(1)\n",
|
||||
"sim.apply_timestep(2)\n",
|
||||
"sim.apply_timestep(3)\n",
|
||||
"sim.apply_timestep(4)\n",
|
||||
"print(f\"client is in state: {client.operating_state.name}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now, if we try to start the DNSClient back up, we get a failure because we cannot start software on a node that is turned off."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"response = sim.apply_request(\n",
|
||||
" request=[\"network\", \"node\", \"client\", \"service\", \"DNSClient\", \"start\"],\n",
|
||||
" context={}\n",
|
||||
" )\n",
|
||||
"print(response)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"gym = PrimaiteGymEnv(game_config=cfg)"
|
||||
"gym = PrimaiteGymEnv(env_config=cfg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,7 +35,6 @@ class PrimaiteGymEnv(gymnasium.Env):
|
||||
"""Current game."""
|
||||
self._agent_name = next(iter(self.game.rl_agents))
|
||||
"""Name of the RL agent. Since there should only be one RL agent we can just pull the first and only key."""
|
||||
|
||||
self.episode_counter: int = 0
|
||||
"""Current episode number."""
|
||||
|
||||
@@ -49,8 +48,8 @@ class PrimaiteGymEnv(gymnasium.Env):
|
||||
# make ProxyAgent store the action chosen by the RL policy
|
||||
step = self.game.step_counter
|
||||
self.agent.store_action(action)
|
||||
# apply_agent_actions accesses the action we just stored
|
||||
self.game.pre_timestep()
|
||||
# apply_agent_actions accesses the action we just stored
|
||||
self.game.apply_agent_actions()
|
||||
self.game.advance_timestep()
|
||||
state = self.game.get_sim_state()
|
||||
@@ -58,6 +57,7 @@ class PrimaiteGymEnv(gymnasium.Env):
|
||||
|
||||
next_obs = self._get_obs() # this doesn't update observation, just gets the current observation
|
||||
reward = self.agent.reward_function.current_reward
|
||||
_LOGGER.info(f"step: {self.game.step_counter}, Blue reward: {reward}")
|
||||
terminated = False
|
||||
truncated = self.game.calculate_truncated()
|
||||
info = {
|
||||
@@ -204,9 +204,13 @@ class PrimaiteRayMARLEnv(MultiAgentEnv):
|
||||
|
||||
def reset(self, *, seed: int = None, options: dict = None) -> Tuple[ObsType, Dict]:
|
||||
"""Reset the environment."""
|
||||
rewards = {name: agent.reward_function.total_reward for name, agent in self.agents.items()}
|
||||
_LOGGER.info(f"Resetting environment, episode {self.episode_counter}, " f"avg. reward: {rewards}")
|
||||
|
||||
if self.io.settings.save_agent_actions:
|
||||
all_agent_actions = {name: agent.action_history for name, agent in self.game.agents.items()}
|
||||
self.io.write_agent_actions(agent_actions=all_agent_actions, episode=self.episode_counter)
|
||||
|
||||
self.episode_counter += 1
|
||||
self.game: PrimaiteGame = PrimaiteGame.from_config(self.episode_scheduler(self.episode_counter))
|
||||
self.game.setup_for_episode(episode=self.episode_counter)
|
||||
@@ -244,6 +248,7 @@ class PrimaiteRayMARLEnv(MultiAgentEnv):
|
||||
|
||||
# 4. Get rewards
|
||||
rewards = {name: agent.reward_function.current_reward for name, agent in self.agents.items()}
|
||||
_LOGGER.info(f"step: {self.game.step_counter}, Rewards: {rewards}")
|
||||
terminateds = {name: False for name, _ in self.agents.items()}
|
||||
truncateds = {name: self.game.calculate_truncated() for name, _ in self.agents.items()}
|
||||
infos = {name: {} for name, _ in self.agents.items()}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# flake8: noqa
|
||||
"""Core of the PrimAITE Simulator."""
|
||||
import warnings
|
||||
from abc import abstractmethod
|
||||
from typing import Callable, Dict, List, Literal, Optional, Union
|
||||
from uuid import uuid4
|
||||
@@ -26,6 +27,12 @@ class RequestPermissionValidator(BaseModel):
|
||||
"""Use the request and context parameters to decide whether the request should be permitted."""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return "request rejected"
|
||||
|
||||
|
||||
class AllowAllValidator(RequestPermissionValidator):
|
||||
"""Always allows the request."""
|
||||
@@ -34,6 +41,16 @@ class AllowAllValidator(RequestPermissionValidator):
|
||||
"""Always allow the request."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""
|
||||
Message that is reported when a request is rejected by this validator.
|
||||
|
||||
This method should really never be called because this validator never rejects requests.
|
||||
"""
|
||||
warnings.warn("Something went wrong - AllowAllValidator rejected a request.")
|
||||
return super().fail_message
|
||||
|
||||
|
||||
class RequestType(BaseModel):
|
||||
"""
|
||||
@@ -99,7 +116,7 @@ class RequestManager(BaseModel):
|
||||
|
||||
if not request_type.validator(request_options, context):
|
||||
_LOGGER.debug(f"Request {request} was denied due to insufficient permissions")
|
||||
return RequestResponse(status="failure", data={"reason": "request validation failed"})
|
||||
return RequestResponse(status="failure", data={"reason": request_type.validator.fail_message})
|
||||
|
||||
return request_type.func(request_options, context)
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@ class GroupMembershipValidator(RequestPermissionValidator):
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return "User does not belong to group"
|
||||
|
||||
|
||||
class DomainController(SimComponent):
|
||||
"""Main object for controlling the domain."""
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import os.path
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
@@ -145,6 +146,10 @@ class File(FileSystemItemABC):
|
||||
|
||||
Return False if corruption is detected, otherwise True
|
||||
"""
|
||||
warnings.warn("NODE_FILE_CHECKHASH is currently not implemented.")
|
||||
self.sys_log.warning("NODE_FILE_CHECKHASH is currently not implemented.")
|
||||
return False
|
||||
|
||||
if self.deleted:
|
||||
self.sys_log.error(f"Unable to check hash of deleted file {self.folder_name}/{self.name}")
|
||||
return False
|
||||
|
||||
@@ -156,7 +156,7 @@ class FileSystemItemABC(SimComponent):
|
||||
@abstractmethod
|
||||
def check_hash(self) -> bool:
|
||||
"""
|
||||
Checks the has of the file to detect any changes.
|
||||
Checks the hash of the file to detect any changes.
|
||||
|
||||
For current implementation, any change in file hash means it is compromised.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Dict, Optional
|
||||
|
||||
from prettytable import MARKDOWN, PrettyTable
|
||||
@@ -380,6 +381,10 @@ class Folder(FileSystemItemABC):
|
||||
|
||||
Return False if corruption is detected, otherwise True
|
||||
"""
|
||||
warnings.warn("NODE_FOLDER_CHECKHASH is currently not implemented.")
|
||||
self.sys_log.error("NODE_FOLDER_CHECKHASH is currently not implemented.")
|
||||
return False
|
||||
|
||||
if self.deleted:
|
||||
self.sys_log.error(f"Unable to check hash of deleted folder {self.name}")
|
||||
return False
|
||||
|
||||
@@ -798,6 +798,11 @@ class Node(SimComponent):
|
||||
"""Return whether the node is on or off."""
|
||||
return self.node.operating_state == NodeOperatingState.ON
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return f"Cannot perform request on node '{self.node.hostname}' because it is not turned on."
|
||||
|
||||
def _init_request_manager(self) -> RequestManager:
|
||||
"""
|
||||
Initialise the request manager.
|
||||
|
||||
@@ -330,7 +330,7 @@ class Firewall(Router):
|
||||
# check if External Inbound ACL Rules permit frame
|
||||
permitted, rule = self.external_inbound_acl.is_permitted(frame)
|
||||
if not permitted:
|
||||
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
|
||||
self.sys_log.info(f"Frame blocked at external inbound by rule {rule}")
|
||||
return
|
||||
self.software_manager.arp.add_arp_cache_entry(
|
||||
ip_address=frame.ip.src_ip_address,
|
||||
@@ -360,7 +360,7 @@ class Firewall(Router):
|
||||
# check if External Outbound ACL Rules permit frame
|
||||
permitted, rule = self.external_outbound_acl.is_permitted(frame=frame)
|
||||
if not permitted:
|
||||
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
|
||||
self.sys_log.info(f"Frame blocked at external outbound by rule {rule}")
|
||||
return
|
||||
|
||||
self.process_frame(frame=frame, from_network_interface=from_network_interface)
|
||||
@@ -380,7 +380,7 @@ class Firewall(Router):
|
||||
# check if Internal Inbound ACL Rules permit frame
|
||||
permitted, rule = self.internal_inbound_acl.is_permitted(frame=frame)
|
||||
if not permitted:
|
||||
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
|
||||
self.sys_log.info(f"Frame blocked at internal inbound by rule {rule}")
|
||||
return
|
||||
|
||||
self.process_frame(frame=frame, from_network_interface=from_network_interface)
|
||||
@@ -398,7 +398,7 @@ class Firewall(Router):
|
||||
"""
|
||||
permitted, rule = self.internal_outbound_acl.is_permitted(frame)
|
||||
if not permitted:
|
||||
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
|
||||
self.sys_log.info(f"Frame blocked at internal outbound by rule {rule}")
|
||||
return
|
||||
self.software_manager.arp.add_arp_cache_entry(
|
||||
ip_address=frame.ip.src_ip_address,
|
||||
@@ -432,7 +432,7 @@ class Firewall(Router):
|
||||
# check if DMZ Inbound ACL Rules permit frame
|
||||
permitted, rule = self.dmz_inbound_acl.is_permitted(frame=frame)
|
||||
if not permitted:
|
||||
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
|
||||
self.sys_log.info(f"Frame blocked at DMZ inbound by rule {rule}")
|
||||
return
|
||||
|
||||
self.process_frame(frame=frame, from_network_interface=from_network_interface)
|
||||
@@ -452,7 +452,7 @@ class Firewall(Router):
|
||||
"""
|
||||
permitted, rule = self.dmz_outbound_acl.is_permitted(frame)
|
||||
if not permitted:
|
||||
self.sys_log.info(f"Frame blocked at interface {from_network_interface} by rule {rule}")
|
||||
self.sys_log.info(f"Frame blocked at DMZ outbound by rule {rule}")
|
||||
return
|
||||
self.software_manager.arp.add_arp_cache_entry(
|
||||
ip_address=frame.ip.src_ip_address,
|
||||
@@ -688,4 +688,9 @@ class Firewall(Router):
|
||||
next_hop_ip_address=IPv4Address(route.get("next_hop_ip_address")),
|
||||
metric=float(route.get("metric", 0)),
|
||||
)
|
||||
if "default_route" in cfg:
|
||||
next_hop_ip_address = cfg["default_route"].get("next_hop_ip_address", None)
|
||||
if next_hop_ip_address:
|
||||
firewall.route_table.set_default_route_next_hop_ip_address(next_hop_ip_address)
|
||||
|
||||
return firewall
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
import yaml
|
||||
|
||||
from primaite import getLogger, PRIMAITE_PATHS
|
||||
from primaite.game.game import PrimaiteGame
|
||||
from primaite.simulator.network.container import Network
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.host_node import NIC
|
||||
@@ -15,6 +19,8 @@ from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
||||
from primaite.simulator.system.services.web_server.web_server import WebServer
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
def client_server_routed() -> Network:
|
||||
"""
|
||||
@@ -279,3 +285,34 @@ def arcd_uc2_network() -> Network:
|
||||
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=3)
|
||||
|
||||
return network
|
||||
|
||||
|
||||
def _get_example_network(path: str) -> Network:
|
||||
try:
|
||||
with open(path, "r") as file:
|
||||
cfg = yaml.safe_load(file)
|
||||
except FileNotFoundError:
|
||||
msg = f"Failed to locate example network config {path}. Run `primaite setup` to load the example config files."
|
||||
_LOGGER.error(msg)
|
||||
raise FileNotFoundError(msg)
|
||||
game = PrimaiteGame.from_config(cfg)
|
||||
|
||||
return game.simulation.network
|
||||
|
||||
|
||||
def client_server_p2p_network_example() -> Network:
|
||||
"""Get the Client-Server P2P example network."""
|
||||
path = PRIMAITE_PATHS.user_config_path / "example_config" / "client_server_p2p_network_example.yaml"
|
||||
return _get_example_network(path)
|
||||
|
||||
|
||||
def basic_lan_network_example() -> Network:
|
||||
"""Get the basic LAN example network."""
|
||||
path = PRIMAITE_PATHS.user_config_path / "example_config" / "basic_network_network_example.yaml"
|
||||
return _get_example_network(path)
|
||||
|
||||
|
||||
def multi_lan_internet_network_example() -> Network:
|
||||
"""Get Multi-LAN with Internet example network."""
|
||||
path = PRIMAITE_PATHS.user_config_path / "example_config" / "multi_lan_internet_network_example.yaml"
|
||||
return _get_example_network(path)
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
from primaite.simulator.network.hardware.nodes.host.computer import Computer
|
||||
from primaite.simulator.network.hardware.nodes.host.server import Server
|
||||
from primaite.simulator.network.networks import multi_lan_internet_network_example
|
||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||
from src.primaite.simulator.system.applications.web_browser import WebBrowser
|
||||
|
||||
|
||||
def test_all_with_configured_dns_server_ip_can_resolve_url():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
for node in network.nodes.values():
|
||||
dns_client: DNSClient = node.software_manager.software.get("DNSClient")
|
||||
|
||||
if not dns_client:
|
||||
continue
|
||||
|
||||
if dns_client.dns_server:
|
||||
assert dns_client.check_domain_exists("sometech.ai")
|
||||
|
||||
|
||||
def test_external_pcs_can_access_sometech_website():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
pc_1_browser: WebBrowser = network.get_node_by_hostname("pc_1").software_manager.software["WebBrowser"]
|
||||
pc_2_browser: WebBrowser = network.get_node_by_hostname("pc_2").software_manager.software["WebBrowser"]
|
||||
|
||||
assert pc_1_browser.get_webpage()
|
||||
assert pc_2_browser.get_webpage()
|
||||
|
||||
|
||||
def test_external_pcs_cannot_access_sometech_db():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
pc_1_db_client: DatabaseClient = network.get_node_by_hostname("pc_1").software_manager.software["DatabaseClient"]
|
||||
pc_2_db_client: DatabaseClient = network.get_node_by_hostname("pc_2").software_manager.software["DatabaseClient"]
|
||||
|
||||
assert not pc_1_db_client.get_new_connection()
|
||||
assert not pc_2_db_client.get_new_connection()
|
||||
|
||||
|
||||
def test_external_pcs_cannot_access_ftp_on_sometech_storage_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv")
|
||||
some_tech_storage_srv.file_system.create_file(file_name="test.png")
|
||||
|
||||
pc_1_ftp_client: FTPClient = network.get_node_by_hostname("pc_1").software_manager.software["FTPClient"]
|
||||
pc_2_ftp_client: FTPClient = network.get_node_by_hostname("pc_2").software_manager.software["FTPClient"]
|
||||
|
||||
assert not pc_1_ftp_client.request_file(
|
||||
dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address,
|
||||
src_folder_name="root",
|
||||
src_file_name="test.png",
|
||||
dest_folder_name="root",
|
||||
dest_file_name="test.png",
|
||||
)
|
||||
|
||||
assert not pc_2_ftp_client.request_file(
|
||||
dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address,
|
||||
src_folder_name="root",
|
||||
src_file_name="test.png",
|
||||
dest_folder_name="root",
|
||||
dest_file_name="test.png",
|
||||
)
|
||||
|
||||
|
||||
def test_sometech_webserver_can_access_sometech_db_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
web_db_client: DatabaseClient = network.get_node_by_hostname("some_tech_web_srv").software_manager.software[
|
||||
"DatabaseClient"
|
||||
]
|
||||
|
||||
assert web_db_client.get_new_connection()
|
||||
|
||||
|
||||
def test_sometech_webserver_cannot_access_ftp_on_sometech_storage_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv")
|
||||
some_tech_storage_srv.file_system.create_file(file_name="test.png")
|
||||
|
||||
web_server: Server = network.get_node_by_hostname("some_tech_web_srv")
|
||||
|
||||
web_ftp_client: FTPClient = web_server.software_manager.software["FTPClient"]
|
||||
|
||||
assert not web_ftp_client.request_file(
|
||||
dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address,
|
||||
src_folder_name="root",
|
||||
src_file_name="test.png",
|
||||
dest_folder_name="root",
|
||||
dest_file_name="test.png",
|
||||
)
|
||||
|
||||
|
||||
def test_sometech_dev_pcs_can_access_sometech_website():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc")
|
||||
|
||||
snr_dev_browser: WebBrowser = some_tech_snr_dev_pc.software_manager.software["WebBrowser"]
|
||||
|
||||
assert snr_dev_browser.get_webpage()
|
||||
|
||||
some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc")
|
||||
|
||||
jnr_dev_browser: WebBrowser = some_tech_jnr_dev_pc.software_manager.software["WebBrowser"]
|
||||
|
||||
assert jnr_dev_browser.get_webpage()
|
||||
|
||||
|
||||
def test_sometech_dev_pcs_can_connect_to_sometech_db_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc")
|
||||
snr_dev_db_client: DatabaseClient = some_tech_snr_dev_pc.software_manager.software["DatabaseClient"]
|
||||
|
||||
assert snr_dev_db_client.get_new_connection()
|
||||
|
||||
some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc")
|
||||
jnr_dev_db_client: DatabaseClient = some_tech_jnr_dev_pc.software_manager.software["DatabaseClient"]
|
||||
|
||||
assert jnr_dev_db_client.get_new_connection()
|
||||
|
||||
|
||||
def test_sometech_snr_dev_can_access_ftp_on_sometech_storage_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv")
|
||||
some_tech_storage_srv.file_system.create_file(file_name="test.png")
|
||||
|
||||
some_tech_snr_dev_pc: Computer = network.get_node_by_hostname("some_tech_snr_dev_pc")
|
||||
snr_dev_ftp_client: FTPClient = some_tech_snr_dev_pc.software_manager.software["FTPClient"]
|
||||
|
||||
assert snr_dev_ftp_client.request_file(
|
||||
dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address,
|
||||
src_folder_name="root",
|
||||
src_file_name="test.png",
|
||||
dest_folder_name="root",
|
||||
dest_file_name="test.png",
|
||||
)
|
||||
|
||||
|
||||
def test_sometech_jnr_dev_cannot_access_ftp_on_sometech_storage_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv")
|
||||
some_tech_storage_srv.file_system.create_file(file_name="test.png")
|
||||
|
||||
some_tech_jnr_dev_pc: Computer = network.get_node_by_hostname("some_tech_jnr_dev_pc")
|
||||
jnr_dev_ftp_client: FTPClient = some_tech_jnr_dev_pc.software_manager.software["FTPClient"]
|
||||
|
||||
assert not jnr_dev_ftp_client.request_file(
|
||||
dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address,
|
||||
src_folder_name="root",
|
||||
src_file_name="test.png",
|
||||
dest_folder_name="root",
|
||||
dest_file_name="test.png",
|
||||
)
|
||||
|
||||
|
||||
def test_sometech_hr_pc_can_access_sometech_website():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1")
|
||||
|
||||
hr_browser: WebBrowser = some_tech_hr_pc.software_manager.software["WebBrowser"]
|
||||
|
||||
assert hr_browser.get_webpage()
|
||||
|
||||
|
||||
def test_sometech_hr_pc_cannot_access_sometech_db():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1")
|
||||
|
||||
hr_db_client: DatabaseClient = some_tech_hr_pc.software_manager.software["DatabaseClient"]
|
||||
|
||||
assert not hr_db_client.get_new_connection()
|
||||
|
||||
|
||||
def test_sometech_hr_pc_cannot_access_ftp_on_sometech_storage_server():
|
||||
network = multi_lan_internet_network_example()
|
||||
|
||||
some_tech_storage_srv = network.get_node_by_hostname("some_tech_storage_srv")
|
||||
some_tech_storage_srv.file_system.create_file(file_name="test.png")
|
||||
|
||||
some_tech_hr_pc: Computer = network.get_node_by_hostname("some_tech_hr_1")
|
||||
hr_ftp_client: FTPClient = some_tech_hr_pc.software_manager.software["FTPClient"]
|
||||
|
||||
assert not hr_ftp_client.request_file(
|
||||
dest_ip_address=some_tech_storage_srv.network_interface[1].ip_address,
|
||||
src_folder_name="root",
|
||||
src_file_name="test.png",
|
||||
dest_folder_name="root",
|
||||
dest_file_name="test.png",
|
||||
)
|
||||
@@ -1,3 +1,7 @@
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from primaite.simulator.file_system.file import File
|
||||
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
|
||||
from primaite.simulator.file_system.file_type import FileType
|
||||
@@ -41,6 +45,7 @@ def test_file_reveal_to_red_scan(file_system):
|
||||
assert file.revealed_to_red is True
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented")
|
||||
def test_simulated_file_check_hash(file_system):
|
||||
file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder")
|
||||
|
||||
@@ -52,6 +57,7 @@ def test_simulated_file_check_hash(file_system):
|
||||
assert file.health_status == FileSystemItemHealthStatus.CORRUPT
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented")
|
||||
def test_real_file_check_hash(file_system):
|
||||
file: File = file_system.create_file(file_name="test_file.txt", real=True)
|
||||
|
||||
@@ -80,3 +86,14 @@ def test_file_corrupt_repair_restore(file_system):
|
||||
|
||||
file.restore()
|
||||
assert file.health_status == FileSystemItemHealthStatus.GOOD
|
||||
|
||||
|
||||
def test_file_warning_triggered(file_system):
|
||||
file: File = file_system.create_file(file_name="test_file.txt", folder_name="test_folder")
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
file.check_hash()
|
||||
# Check warning issued
|
||||
assert len(w) == 1
|
||||
assert "not implemented" in str(w[-1].message)
|
||||
|
||||
@@ -31,6 +31,7 @@ def test_file_scan_request(populated_file_system):
|
||||
assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented")
|
||||
def test_file_checkhash_request(populated_file_system):
|
||||
"""Test that an agent can request a file hash check."""
|
||||
fs, folder, file = populated_file_system
|
||||
|
||||
@@ -119,6 +119,7 @@ def test_folder_corrupt_repair(file_system):
|
||||
assert file.health_status == FileSystemItemHealthStatus.GOOD
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented")
|
||||
def test_simulated_folder_check_hash(file_system):
|
||||
folder: Folder = file_system.create_folder(folder_name="test_folder")
|
||||
file_system.create_file(file_name="test_file.txt", folder_name="test_folder")
|
||||
@@ -133,6 +134,7 @@ def test_simulated_folder_check_hash(file_system):
|
||||
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="NODE_FILE_CHECKHASH not implemented")
|
||||
def test_real_folder_check_hash(file_system):
|
||||
folder: Folder = file_system.create_folder(folder_name="test_folder")
|
||||
file_system.create_file(file_name="test_file.txt", folder_name="test_folder", real=True)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import warnings
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
@@ -49,6 +50,7 @@ def test_folder_scan_request(populated_file_system):
|
||||
assert file2.visible_health_status == FileSystemItemHealthStatus.CORRUPT
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="NODE_FOLDER_CHECKHASH not implemented")
|
||||
def test_folder_checkhash_request(populated_file_system):
|
||||
"""Test that an agent can request a folder hash check."""
|
||||
fs, folder, file = populated_file_system
|
||||
@@ -62,6 +64,16 @@ def test_folder_checkhash_request(populated_file_system):
|
||||
assert folder.health_status == FileSystemItemHealthStatus.CORRUPT
|
||||
|
||||
|
||||
def test_folder_warning_triggered(populated_file_system):
|
||||
fs, folder, _ = populated_file_system
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
fs.apply_request(request=["folder", folder.name, "checkhash"])
|
||||
# Check warning issued
|
||||
assert len(w) == 1
|
||||
assert "not implemented" in str(w[-1].message)
|
||||
|
||||
|
||||
def test_folder_repair_request(populated_file_system):
|
||||
"""Test that an agent can request a folder repair."""
|
||||
fs, folder, file = populated_file_system
|
||||
|
||||