Merge remote-tracking branch 'origin/dev' into feature/2476-training-schedules-mockup
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
"""""""""""""""""
|
"""""""""""""""""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user