Merge remote-tracking branch 'origin/dev' into feature/2476-training-schedules-mockup

This commit is contained in:
Marek Wolan
2024-04-26 16:23:41 +01:00
21 changed files with 502 additions and 156 deletions

View File

@@ -13,7 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added ipywidgets to the dependencies - Added ipywidgets to the dependencies
- Added ability to define scenarios that change depending on the episode number. - Added ability to define scenarios that change depending on the episode number.
- Standardised Environment API by renaming the config parameter of `PrimaiteGymEnv` from `game_config` to `env_config` - Standardised Environment API by renaming the config parameter of `PrimaiteGymEnv` from `game_config` to `env_config`
- Database Connection ID's are now created/issued by DatabaseService and not DatabaseClient
- added ability to set PrimAITE between development and production modes via PrimAITE CLI ``mode`` command - added ability to set PrimAITE between development and production modes via PrimAITE CLI ``mode`` command
- Updated DatabaseClient so that it can now have a single native DatabaseClientConnection along with a collection of DatabaseClientConnection's.
- Implemented the uninstall functionality for DatabaseClient so that all connections are terminated at the DatabaseService.
- 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.
## [Unreleased] ## [Unreleased]
- Made requests fail to reach their target if the node is off - Made requests fail to reach their target if the node is off

View File

@@ -14,13 +14,14 @@ Key features
- Connects to the :ref:`DatabaseService` via the ``SoftwareManager``. - Connects to the :ref:`DatabaseService` via the ``SoftwareManager``.
- Handles connecting and disconnecting. - Handles connecting and disconnecting.
- Handles multiple connections using a dictionary, mapped to connection UIDs
- Executes SQL queries and retrieves result sets. - Executes SQL queries and retrieves result sets.
Usage Usage
===== =====
- Initialise with server IP address and optional password. - Initialise with server IP address and optional password.
- Connect to the :ref:`DatabaseService` with ``connect``. - Connect to the :ref:`DatabaseService` with ``get_new_connection``.
- Retrieve results in a dictionary. - Retrieve results in a dictionary.
- Disconnect when finished. - Disconnect when finished.
@@ -28,6 +29,7 @@ Implementation
============== ==============
- Leverages ``SoftwareManager`` for sending payloads over the network. - Leverages ``SoftwareManager`` for sending payloads over the network.
- Active sessions are held as ``DatabaseClientConnection`` objects in a dictionary.
- Connect and disconnect methods manage sessions. - Connect and disconnect methods manage sessions.
- Payloads serialised as dictionaries for transmission. - Payloads serialised as dictionaries for transmission.
- Extends base Application class. - Extends base Application class.
@@ -63,6 +65,9 @@ Python
database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) # address of the DatabaseService database_client.configure(server_ip_address=IPv4Address("192.168.0.1")) # address of the DatabaseService
database_client.run() database_client.run()
# Establish a new connection
database_client.get_new_connection()
Via Configuration Via Configuration
""""""""""""""""" """""""""""""""""

View File

@@ -1299,7 +1299,6 @@ class Node(SimComponent):
self.services.pop(service.uuid) self.services.pop(service.uuid)
service.parent = None service.parent = None
self.sys_log.info(f"Uninstalled service {service.name}") self.sys_log.info(f"Uninstalled service {service.name}")
_LOGGER.info(f"Removed service {service.name} from node {self.hostname}")
self._service_request_manager.remove_request(service.name) self._service_request_manager.remove_request(service.name)
def install_application(self, application: Application) -> None: def install_application(self, application: Application) -> None:
@@ -1335,7 +1334,6 @@ class Node(SimComponent):
self.applications.pop(application.uuid) self.applications.pop(application.uuid)
application.parent = None application.parent = None
self.sys_log.info(f"Uninstalled application {application.name}") self.sys_log.info(f"Uninstalled application {application.name}")
_LOGGER.info(f"Removed application {application.name} from node {self.hostname}")
self._application_request_manager.remove_request(application.name) self._application_request_manager.remove_request(application.name)
def application_install_action(self, application: Application, ip_address: Optional[str] = None) -> bool: def application_install_action(self, application: Application, ip_address: Optional[str] = None) -> bool:

View File

@@ -1,15 +1,58 @@
from __future__ import annotations
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from uuid import uuid4 from uuid import uuid4
from prettytable import MARKDOWN, PrettyTable
from pydantic import BaseModel
from primaite.interface.request import RequestResponse from primaite.interface.request import RequestResponse
from primaite.simulator.core import RequestManager, RequestType from primaite.simulator.core import RequestManager, RequestType
from primaite.simulator.network.hardware.nodes.host.host_node import HostNode
from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.application import Application
from primaite.simulator.system.core.software_manager import SoftwareManager from primaite.simulator.system.core.software_manager import SoftwareManager
class DatabaseClientConnection(BaseModel):
"""
DatabaseClientConnection Class.
This class is used to record current DatabaseConnections within the DatabaseClient class.
"""
connection_id: str
"""Connection UUID."""
parent_node: HostNode
"""The parent Node that this connection was created on."""
is_active: bool = True
"""Flag to state whether the connection is still active or not."""
@property
def client(self) -> Optional[DatabaseClient]:
"""The DatabaseClient that holds this connection."""
return self.parent_node.software_manager.software.get("DatabaseClient")
def query(self, sql: str) -> bool:
"""
Query the databaseserver.
:return: Boolean value
"""
if self.is_active and self.client:
return self.client._query(connection_id=self.connection_id, sql=sql) # noqa
return False
def disconnect(self):
"""Disconnect the connection."""
if self.client and self.is_active:
self.client._disconnect(self.connection_id) # noqa
class DatabaseClient(Application): class DatabaseClient(Application):
""" """
A DatabaseClient application. A DatabaseClient application.
@@ -22,13 +65,21 @@ class DatabaseClient(Application):
server_ip_address: Optional[IPv4Address] = None server_ip_address: Optional[IPv4Address] = None
server_password: Optional[str] = None server_password: Optional[str] = None
connected: bool = False
_query_success_tracker: Dict[str, bool] = {}
_last_connection_successful: Optional[bool] = None _last_connection_successful: Optional[bool] = None
_query_success_tracker: Dict[str, bool] = {}
"""Keep track of connections that were established or verified during this step. Used for rewards.""" """Keep track of connections that were established or verified during this step. Used for rewards."""
last_query_response: Optional[Dict] = None last_query_response: Optional[Dict] = None
"""Keep track of the latest query response. Used to determine rewards.""" """Keep track of the latest query response. Used to determine rewards."""
_server_connection_id: Optional[str] = None _server_connection_id: Optional[str] = None
"""Connection ID to the Database Server."""
client_connections: Dict[str, DatabaseClientConnection] = {}
"""Keep track of active connections to Database Server."""
_client_connection_requests: Dict[str, Optional[str]] = {}
"""Dictionary of connection requests to Database Server."""
connected: bool = False
"""Boolean Value for whether connected to DB Server."""
native_connection: Optional[DatabaseClientConnection] = None
"""Native Client Connection for using the client directly (similar to psql in a terminal)."""
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs["name"] = "DatabaseClient" kwargs["name"] = "DatabaseClient"
@@ -48,12 +99,18 @@ class DatabaseClient(Application):
def execute(self) -> bool: def execute(self) -> bool:
"""Execution definition for db client: perform a select query.""" """Execution definition for db client: perform a select query."""
if not self._can_perform_action():
return False
self.num_executions += 1 # trying to connect counts as an execution self.num_executions += 1 # trying to connect counts as an execution
if not self._server_connection_id:
if not self.native_connection:
self.connect() self.connect()
can_connect = self.check_connection(connection_id=self._server_connection_id)
self._last_connection_successful = can_connect if self.native_connection:
return can_connect return self.check_connection(connection_id=self.native_connection.connection_id)
return False
def describe_state(self) -> Dict: def describe_state(self) -> Dict:
""" """
@@ -66,6 +123,23 @@ class DatabaseClient(Application):
state["last_connection_successful"] = self._last_connection_successful state["last_connection_successful"] = self._last_connection_successful
return state return state
def show(self, markdown: bool = False):
"""
Display the client connections in tabular format.
:param markdown: Whether to display the table in Markdown format or not. Default is `False`.
"""
table = PrettyTable(["Connection ID", "Active"])
if markdown:
table.set_style(MARKDOWN)
table.align = "l"
table.title = f"{self.sys_log.hostname} {self.name} Client Connections"
if self.native_connection:
table.add_row([self.native_connection.connection_id, self.native_connection.is_active])
for connection_id, connection in self.client_connections.items():
table.add_row([connection_id, connection.is_active])
print(table.get_string(sortby="Connection ID"))
def configure(self, server_ip_address: IPv4Address, server_password: Optional[str] = None): def configure(self, server_ip_address: IPv4Address, server_password: Optional[str] = None):
""" """
Configure the DatabaseClient to communicate with a DatabaseService. Configure the DatabaseClient to communicate with a DatabaseService.
@@ -78,21 +152,17 @@ class DatabaseClient(Application):
self.sys_log.info(f"{self.name}: Configured the {self.name} with {server_ip_address=}, {server_password=}.") self.sys_log.info(f"{self.name}: Configured the {self.name} with {server_ip_address=}, {server_password=}.")
def connect(self) -> bool: def connect(self) -> bool:
"""Connect to a Database Service.""" """Connect the native client connection."""
if not self._can_perform_action(): if self.native_connection:
return False return True
self.native_connection = self.get_new_connection()
return self.native_connection is not None
if not self._server_connection_id: def disconnect(self):
self._server_connection_id = str(uuid4()) """Disconnect the native client connection."""
if self.native_connection:
self.connected = self._connect( self._disconnect(self.native_connection.connection_id)
server_ip_address=self.server_ip_address, self.native_connection = None
password=self.server_password,
connection_id=self._server_connection_id,
)
if not self.connected:
self._server_connection_id = None
return self.connected
def check_connection(self, connection_id: str) -> bool: def check_connection(self, connection_id: str) -> bool:
"""Check whether the connection can be successfully re-established. """Check whether the connection can be successfully re-established.
@@ -104,15 +174,19 @@ class DatabaseClient(Application):
""" """
if not self._can_perform_action(): if not self._can_perform_action():
return False return False
return self.query("SELECT * FROM pg_stat_activity", connection_id=connection_id) return self._query("SELECT * FROM pg_stat_activity", connection_id=connection_id)
def _check_client_connection(self, connection_id: str) -> bool:
"""Check that client_connection_id is valid."""
return True if connection_id in self._client_connection_requests else False
def _connect( def _connect(
self, self,
server_ip_address: IPv4Address, server_ip_address: IPv4Address,
connection_id: Optional[str] = None, connection_request_id: str,
password: Optional[str] = None, password: Optional[str] = None,
is_reattempt: bool = False, is_reattempt: bool = False,
) -> bool: ) -> Optional[DatabaseClientConnection]:
""" """
Connects the DatabaseClient to the DatabaseServer. Connects the DatabaseClient to the DatabaseServer.
@@ -126,56 +200,106 @@ class DatabaseClient(Application):
:type: is_reattempt: Optional[bool] :type: is_reattempt: Optional[bool]
""" """
if is_reattempt: if is_reattempt:
if self._server_connection_id: valid_connection = self._check_client_connection(connection_id=connection_request_id)
if valid_connection:
database_client_connection = self._client_connection_requests.pop(connection_request_id)
self.sys_log.info( self.sys_log.info(
f"{self.name} {connection_id=}: DatabaseClient connection to {server_ip_address} authorised" f"{self.name}: DatabaseClient connection to {server_ip_address} authorised."
f"Connection Request ID was {connection_request_id}."
) )
self.server_ip_address = server_ip_address self.connected = True
return True self._last_connection_successful = True
return database_client_connection
else: else:
self.sys_log.warning( self.sys_log.warning(
f"{self.name} {connection_id=}: DatabaseClient connection to {server_ip_address} declined" f"{self.name}: DatabaseClient connection to {server_ip_address} declined."
f"Connection Request ID was {connection_request_id}."
) )
return False self._last_connection_successful = False
payload = { return None
"type": "connect_request", payload = {"type": "connect_request", "password": password, "connection_request_id": connection_request_id}
"password": password,
"connection_id": connection_id,
}
software_manager: SoftwareManager = self.software_manager software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager( software_manager.send_payload_to_session_manager(
payload=payload, dest_ip_address=server_ip_address, dest_port=self.port payload=payload, dest_ip_address=server_ip_address, dest_port=self.port
) )
return self._connect( return self._connect(
server_ip_address=server_ip_address, password=password, connection_id=connection_id, is_reattempt=True server_ip_address=server_ip_address,
password=password,
is_reattempt=True,
connection_request_id=connection_request_id,
) )
def disconnect(self) -> bool: def _disconnect(self, connection_id: str) -> bool:
"""Disconnect from the Database Service.""" """Disconnect from the Database Service.
If no connection_id is provided, connect from first ID in
self.client_connections.
:param: connection_id: connection ID to disconnect.
:type: connection_id: str
:return: bool
"""
if not self._can_perform_action(): if not self._can_perform_action():
self.sys_log.warning(f"Unable to disconnect - {self.name} is {self.operating_state.name}")
return False return False
# if there are no connections - nothing to disconnect # if there are no connections - nothing to disconnect
if not self._server_connection_id: if len(self.client_connections) == 0:
self.sys_log.warning(f"Unable to disconnect - {self.name} has no active connections.") self.sys_log.warning(f"{self.name}: Unable to disconnect, no active connections.")
return False
if not self.client_connections.get(connection_id):
return False return False
# if no connection provided, disconnect the first connection
software_manager: SoftwareManager = self.software_manager software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager( software_manager.send_payload_to_session_manager(
payload={"type": "disconnect", "connection_id": self._server_connection_id}, payload={"type": "disconnect", "connection_id": connection_id},
dest_ip_address=self.server_ip_address, dest_ip_address=self.server_ip_address,
dest_port=self.port, dest_port=self.port,
) )
self.remove_connection(connection_id=self._server_connection_id) connection = self.client_connections.pop(connection_id)
self.terminate_connection(connection_id=connection_id)
self.sys_log.info( connection.is_active = False
f"{self.name}: DatabaseClient disconnected {self._server_connection_id} from {self.server_ip_address}"
) self.sys_log.info(f"{self.name}: DatabaseClient disconnected {connection_id} from {self.server_ip_address}")
self.connected = False self.connected = False
return True
def _query(self, sql: str, query_id: str, connection_id: str, is_reattempt: bool = False) -> bool: def uninstall(self) -> None:
"""
Uninstall the DatabaseClient.
Calls disconnect on all client connections to ensure that both client and server connections are killed.
"""
while self.client_connections.values():
client_connection = self.client_connections[next(iter(self.client_connections.keys()))]
client_connection.disconnect()
super().uninstall()
def get_new_connection(self) -> Optional[DatabaseClientConnection]:
"""Get a new connection to the DatabaseServer.
:return: DatabaseClientConnection object
"""
if not self._can_perform_action():
return None
connection_request_id = str(uuid4())
self._client_connection_requests[connection_request_id] = None
return self._connect(
server_ip_address=self.server_ip_address,
password=self.server_password,
connection_request_id=connection_request_id,
)
def _create_client_connection(self, connection_id: str, connection_request_id: str) -> None:
"""Create a new DatabaseClientConnection Object."""
client_connection = DatabaseClientConnection(
connection_id=connection_id, client=self, parent_node=self.software_manager.node
)
self.client_connections[connection_id] = client_connection
self._client_connection_requests[connection_request_id] = client_connection
def _query(self, sql: str, connection_id: str, query_id: Optional[str] = False, is_reattempt: bool = False) -> bool:
""" """
Send a query to the connected database server. Send a query to the connected database server.
@@ -185,15 +309,22 @@ class DatabaseClient(Application):
:param: query_id: ID of the query, used as reference :param: query_id: ID of the query, used as reference
:type: query_id: str :type: query_id: str
:param: connection_id: ID of the connection to the database server.
:type: connection_id: str
:param: is_reattempt: True if the query request has been reattempted. Default False :param: is_reattempt: True if the query request has been reattempted. Default False
:type: is_reattempt: Optional[bool] :type: is_reattempt: Optional[bool]
""" """
if not query_id:
query_id = str(uuid4())
if is_reattempt: if is_reattempt:
success = self._query_success_tracker.get(query_id) success = self._query_success_tracker.get(query_id)
if success: if success:
self.sys_log.info(f"{self.name}: Query successful {sql}") self.sys_log.info(f"{self.name}: Query successful {sql}")
self._last_connection_successful = True
return True return True
self.sys_log.error(f"{self.name}: Unable to run query {sql}") self.sys_log.error(f"{self.name}: Unable to run query {sql}")
self._last_connection_successful = False
return False return False
else: else:
software_manager: SoftwareManager = self.software_manager software_manager: SoftwareManager = self.software_manager
@@ -208,39 +339,29 @@ class DatabaseClient(Application):
"""Run the DatabaseClient.""" """Run the DatabaseClient."""
super().run() super().run()
def query(self, sql: str, connection_id: Optional[str] = None) -> bool: def query(self, sql: str) -> bool:
""" """
Send a query to the Database Service. Send a query to the Database Service.
:param: sql: The SQL query. :param: sql: The SQL query.
:param: is_reattempt: If true, the action has been reattempted. :type: sql: str
:return: True if the query was successful, otherwise False. :return: True if the query was successful, otherwise False.
""" """
if not self._can_perform_action(): if not self._can_perform_action():
return False return False
if not self.native_connection:
return False
# reset last query response # reset last query response
self.last_query_response = None self.last_query_response = None
connection_id: str
if not connection_id:
connection_id = self._server_connection_id
if not connection_id:
self.connect()
connection_id = self._server_connection_id
if not connection_id:
msg = "Cannot run sql query, could not establish connection with the server."
self.parent.sys_log.warning(msg)
return False
uuid = str(uuid4()) uuid = str(uuid4())
self._query_success_tracker[uuid] = False self._query_success_tracker[uuid] = False
return self._query(sql=sql, query_id=uuid, connection_id=connection_id) return self.native_connection.query(sql)
def receive(self, payload: Any, session_id: str, **kwargs) -> bool: def receive(self, session_id: str, payload: Any, **kwargs) -> bool:
""" """
Receive a payload from the Software Manager. Receive a payload from the Software Manager.
@@ -250,12 +371,14 @@ class DatabaseClient(Application):
""" """
if not self._can_perform_action(): if not self._can_perform_action():
return False return False
if isinstance(payload, dict) and payload.get("type"): if isinstance(payload, dict) and payload.get("type"):
if payload["type"] == "connect_response": if payload["type"] == "connect_response":
if payload["response"] is True: if payload["response"] is True:
# add connection # add connection
self.add_connection(connection_id=payload.get("connection_id"), session_id=session_id) connection_id = payload["connection_id"]
self._create_client_connection(
connection_id=connection_id, connection_request_id=payload["connection_request_id"]
)
elif payload["type"] == "sql": elif payload["type"] == "sql":
self.last_query_response = payload self.last_query_response = payload
query_id = payload.get("uuid") query_id = payload.get("uuid")
@@ -263,4 +386,8 @@ class DatabaseClient(Application):
self._query_success_tracker[query_id] = status_code == 200 self._query_success_tracker[query_id] = status_code == 200
if self._query_success_tracker[query_id]: if self._query_success_tracker[query_id]:
self.sys_log.debug(f"Received {payload=}") self.sys_log.debug(f"Received {payload=}")
elif payload["type"] == "disconnect":
connection_id = payload["connection_id"]
self.sys_log.info(f"{self.name}: Received disconnect command for {connection_id=} from the server")
self._disconnect(payload["connection_id"])
return True return True

View File

@@ -9,7 +9,7 @@ from primaite.simulator.core import RequestManager, RequestType
from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.application import Application
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -53,6 +53,7 @@ class DataManipulationBot(Application):
kwargs["protocol"] = IPProtocol.NONE kwargs["protocol"] = IPProtocol.NONE
super().__init__(**kwargs) super().__init__(**kwargs)
self._db_connection: Optional[DatabaseClientConnection] = None
def describe_state(self) -> Dict: def describe_state(self) -> Dict:
""" """
@@ -148,6 +149,11 @@ class DataManipulationBot(Application):
self.sys_log.debug(f"{self.name}: ") self.sys_log.debug(f"{self.name}: ")
self.attack_stage = DataManipulationAttackStage.PORT_SCAN self.attack_stage = DataManipulationAttackStage.PORT_SCAN
def _establish_db_connection(self) -> bool:
"""Establish a db connection to the Database Server."""
self._db_connection = self._host_db_client.get_new_connection()
return True if self._db_connection else False
def _perform_data_manipulation(self, p_of_success: Optional[float] = 0.1): def _perform_data_manipulation(self, p_of_success: Optional[float] = 0.1):
""" """
Execute the data manipulation attack on the target. Execute the data manipulation attack on the target.
@@ -167,12 +173,11 @@ class DataManipulationBot(Application):
if simulate_trial(p_of_success): if simulate_trial(p_of_success):
self.sys_log.info(f"{self.name}: Performing data manipulation") self.sys_log.info(f"{self.name}: Performing data manipulation")
# perform the attack # perform the attack
if not len(self._host_db_client.connections): if not self._db_connection:
self._host_db_client.connect() self._establish_db_connection()
if len(self._host_db_client.connections): if self._db_connection:
self._host_db_client.query(self.payload) attack_successful = self._db_connection.query(self.payload)
self.sys_log.info(f"{self.name} payload delivered: {self.payload}") self.sys_log.info(f"{self.name} payload delivered: {self.payload}")
attack_successful = True
if attack_successful: if attack_successful:
self.sys_log.info(f"{self.name}: Data manipulation successful") self.sys_log.info(f"{self.name}: Data manipulation successful")
self.attack_stage = DataManipulationAttackStage.SUCCEEDED self.attack_stage = DataManipulationAttackStage.SUCCEEDED

View File

@@ -8,7 +8,7 @@ from primaite.simulator.core import RequestManager, RequestType
from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import Application from primaite.simulator.system.applications.application import Application
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
class RansomwareAttackStage(IntEnum): class RansomwareAttackStage(IntEnum):
@@ -73,6 +73,7 @@ class RansomwareScript(Application):
kwargs["protocol"] = IPProtocol.NONE kwargs["protocol"] = IPProtocol.NONE
super().__init__(**kwargs) super().__init__(**kwargs)
self._db_connection: Optional[DatabaseClientConnection] = None
def describe_state(self) -> Dict: def describe_state(self) -> Dict:
""" """
@@ -256,6 +257,11 @@ class RansomwareScript(Application):
self.num_executions += 1 self.num_executions += 1
return self._application_loop() return self._application_loop()
def _establish_db_connection(self) -> bool:
"""Establish a db connection to the Database Server."""
self._db_connection = self._host_db_client.get_new_connection()
return True if self._db_connection else False
def _perform_ransomware_encrypt(self): def _perform_ransomware_encrypt(self):
""" """
Execute the Ransomware Encrypt payload on the target. Execute the Ransomware Encrypt payload on the target.
@@ -273,12 +279,11 @@ class RansomwareScript(Application):
if self.attack_stage == RansomwareAttackStage.COMMAND_AND_CONTROL: if self.attack_stage == RansomwareAttackStage.COMMAND_AND_CONTROL:
if simulate_trial(self.ransomware_encrypt_p_of_success): if simulate_trial(self.ransomware_encrypt_p_of_success):
self.sys_log.info(f"{self.name}: Attempting to launch payload") self.sys_log.info(f"{self.name}: Attempting to launch payload")
if not len(self._host_db_client.connections): if not self._db_connection:
self._host_db_client.connect() self._establish_db_connection()
if len(self._host_db_client.connections): if self._db_connection:
self._host_db_client.query(self.payload) attack_successful = self._db_connection.query(self.payload)
self.sys_log.info(f"{self.name} Payload delivered: {self.payload}") self.sys_log.info(f"{self.name} Payload delivered: {self.payload}")
attack_successful = True
if attack_successful: if attack_successful:
self.sys_log.info(f"{self.name}: Payload Successful") self.sys_log.info(f"{self.name}: Payload Successful")
self.attack_stage = RansomwareAttackStage.SUCCEEDED self.attack_stage = RansomwareAttackStage.SUCCEEDED

View File

@@ -113,6 +113,7 @@ class SoftwareManager:
:param software_name: The software name. :param software_name: The software name.
""" """
if software_name in self.software: if software_name in self.software:
self.software[software_name].uninstall()
software = self.software.pop(software_name) # noqa software = self.software.pop(software_name) # noqa
if isinstance(software, Application): if isinstance(software, Application):
self.node.uninstall_application(software) self.node.uninstall_application(software)

View File

@@ -1,5 +1,6 @@
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Any, Dict, List, Literal, Optional, Union from typing import Any, Dict, List, Literal, Optional, Union
from uuid import uuid4
from primaite import getLogger from primaite import getLogger
from primaite.simulator.file_system.file_system import File from primaite.simulator.file_system.file_system import File
@@ -145,8 +146,16 @@ class DatabaseService(Service):
"""Returns the database folder.""" """Returns the database folder."""
return self.file_system.get_folder_by_id(self.db_file.folder_id) return self.file_system.get_folder_by_id(self.db_file.folder_id)
def _generate_connection_id(self) -> str:
"""Generate a unique connection ID."""
return str(uuid4())
def _process_connect( def _process_connect(
self, connection_id: str, password: Optional[str] = None self,
src_ip: IPv4Address,
connection_request_id: str,
password: Optional[str] = None,
session_id: Optional[str] = None,
) -> Dict[str, Union[int, Dict[str, bool]]]: ) -> Dict[str, Union[int, Dict[str, bool]]]:
"""Process an incoming connection request. """Process an incoming connection request.
@@ -158,17 +167,17 @@ class DatabaseService(Service):
:rtype: Dict[str, Union[int, Dict[str, bool]]] :rtype: Dict[str, Union[int, Dict[str, bool]]]
""" """
status_code = 500 # Default internal server error status_code = 500 # Default internal server error
connection_id = None
if self.operating_state == ServiceOperatingState.RUNNING: if self.operating_state == ServiceOperatingState.RUNNING:
status_code = 503 # service unavailable status_code = 503 # service unavailable
if self.health_state_actual == SoftwareHealthState.OVERWHELMED: if self.health_state_actual == SoftwareHealthState.OVERWHELMED:
self.sys_log.error( self.sys_log.error(f"{self.name}: Connect request for {src_ip=} declined. Service is at capacity.")
f"{self.name}: Connect request for {connection_id=} declined. Service is at capacity."
)
if self.health_state_actual == SoftwareHealthState.GOOD: if self.health_state_actual == SoftwareHealthState.GOOD:
if self.password == password: if self.password == password:
status_code = 200 # ok status_code = 200 # ok
connection_id = self._generate_connection_id()
# try to create connection # try to create connection
if not self.add_connection(connection_id=connection_id): if not self.add_connection(connection_id=connection_id, session_id=session_id):
status_code = 500 status_code = 500
self.sys_log.warning(f"{self.name}: Connect request for {connection_id=} declined") self.sys_log.warning(f"{self.name}: Connect request for {connection_id=} declined")
else: else:
@@ -183,6 +192,7 @@ class DatabaseService(Service):
"type": "connect_response", "type": "connect_response",
"response": status_code == 200, "response": status_code == 200,
"connection_id": connection_id, "connection_id": connection_id,
"connection_request_id": connection_request_id,
} }
def _process_sql( def _process_sql(
@@ -299,19 +309,34 @@ class DatabaseService(Service):
:return: True if the Status Code is 200, otherwise False. :return: True if the Status Code is 200, otherwise False.
""" """
result = {"status_code": 500, "data": []} result = {"status_code": 500, "data": []}
# if server service is down, return error # if server service is down, return error
if not self._can_perform_action(): if not self._can_perform_action():
return False return False
if isinstance(payload, dict) and payload.get("type"): if isinstance(payload, dict) and payload.get("type"):
if payload["type"] == "connect_request": if payload["type"] == "connect_request":
src_ip = kwargs.get("frame").ip.src_ip_address
result = self._process_connect( result = self._process_connect(
connection_id=payload.get("connection_id"), password=payload.get("password") src_ip=src_ip,
password=payload.get("password"),
connection_request_id=payload.get("connection_request_id"),
session_id=session_id,
) )
elif payload["type"] == "disconnect": elif payload["type"] == "disconnect":
if payload["connection_id"] in self.connections: if payload["connection_id"] in self.connections:
self.remove_connection(connection_id=payload["connection_id"]) connection_id = payload["connection_id"]
connected_ip_address = self.connections[connection_id]["ip_address"]
frame = kwargs.get("frame")
if connected_ip_address == frame.ip.src_ip_address:
self.sys_log.info(
f"{self.name}: Received disconnect command for {connection_id=} from {connected_ip_address}"
)
self.terminate_connection(connection_id=payload["connection_id"], send_disconnect=False)
else:
self.sys_log.warning(
f"{self.name}: Ignoring disconnect command for {connection_id=} as the command source "
f"({frame.ip.src_ip_address}) doesn't match the connection source ({connected_ip_address})"
)
elif payload["type"] == "sql": elif payload["type"] == "sql":
if payload.get("connection_id") in self.connections: if payload.get("connection_id") in self.connections:
result = self._process_sql( result = self._process_sql(

View File

@@ -276,7 +276,7 @@ class FTPClient(FTPServiceABC):
# if QUIT succeeded, remove the session from active connection list # if QUIT succeeded, remove the session from active connection list
if payload.ftp_command is FTPCommand.QUIT and payload.status_code is FTPStatusCode.OK: if payload.ftp_command is FTPCommand.QUIT and payload.status_code is FTPStatusCode.OK:
self.remove_connection(connection_id=session_id) self.terminate_connection(connection_id=session_id)
self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}") self.sys_log.info(f"{self.name}: Received FTP Response {payload.ftp_command.name} {payload.status_code.value}")

View File

@@ -61,7 +61,7 @@ class FTPServer(FTPServiceABC):
return payload return payload
if payload.ftp_command == FTPCommand.QUIT: if payload.ftp_command == FTPCommand.QUIT:
self.remove_connection(connection_id=session_id) self.terminate_connection(connection_id=session_id)
payload.status_code = FTPStatusCode.OK payload.status_code = FTPStatusCode.OK
return payload return payload

View File

@@ -11,7 +11,7 @@ from primaite.simulator.network.protocols.http import (
) )
from primaite.simulator.network.transmission.network_layer import IPProtocol from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClientConnection
from primaite.simulator.system.services.service import Service from primaite.simulator.system.services.service import Service
from primaite.simulator.system.software import SoftwareHealthState from primaite.simulator.system.software import SoftwareHealthState
@@ -48,6 +48,7 @@ class WebServer(Service):
super().__init__(**kwargs) super().__init__(**kwargs)
self._install_web_files() self._install_web_files()
self.start() self.start()
self.db_connection: Optional[DatabaseClientConnection] = None
def _install_web_files(self): def _install_web_files(self):
""" """
@@ -108,9 +109,11 @@ class WebServer(Service):
if path.startswith("users"): if path.startswith("users"):
# get data from DatabaseServer # get data from DatabaseServer
db_client: DatabaseClient = self.software_manager.software.get("DatabaseClient")
# get all users # get all users
if db_client.query("SELECT"): if not self.db_connection:
self._establish_db_connection()
if self.db_connection.query("SELECT"):
# query succeeded # query succeeded
self.set_health_state(SoftwareHealthState.GOOD) self.set_health_state(SoftwareHealthState.GOOD)
response.status_code = HttpStatusCode.OK response.status_code = HttpStatusCode.OK
@@ -123,6 +126,11 @@ class WebServer(Service):
response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR
return response return response
def _establish_db_connection(self) -> None:
"""Establish a connection to db."""
db_client = self.software_manager.software.get("DatabaseClient")
self.db_connection: DatabaseClientConnection = db_client.get_new_connection()
def send( def send(
self, self,
payload: HttpResponsePacket, payload: HttpResponsePacket,

View File

@@ -5,6 +5,8 @@ from enum import Enum
from ipaddress import IPv4Address, IPv4Network from ipaddress import IPv4Address, IPv4Network
from typing import Any, Dict, Optional, TYPE_CHECKING, Union from typing import Any, Dict, Optional, TYPE_CHECKING, Union
from prettytable import MARKDOWN, PrettyTable
from primaite.interface.request import RequestResponse from primaite.interface.request import RequestResponse
from primaite.simulator.core import RequestManager, RequestType, SimComponent from primaite.simulator.core import RequestManager, RequestType, SimComponent
from primaite.simulator.file_system.file_system import FileSystem, Folder from primaite.simulator.file_system.file_system import FileSystem, Folder
@@ -298,7 +300,7 @@ class IOSoftware(Software):
"""Return the public version of connections.""" """Return the public version of connections."""
return copy.copy(self._connections) return copy.copy(self._connections)
def add_connection(self, connection_id: str, session_id: Optional[str] = None) -> bool: def add_connection(self, connection_id: Union[str, int], session_id: Optional[str] = None) -> bool:
""" """
Create a new connection to this service. Create a new connection to this service.
@@ -323,6 +325,7 @@ class IOSoftware(Software):
if session_id: if session_id:
session_details = self._get_session_details(session_id) session_details = self._get_session_details(session_id)
self._connections[connection_id] = { self._connections[connection_id] = {
"session_id": session_id,
"ip_address": session_details.with_ip_address if session_details else None, "ip_address": session_details.with_ip_address if session_details else None,
"time": datetime.now(), "time": datetime.now(),
} }
@@ -334,19 +337,41 @@ class IOSoftware(Software):
) )
return False return False
def remove_connection(self, connection_id: str) -> bool: def terminate_connection(self, connection_id: str, send_disconnect: bool = True) -> bool:
""" """
Remove a connection from this service. Terminates a connection from this service.
Returns true if connection successfully removed Returns true if connection successfully removed
:param: connection_id: UUID of the connection to create :param: connection_id: UUID of the connection to create
:param send_disconnect: If True, sends a disconnect payload to the ip address of the associated session.
:type: string :type: string
""" """
if self.connections.get(connection_id): if self.connections.get(connection_id):
self._connections.pop(connection_id) connection_dict = self._connections.pop(connection_id)
self.sys_log.info(f"{self.name}: Connection {connection_id=} closed.") if send_disconnect:
return True self.software_manager.send_payload_to_session_manager(
payload={"type": "disconnect", "connection_id": connection_id},
session_id=connection_dict["session_id"],
)
self.sys_log.info(f"{self.name}: Connection {connection_id=} terminated")
return True
return False
def show_connections(self, markdown: bool = False):
"""
Display the connections in tabular format.
:param markdown: Whether to display the table in Markdown format or not. Default is `False`.
"""
table = PrettyTable(["IP Address", "Connection ID", "Creation Timestamp"])
if markdown:
table.set_style(MARKDOWN)
table.align = "l"
table.title = f"{self.sys_log.hostname} {self.name} Connections"
for connection_id, data in self.connections.items():
table.add_row([data["ip_address"], connection_id, str(data["time"])])
print(table.get_string(sortby="Creation Timestamp"))
def clear_connections(self): def clear_connections(self):
"""Clears all the connections from the software.""" """Clears all the connections from the software."""

View File

@@ -4,7 +4,7 @@ from primaite.game.game import PrimaiteGame
from primaite.session.environment import PrimaiteGymEnv from primaite.session.environment import PrimaiteGymEnv
from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.host.server import Server
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot from primaite.simulator.system.applications.red_applications.data_manipulation_bot import DataManipulationBot
from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.database.database_service import DatabaseService
from tests import TEST_ASSETS_ROOT from tests import TEST_ASSETS_ROOT
@@ -20,23 +20,23 @@ def test_data_manipulation(uc2_network):
web_server: Server = uc2_network.get_node_by_hostname("web_server") web_server: Server = uc2_network.get_node_by_hostname("web_server")
db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient")
db_connection: DatabaseClientConnection = db_client.get_new_connection()
db_service.backup_database() db_service.backup_database()
# First check that the DB client on the web_server can successfully query the users table on the database # First check that the DB client on the web_server can successfully query the users table on the database
assert db_client.query("SELECT") assert db_connection.query("SELECT")
# Now we run the DataManipulationBot # Now we run the DataManipulationBot
db_manipulation_bot.attack() db_manipulation_bot.attack()
# Now check that the DB client on the web_server cannot query the users table on the database # Now check that the DB client on the web_server cannot query the users table on the database
assert not db_client.query("SELECT") assert not db_connection.query("SELECT")
# Now restore the database # Now restore the database
db_service.restore_backup() db_service.restore_backup()
# Now check that the DB client on the web_server can successfully query the users table on the database # Now check that the DB client on the web_server can successfully query the users table on the database
assert db_client.query("SELECT") assert db_connection.query("SELECT")
def test_application_install_uninstall_on_uc2(): def test_application_install_uninstall_on_uc2():

View File

@@ -2,7 +2,7 @@ from primaite.game.agent.observations.nic_observations import NICObservation
from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.host.server import Server
from primaite.simulator.network.nmne import set_nmne_config from primaite.simulator.network.nmne import set_nmne_config
from primaite.simulator.sim_container import Simulation from primaite.simulator.sim_container import Simulation
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
def test_capture_nmne(uc2_network): def test_capture_nmne(uc2_network):
@@ -15,7 +15,7 @@ def test_capture_nmne(uc2_network):
""" """
web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa
db_client.connect() db_client_connection: DatabaseClientConnection = db_client.get_new_connection()
db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa
@@ -39,42 +39,42 @@ def test_capture_nmne(uc2_network):
assert db_server_nic.nmne == {} assert db_server_nic.nmne == {}
# Perform a "SELECT" query # Perform a "SELECT" query
db_client.query("SELECT") db_client_connection.query(sql="SELECT")
# Check that it does not trigger an MNE capture. # Check that it does not trigger an MNE capture.
assert web_server_nic.nmne == {} assert web_server_nic.nmne == {}
assert db_server_nic.nmne == {} assert db_server_nic.nmne == {}
# Perform a "DELETE" query # Perform a "DELETE" query
db_client.query("DELETE") db_client_connection.query(sql="DELETE")
# Check that the web server's outbound interface and the database server's inbound interface register the MNE # Check that the web server's outbound interface and the database server's inbound interface register the MNE
assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 1}}}} assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 1}}}}
assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 1}}}} assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 1}}}}
# Perform another "SELECT" query # Perform another "SELECT" query
db_client.query("SELECT") db_client_connection.query(sql="SELECT")
# Check that no additional MNEs are captured # Check that no additional MNEs are captured
assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 1}}}} assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 1}}}}
assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 1}}}} assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 1}}}}
# Perform another "DELETE" query # Perform another "DELETE" query
db_client.query("DELETE") db_client_connection.query(sql="DELETE")
# Check that the web server and database server interfaces register an additional MNE # Check that the web server and database server interfaces register an additional MNE
assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 2}}}} assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 2}}}}
assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 2}}}} assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 2}}}}
# Perform an "ENCRYPT" query # Perform an "ENCRYPT" query
db_client.query("ENCRYPT") db_client_connection.query(sql="ENCRYPT")
# Check that the web server and database server interfaces register an additional MNE # Check that the web server and database server interfaces register an additional MNE
assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 3}}}} assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 3}}}}
assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 3}}}} assert db_server_nic.nmne == {"direction": {"inbound": {"keywords": {"*": 3}}}}
# Perform another "SELECT" query # Perform another "SELECT" query
db_client.query("SELECT") db_client_connection.query(sql="SELECT")
# Check that no additional MNEs are captured # Check that no additional MNEs are captured
assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 3}}}} assert web_server_nic.nmne == {"direction": {"outbound": {"keywords": {"*": 3}}}}
@@ -92,7 +92,7 @@ def test_describe_state_nmne(uc2_network):
""" """
web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa web_server: Server = uc2_network.get_node_by_hostname("web_server") # noqa
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] # noqa
db_client.connect() db_client_connection: DatabaseClientConnection = db_client.get_new_connection()
db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa db_server: Server = uc2_network.get_node_by_hostname("database_server") # noqa
@@ -119,7 +119,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {} assert db_server_nic_state["nmne"] == {}
# Perform a "SELECT" query # Perform a "SELECT" query
db_client.query("SELECT") db_client_connection.query(sql="SELECT")
# Check that it does not trigger an MNE capture. # Check that it does not trigger an MNE capture.
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -129,7 +129,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {} assert db_server_nic_state["nmne"] == {}
# Perform a "DELETE" query # Perform a "DELETE" query
db_client.query("DELETE") db_client_connection.query(sql="DELETE")
# Check that the web server's outbound interface and the database server's inbound interface register the MNE # Check that the web server's outbound interface and the database server's inbound interface register the MNE
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -139,7 +139,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}}
# Perform another "SELECT" query # Perform another "SELECT" query
db_client.query("SELECT") db_client_connection.query(sql="SELECT")
# Check that no additional MNEs are captured # Check that no additional MNEs are captured
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -149,7 +149,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 1}}}}
# Perform another "DELETE" query # Perform another "DELETE" query
db_client.query("DELETE") db_client_connection.query(sql="DELETE")
# Check that the web server and database server interfaces register an additional MNE # Check that the web server and database server interfaces register an additional MNE
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -159,7 +159,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 2}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 2}}}}
# Perform a "ENCRYPT" query # Perform a "ENCRYPT" query
db_client.query("ENCRYPT") db_client_connection.query(sql="ENCRYPT")
# Check that the web server's outbound interface and the database server's inbound interface register the MNE # Check that the web server's outbound interface and the database server's inbound interface register the MNE
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -169,7 +169,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 3}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 3}}}}
# Perform another "SELECT" query # Perform another "SELECT" query
db_client.query("SELECT") db_client_connection.query(sql="SELECT")
# Check that no additional MNEs are captured # Check that no additional MNEs are captured
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -179,7 +179,7 @@ def test_describe_state_nmne(uc2_network):
assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 3}}}} assert db_server_nic_state["nmne"] == {"direction": {"inbound": {"keywords": {"*": 3}}}}
# Perform another "ENCRYPT" # Perform another "ENCRYPT"
db_client.query("ENCRYPT") db_client_connection.query(sql="ENCRYPT")
# Check that the web server and database server interfaces register an additional MNE # Check that the web server and database server interfaces register an additional MNE
web_server_nic_state = web_server_nic.describe_state() web_server_nic_state = web_server_nic.describe_state()
@@ -206,7 +206,7 @@ def test_capture_nmne_observations(uc2_network):
web_server: Server = uc2_network.get_node_by_hostname("web_server") web_server: Server = uc2_network.get_node_by_hostname("web_server")
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"]
db_client.connect() db_client_connection: DatabaseClientConnection = db_client.get_new_connection()
# Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs # Set the NMNE configuration to capture DELETE/ENCRYPT queries as MNEs
nmne_config = { nmne_config = {
@@ -228,7 +228,7 @@ def test_capture_nmne_observations(uc2_network):
for i in range(0, 20): for i in range(0, 20):
# Perform a "DELETE" query each iteration # Perform a "DELETE" query each iteration
for j in range(i): for j in range(i):
db_client.query("DELETE") db_client_connection.query(sql="DELETE")
# Observe the current state of NMNEs from the NICs of both the database and web servers # Observe the current state of NMNEs from the NICs of both the database and web servers
state = sim.describe_state() state = sim.describe_state()
@@ -253,7 +253,7 @@ def test_capture_nmne_observations(uc2_network):
for i in range(0, 20): for i in range(0, 20):
# Perform a "ENCRYPT" query each iteration # Perform a "ENCRYPT" query each iteration
for j in range(i): for j in range(i):
db_client.query("ENCRYPT") db_client_connection.query(sql="ENCRYPT")
# Observe the current state of NMNEs from the NICs of both the database and web servers # Observe the current state of NMNEs from the NICs of both the database and web servers
state = sim.describe_state() state = sim.describe_state()

View File

@@ -10,7 +10,7 @@ from primaite.simulator.network.hardware.nodes.host.server import Server
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.application import ApplicationOperatingState
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
from primaite.simulator.system.applications.red_applications.data_manipulation_bot import ( from primaite.simulator.system.applications.red_applications.data_manipulation_bot import (
DataManipulationAttackStage, DataManipulationAttackStage,
DataManipulationBot, DataManipulationBot,
@@ -141,8 +141,10 @@ def test_data_manipulation_disrupts_green_agent_connection(data_manipulation_db_
server: Server = network.get_node_by_hostname("server_1") server: Server = network.get_node_by_hostname("server_1")
db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService")
green_db_connection: DatabaseClientConnection = green_db_client.get_new_connection()
assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD
assert green_db_client.query("SELECT") assert green_db_connection.query("SELECT")
assert green_db_client.last_query_response.get("status_code") == 200 assert green_db_client.last_query_response.get("status_code") == 200
data_manipulation_bot.port_scan_p_of_success = 1 data_manipulation_bot.port_scan_p_of_success = 1
@@ -151,5 +153,5 @@ def test_data_manipulation_disrupts_green_agent_connection(data_manipulation_db_
data_manipulation_bot.attack() data_manipulation_bot.attack()
assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.COMPROMISED assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.COMPROMISED
assert green_db_client.query("SELECT") is False assert green_db_connection.query("SELECT") is False
assert green_db_client.last_query_response.get("status_code") != 200 assert green_db_client.last_query_response.get("status_code") != 200

View File

@@ -10,7 +10,7 @@ from primaite.simulator.network.hardware.nodes.host.server import Server
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
from primaite.simulator.network.transmission.transport_layer import Port from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.applications.application import ApplicationOperatingState from primaite.simulator.system.applications.application import ApplicationOperatingState
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
from primaite.simulator.system.applications.red_applications.ransomware_script import ( from primaite.simulator.system.applications.red_applications.ransomware_script import (
RansomwareAttackStage, RansomwareAttackStage,
RansomwareScript, RansomwareScript,
@@ -144,12 +144,13 @@ def test_ransomware_disrupts_green_agent_connection(ransomware_script_db_server_
client_2: Computer = network.get_node_by_hostname("client_2") client_2: Computer = network.get_node_by_hostname("client_2")
green_db_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient") green_db_client: DatabaseClient = client_2.software_manager.software.get("DatabaseClient")
green_db_client_connection: DatabaseClientConnection = green_db_client.get_new_connection()
server: Server = network.get_node_by_hostname("server_1") server: Server = network.get_node_by_hostname("server_1")
db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService") db_server_service: DatabaseService = server.software_manager.software.get("DatabaseService")
assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.GOOD
assert green_db_client.query("SELECT") assert green_db_client_connection.query("SELECT")
assert green_db_client.last_query_response.get("status_code") == 200 assert green_db_client.last_query_response.get("status_code") == 200
ransomware_script_application.target_scan_p_of_success = 1 ransomware_script_application.target_scan_p_of_success = 1
@@ -159,5 +160,5 @@ def test_ransomware_disrupts_green_agent_connection(ransomware_script_db_server_
ransomware_script_application.attack() ransomware_script_application.attack()
assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.CORRUPT assert db_server_service.db_file.health_status is FileSystemItemHealthStatus.CORRUPT
assert green_db_client.query("SELECT") is True assert green_db_client_connection.query("SELECT") is True
assert green_db_client.last_query_response.get("status_code") == 200 assert green_db_client.last_query_response.get("status_code") == 200

View File

@@ -8,7 +8,8 @@ from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
from primaite.simulator.network.hardware.nodes.host.computer import Computer from primaite.simulator.network.hardware.nodes.host.computer import Computer
from primaite.simulator.network.hardware.nodes.host.server import Server from primaite.simulator.network.hardware.nodes.host.server import Server
from primaite.simulator.system.applications.database_client import DatabaseClient from primaite.simulator.network.hardware.nodes.network.switch import Switch
from primaite.simulator.system.applications.database_client import DatabaseClient, DatabaseClientConnection
from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.database.database_service import DatabaseService
from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.ftp.ftp_server import FTPServer
from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.service import ServiceOperatingState
@@ -56,11 +57,12 @@ def test_database_client_server_connection(peer_to_peer):
db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] db_service: DatabaseService = node_b.software_manager.software["DatabaseService"]
db_client.connect() db_client.connect()
assert len(db_client.connections) == 1
assert len(db_client.client_connections) == 1
assert len(db_service.connections) == 1 assert len(db_service.connections) == 1
db_client.disconnect() db_client.disconnect()
assert len(db_client.connections) == 0 assert len(db_client.client_connections) == 0
assert len(db_service.connections) == 0 assert len(db_service.connections) == 0
@@ -73,7 +75,7 @@ def test_database_client_server_correct_password(peer_to_peer_secure_db):
db_client.configure(server_ip_address=IPv4Address("192.168.0.11"), server_password="12345") db_client.configure(server_ip_address=IPv4Address("192.168.0.11"), server_password="12345")
db_client.connect() db_client.connect()
assert len(db_client.connections) == 1 assert len(db_client.client_connections) == 1
assert len(db_service.connections) == 1 assert len(db_service.connections) == 1
@@ -95,14 +97,24 @@ def test_database_client_server_incorrect_password(peer_to_peer_secure_db):
assert len(db_service.connections) == 0 assert len(db_service.connections) == 0
def test_database_client_query(uc2_network): def test_database_client_native_connection_query(uc2_network):
"""Tests DB query across the network returns HTTP status 200 and date.""" """Tests DB query across the network returns HTTP status 200 and date."""
web_server: Server = uc2_network.get_node_by_hostname("web_server") web_server: Server = uc2_network.get_node_by_hostname("web_server")
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"] db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"]
db_client.connect()
assert db_client.query("SELECT") assert db_client.query(sql="SELECT")
assert db_client.query("INSERT") assert db_client.query(sql="INSERT")
def test_database_client_connection_query(uc2_network):
"""Tests DB query across the network returns HTTP status 200 and date."""
web_server: Server = uc2_network.get_node_by_hostname("web_server")
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"]
db_connection: DatabaseClientConnection = db_client.get_new_connection()
assert db_connection.query(sql="SELECT")
assert db_connection.query(sql="INSERT")
def test_create_database_backup(uc2_network): def test_create_database_backup(uc2_network):
@@ -172,7 +184,6 @@ def test_restore_backup_after_deleting_file_without_updating_scan(uc2_network):
db_server: Server = uc2_network.get_node_by_hostname("database_server") db_server: Server = uc2_network.get_node_by_hostname("database_server")
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] db_service: DatabaseService = db_server.software_manager.software["DatabaseService"]
# create a back up
assert db_service.backup_database() is True assert db_service.backup_database() is True
db_service.db_file.corrupt() # corrupt the db db_service.db_file.corrupt() # corrupt the db
@@ -211,10 +222,13 @@ def test_database_client_cannot_query_offline_database_server(uc2_network):
web_server: Server = uc2_network.get_node_by_hostname("web_server") web_server: Server = uc2_network.get_node_by_hostname("web_server")
db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient") db_client: DatabaseClient = web_server.software_manager.software.get("DatabaseClient")
assert len(db_client.connections) assert len(db_client.client_connections)
assert db_client.query("SELECT") is True # Establish a new connection to the DatabaseService
assert db_client.query("INSERT") is True db_connection: DatabaseClientConnection = db_client.get_new_connection()
assert db_connection.query("SELECT") is True
assert db_connection.query("INSERT") is True
db_server.power_off() db_server.power_off()
for i in range(db_server.shut_down_duration + 1): for i in range(db_server.shut_down_duration + 1):
@@ -223,5 +237,121 @@ def test_database_client_cannot_query_offline_database_server(uc2_network):
assert db_server.operating_state is NodeOperatingState.OFF assert db_server.operating_state is NodeOperatingState.OFF
assert db_service.operating_state is ServiceOperatingState.STOPPED assert db_service.operating_state is ServiceOperatingState.STOPPED
assert db_client.query("SELECT") is False assert db_connection.query("SELECT") is False
assert db_client.query("INSERT") is False assert db_connection.query("INSERT") is False
def test_database_client_uninstall_terminates_connections(peer_to_peer):
node_a, node_b = peer_to_peer
db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"]
db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa
db_connection: DatabaseClientConnection = db_client.get_new_connection()
# Check that all connection counters are correct and that the client connection can query the database
assert len(db_service.connections) == 1
assert len(db_client.client_connections) == 1
assert db_connection.is_active
assert db_connection.query("SELECT")
# Perform the DatabaseClient uninstall
node_a.software_manager.uninstall("DatabaseClient")
# Check that all connection counters are updated accordingly and client connection can no longer query the database
assert len(db_service.connections) == 0
assert len(db_client.client_connections) == 0
assert not db_connection.query("SELECT")
assert not db_connection.is_active
def test_database_service_can_terminate_connection(peer_to_peer):
node_a, node_b = peer_to_peer
db_client: DatabaseClient = node_a.software_manager.software["DatabaseClient"]
db_service: DatabaseService = node_b.software_manager.software["DatabaseService"] # noqa
db_connection: DatabaseClientConnection = db_client.get_new_connection()
# Check that all connection counters are correct and that the client connection can query the database
assert len(db_service.connections) == 1
assert len(db_client.client_connections) == 1
assert db_connection.is_active
assert db_connection.query("SELECT")
# Perform the server-led connection termination
connection_id = next(iter(db_service.connections.keys()))
db_service.terminate_connection(connection_id)
# Check that all connection counters are updated accordingly and client connection can no longer query the database
assert len(db_service.connections) == 0
assert len(db_client.client_connections) == 0
assert not db_connection.query("SELECT")
assert not db_connection.is_active
def test_client_connection_terminate_does_not_terminate_another_clients_connection():
network = Network()
db_server = Server(
hostname="db_client", ip_address="192.168.0.11", subnet_mask="255.255.255.0", start_up_duration=0
)
db_server.power_on()
db_server.software_manager.install(DatabaseService)
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"] # noqa
db_service.start()
client_a = Computer(
hostname="client_a", ip_address="192.168.0.12", subnet_mask="255.255.255.0", start_up_duration=0
)
client_a.power_on()
client_a.software_manager.install(DatabaseClient)
client_a.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11"))
client_a.software_manager.software["DatabaseClient"].run()
client_b = Computer(
hostname="client_b", ip_address="192.168.0.13", subnet_mask="255.255.255.0", start_up_duration=0
)
client_b.power_on()
client_b.software_manager.install(DatabaseClient)
client_b.software_manager.software["DatabaseClient"].configure(server_ip_address=IPv4Address("192.168.0.11"))
client_b.software_manager.software["DatabaseClient"].run()
switch = Switch(hostname="switch", start_up_duration=0, num_ports=3)
switch.power_on()
network.connect(endpoint_a=switch.network_interface[1], endpoint_b=db_server.network_interface[1])
network.connect(endpoint_a=switch.network_interface[2], endpoint_b=client_a.network_interface[1])
network.connect(endpoint_a=switch.network_interface[3], endpoint_b=client_b.network_interface[1])
db_client_a: DatabaseClient = client_a.software_manager.software["DatabaseClient"] # noqa
db_connection_a = db_client_a.get_new_connection()
assert db_connection_a.query("SELECT")
assert len(db_service.connections) == 1
db_client_b: DatabaseClient = client_b.software_manager.software["DatabaseClient"] # noqa
db_connection_b = db_client_b.get_new_connection()
assert db_connection_b.query("SELECT")
assert len(db_service.connections) == 2
db_connection_a.disconnect()
assert db_connection_b.query("SELECT")
assert len(db_service.connections) == 1

View File

@@ -7,6 +7,7 @@
import pytest import pytest
from primaite.interface.request import RequestResponse from primaite.interface.request import RequestResponse
from primaite.simulator.network.container import Network
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
from primaite.simulator.network.hardware.nodes.host.host_node import HostNode from primaite.simulator.network.hardware.nodes.host.host_node import HostNode
from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router from primaite.simulator.network.hardware.nodes.network.router import ACLAction, Router
@@ -97,7 +98,7 @@ def test_request_fails_if_node_off(example_network, node_request):
class TestDataManipulationGreenRequests: class TestDataManipulationGreenRequests:
def test_node_off(self, uc2_network): def test_node_off(self, uc2_network):
"""Test that green requests succeed when the node is on and fail if the node is off.""" """Test that green requests succeed when the node is on and fail if the node is off."""
net = uc2_network net: Network = uc2_network
client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"]) client_1_browser_execute = net.apply_request(["node", "client_1", "application", "WebBrowser", "execute"])
client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"]) client_1_db_client_execute = net.apply_request(["node", "client_1", "application", "DatabaseClient", "execute"])
@@ -131,7 +132,7 @@ class TestDataManipulationGreenRequests:
def test_acl_block(self, uc2_network): def test_acl_block(self, uc2_network):
"""Test that green requests succeed when not blocked by ACLs but fail when blocked.""" """Test that green requests succeed when not blocked by ACLs but fail when blocked."""
net = uc2_network net: Network = uc2_network
router: Router = net.get_node_by_hostname("router_1") router: Router = net.get_node_by_hostname("router_1")
client_1: HostNode = net.get_node_by_hostname("client_1") client_1: HostNode = net.get_node_by_hostname("client_1")

View File

@@ -70,7 +70,7 @@ def test_dm_bot_perform_data_manipulation_success(dm_bot):
dm_bot._perform_data_manipulation(p_of_success=1.0) dm_bot._perform_data_manipulation(p_of_success=1.0)
assert dm_bot.attack_stage in (DataManipulationAttackStage.SUCCEEDED, DataManipulationAttackStage.FAILED) assert dm_bot.attack_stage in (DataManipulationAttackStage.SUCCEEDED, DataManipulationAttackStage.FAILED)
assert len(dm_bot._host_db_client.connections) assert len(dm_bot._host_db_client.client_connections)
def test_dm_bot_fails_without_db_client(dm_client): def test_dm_bot_fails_without_db_client(dm_client):

View File

@@ -56,7 +56,11 @@ def test_connect_to_database_fails_on_reattempt(database_client_on_computer):
database_client, computer = database_client_on_computer database_client, computer = database_client_on_computer
database_client.connected = False database_client.connected = False
assert database_client._connect(server_ip_address=IPv4Address("192.168.0.1"), is_reattempt=True) is False
database_connection = database_client._connect(
server_ip_address=IPv4Address("192.168.0.1"), connection_request_id="", is_reattempt=True
)
assert database_connection is None
def test_disconnect_when_client_is_closed(database_client_on_computer): def test_disconnect_when_client_is_closed(database_client_on_computer):
@@ -79,21 +83,20 @@ def test_disconnect(database_client_on_computer):
"""Database client should remove the connection.""" """Database client should remove the connection."""
database_client, computer = database_client_on_computer database_client, computer = database_client_on_computer
assert not database_client.connected assert database_client.connected is False
database_client.connect() database_client.connect()
assert database_client.connected assert database_client.connected is True
database_client.disconnect() database_client.disconnect()
assert not database_client.connected assert database_client.connected is False
def test_query_when_client_is_closed(database_client_on_computer): def test_query_when_client_is_closed(database_client_on_computer):
"""Database client should return False when it is not running.""" """Database client should return False when it is not running."""
database_client, computer = database_client_on_computer database_client, computer = database_client_on_computer
database_client.close() database_client.close()
assert database_client.operating_state is ApplicationOperatingState.CLOSED assert database_client.operating_state is ApplicationOperatingState.CLOSED

View File

@@ -1,5 +1,7 @@
from uuid import uuid4 from uuid import uuid4
import pytest
from primaite.simulator.system.services.service import ServiceOperatingState from primaite.simulator.system.services.service import ServiceOperatingState
from primaite.simulator.system.software import SoftwareHealthState from primaite.simulator.system.software import SoftwareHealthState
@@ -179,7 +181,8 @@ def test_overwhelm_service(service):
assert service.health_state_actual is SoftwareHealthState.OVERWHELMED assert service.health_state_actual is SoftwareHealthState.OVERWHELMED
def test_create_and_remove_connections(service): @pytest.mark.xfail(reason="Fails as it's now too simple. Needs to be be refactored so that uses a service on a node.")
def test_create_and_terminate_connections(service):
service.start() service.start()
uuid = str(uuid4()) uuid = str(uuid4())
@@ -187,6 +190,6 @@ def test_create_and_remove_connections(service):
assert len(service.connections) == 1 assert len(service.connections) == 1
assert service.health_state_actual is SoftwareHealthState.GOOD assert service.health_state_actual is SoftwareHealthState.GOOD
assert service.remove_connection(connection_id=uuid) # should be true assert service.terminate_connection(connection_id=uuid) # should be true
assert len(service.connections) == 0 assert len(service.connections) == 0
assert service.health_state_actual is SoftwareHealthState.GOOD assert service.health_state_actual is SoftwareHealthState.GOOD