diff --git a/docs/_static/c2_sequence.png b/docs/_static/c2_sequence.png new file mode 100644 index 00000000..9c7ba397 Binary files /dev/null and b/docs/_static/c2_sequence.png differ diff --git a/docs/source/simulation_components/system/applications/c2_suite.rst b/docs/source/simulation_components/system/applications/c2_suite.rst index 28bb1bf8..1fa05466 100644 --- a/docs/source/simulation_components/system/applications/c2_suite.rst +++ b/docs/source/simulation_components/system/applications/c2_suite.rst @@ -99,6 +99,12 @@ However, each host implements it's own receive methods. - Sends C2 Commands to the C2 Beacon via ``C2Payload.INPUT``. - Receives the RequestResponse of the C2 Commands executed by C2 Beacon via ``C2Payload.OUTPUT``. +The sequence diagram below clarifies the functionality of both applications: + +.. image:: ../_static/c2_sequence.png + :width: 500 + :align: center + For further details and more in-depth examples please refer to the ``Command-&-Control notebook`` diff --git a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb index 03e50ae4..b6b13f28 100644 --- a/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb +++ b/src/primaite/notebooks/Command-&-Control-E2E-Demonstration.ipynb @@ -1454,7 +1454,7 @@ "\n", "In the case of the C2 Beacon, the C2 Server's IP address must be supplied before the C2 beacon will be able to perform any other actions (including ``APPLICATION EXECUTE``).\n", "\n", - "If the network contains multiple C2 Servers then it's also possible to switch to a different C2 servers mid-episode which is demonstrated in the below code cells." + "If the network contains multiple C2 Servers then it's also possible to switch to a different C2 server mid-episode which is demonstrated in the below code cells." ] }, { diff --git a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py index 7e9de77e..82e740c5 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/abstract_c2.py @@ -75,7 +75,7 @@ class AbstractC2(Application, identifier="AbstractC2"): keep_alive_inactivity: int = 0 """Indicates how many timesteps since the last time the c2 application received a keep alive.""" - class C2_Opts(BaseModel): + class _C2Opts(BaseModel): """A Pydantic Schema for the different C2 configuration options.""" keep_alive_frequency: int = Field(default=5, ge=1) @@ -87,7 +87,7 @@ class AbstractC2(Application, identifier="AbstractC2"): masquerade_port: Port = Field(default=Port.HTTP) """The currently chosen port that the C2 traffic is masquerading as. Defaults at HTTP.""" - c2_config: C2_Opts = C2_Opts() + c2_config: _C2Opts = _C2Opts() """ Holds the current configuration settings of the C2 Suite. diff --git a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py index 53552e6e..f948d696 100644 --- a/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py +++ b/src/primaite/simulator/system/applications/red_applications/c2/c2_server.py @@ -208,7 +208,7 @@ class C2Server(AbstractC2, identifier="C2Server"): grants more opportunity to the blue agent to prevent attacks. Additionally, future editions of primAITE may expand the C2 repertoire to allow for - more complex red agent behaviour such as file extraction, establishing further fall back channels + more complex red agent behaviour such as establishing further fall back channels or introduce red applications that are only installable via C2 Servers. (T1105) For more information on the impact of these commands please refer to the terminal diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 79216deb..f823e42c 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -52,7 +52,7 @@ class FTPClient(FTPServiceABC): dest_ip = request[-1].get("dest_ip_address") dest_ip = None if dest_ip is None else IPv4Address(dest_ip) - # TODO: Confirm that the default values lead to a safe failure. + # Missing FTP Options results is an automatic failure. src_folder = request[-1].get("src_folder_name", None) src_file_name = request[-1].get("src_file_name", None) dest_folder = request[-1].get("dest_folder_name", None) @@ -63,7 +63,10 @@ class FTPClient(FTPServiceABC): f"{self.name}: Received a FTP Request to transfer file: {src_file_name} to Remote IP: {dest_ip}." ) return RequestResponse( - status="failure", data={"reason": "Unable to locate requested file on local file system."} + status="failure", + data={ + "reason": "Unable to locate given file on local file system. Perhaps given options are invalid?" + }, ) return RequestResponse.from_bool( diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py index fcd13609..22c5d484 100644 --- a/tests/integration_tests/system/test_ftp_client_server.py +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -3,6 +3,7 @@ from typing import Tuple import pytest +from primaite.interface.request import RequestResponse from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.system.services.ftp.ftp_client import FTPClient @@ -105,3 +106,65 @@ def test_ftp_client_tries_to_connect_to_offline_server(ftp_client_and_ftp_server # client should have retrieved the file assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt") is None + + +def test_ftp_client_store_file_in_server_via_request_success(ftp_client_and_ftp_server): + """ + Test checks to see if the client can successfully store files in the backup server via the request manager. + """ + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp client + ftp_client.file_system.create_file(file_name="test_file.txt") + + ftp_opts = { + "src_folder_name": "root", + "src_file_name": "test_file.txt", + "dest_folder_name": "client_1_backup", + "dest_file_name": "test_file.txt", + "dest_ip_address": server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, + } + + ftp_client.apply_request(["send", ftp_opts]) + + assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") + + +def test_ftp_client_store_file_in_server_via_request_failure(ftp_client_and_ftp_server): + """ + Test checks to see if the client fails to store files in the backup server via the request manager. + """ + ftp_client, computer, ftp_server, server = ftp_client_and_ftp_server + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp client + ftp_client.file_system.create_file(file_name="test_file.txt") + + # Purposefully misconfigured FTP Options + ftp_opts = { + "src_folder_name": "root", + "dest_ip_address": server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, + } + + request_response: RequestResponse = ftp_client.apply_request(["send", ftp_opts]) + + assert request_response.status == "failure" + + # Purposefully misconfigured FTP Options + + ftp_opts = { + "src_folder_name": "root", + "src_file_name": "not_a_real_file.txt", + "dest_folder_name": "client_1_backup", + "dest_file_name": "test_file.txt", + "dest_ip_address": server.network_interfaces.get(next(iter(server.network_interfaces))).ip_address, + } + + request_response: RequestResponse = ftp_client.apply_request(["send", ftp_opts]) + + assert request_response.status == "failure"