Merged PR 256: Align the database backup and corruption processes with IYak
## Summary **Changed:** - Copying a file via FTP also copies its health status - The database automatically attempts to make a backup on step 1 - make the db file a property that is fetched by name from the file system instead of a handle to a file (bruh) - fixed ftp server re-sending requests back to the client - fix issue where links with >100% bandwidth cause the observation space to crash - fix issue where starting a node didn't start services. (not sure how that one passed tests previously) **To align with Yak:** - database service removed from uc2 observation space - sql attack affects the file health status instead of the service - when the web server fails to fetch data, it goes into compromised state until a successful data fetch ## Test process Notebooks ## Checklist - [x] PR is linked to a **work item** - [x] **acceptance criteria** of linked ticket are met - [x] performed **self-review** of the code - [ ] written **tests** for any new functionality added with this PR - [ ] updated the **documentation** if this PR changes or adds functionality - [ ] written/updated **design docs** if this PR implements new functionality - [ ] updated the **change log** - [x] ran **pre-commit** checks for code style - [x] attended to any **TO-DOs** left in the code Related work items: #2176
This commit is contained in:
@@ -112,10 +112,8 @@ agents:
|
|||||||
- service_ref: domain_controller_dns_server
|
- service_ref: domain_controller_dns_server
|
||||||
- node_ref: web_server
|
- node_ref: web_server
|
||||||
services:
|
services:
|
||||||
- service_ref: web_server_database_client
|
- service_ref: web_server_web_service
|
||||||
- node_ref: database_server
|
- node_ref: database_server
|
||||||
services:
|
|
||||||
- service_ref: database_service
|
|
||||||
folders:
|
folders:
|
||||||
- folder_name: database
|
- folder_name: database
|
||||||
files:
|
files:
|
||||||
|
|||||||
@@ -205,12 +205,15 @@ class LinkObservation(AbstractObservation):
|
|||||||
|
|
||||||
bandwidth = link_state["bandwidth"]
|
bandwidth = link_state["bandwidth"]
|
||||||
load = link_state["current_load"]
|
load = link_state["current_load"]
|
||||||
utilisation_fraction = load / bandwidth
|
if load == 0:
|
||||||
# 0 is UNUSED, 1 is 0%-10%. 2 is 10%-20%. 3 is 20%-30%. And so on... 10 is exactly 100%
|
utilisation_category = 0
|
||||||
utilisation_category = int(utilisation_fraction * 10) + 1
|
else:
|
||||||
|
utilisation_fraction = load / bandwidth
|
||||||
|
# 0 is UNUSED, 1 is 0%-10%. 2 is 10%-20%. 3 is 20%-30%. And so on... 10 is exactly 100%
|
||||||
|
utilisation_category = int(utilisation_fraction * 9) + 1
|
||||||
|
|
||||||
# TODO: once the links support separte load per protocol, this needs amendment to reflect that.
|
# TODO: once the links support separte load per protocol, this needs amendment to reflect that.
|
||||||
return {"PROTOCOLS": {"ALL": utilisation_category}}
|
return {"PROTOCOLS": {"ALL": min(utilisation_category, 10)}}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def space(self) -> spaces.Space:
|
def space(self) -> spaces.Space:
|
||||||
@@ -555,7 +558,7 @@ class NodeObservation(AbstractObservation):
|
|||||||
folder_configs = config.get("folders", {})
|
folder_configs = config.get("folders", {})
|
||||||
folders = [
|
folders = [
|
||||||
FolderObservation.from_config(
|
FolderObservation.from_config(
|
||||||
config=c, game=game, parent_where=where, num_files_per_folder=num_files_per_folder
|
config=c, game=game, parent_where=where + ["file_system"], num_files_per_folder=num_files_per_folder
|
||||||
)
|
)
|
||||||
for c in folder_configs
|
for c in folder_configs
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class PrimaiteGymEnv(gymnasium.Env):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.game: "PrimaiteGame" = game
|
self.game: "PrimaiteGame" = game
|
||||||
self.agent: ProxyAgent = self.game.rl_agents[0]
|
self.agent: ProxyAgent = self.game.rl_agents[0]
|
||||||
self.flatten_obs: bool = False
|
|
||||||
|
|
||||||
def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]:
|
def step(self, action: ActType) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]:
|
||||||
"""Perform a step in the environment."""
|
"""Perform a step in the environment."""
|
||||||
|
|||||||
@@ -1271,8 +1271,8 @@ class Node(SimComponent):
|
|||||||
self.start_up_countdown = self.start_up_duration
|
self.start_up_countdown = self.start_up_duration
|
||||||
|
|
||||||
if self.start_up_duration <= 0:
|
if self.start_up_duration <= 0:
|
||||||
self._start_up_actions()
|
|
||||||
self.operating_state = NodeOperatingState.ON
|
self.operating_state = NodeOperatingState.ON
|
||||||
|
self._start_up_actions()
|
||||||
self.sys_log.info("Turned on")
|
self.sys_log.info("Turned on")
|
||||||
for nic in self.nics.values():
|
for nic in self.nics.values():
|
||||||
if nic._connected_link:
|
if nic._connected_link:
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ class Application(IOSoftware):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.health_state_visible = SoftwareHealthState.UNUSED
|
|
||||||
self.health_state_actual = SoftwareHealthState.UNUSED
|
|
||||||
|
|
||||||
def set_original_state(self):
|
def set_original_state(self):
|
||||||
"""Sets the original state."""
|
"""Sets the original state."""
|
||||||
super().set_original_state()
|
super().set_original_state()
|
||||||
@@ -95,6 +92,9 @@ class Application(IOSoftware):
|
|||||||
if self.operating_state == ApplicationOperatingState.CLOSED:
|
if self.operating_state == ApplicationOperatingState.CLOSED:
|
||||||
self.sys_log.info(f"Running Application {self.name}")
|
self.sys_log.info(f"Running Application {self.name}")
|
||||||
self.operating_state = ApplicationOperatingState.RUNNING
|
self.operating_state = ApplicationOperatingState.RUNNING
|
||||||
|
# set software health state to GOOD if initially set to UNUSED
|
||||||
|
if self.health_state_actual == SoftwareHealthState.UNUSED:
|
||||||
|
self.set_health_state(SoftwareHealthState.GOOD)
|
||||||
|
|
||||||
def _application_loop(self):
|
def _application_loop(self):
|
||||||
"""The main application loop."""
|
"""The main application loop."""
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from typing import Any, Dict, List, Literal, Optional, Union
|
|||||||
|
|
||||||
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
|
||||||
|
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
|
||||||
|
from primaite.simulator.file_system.folder import Folder
|
||||||
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.core.software_manager import SoftwareManager
|
from primaite.simulator.system.core.software_manager import SoftwareManager
|
||||||
@@ -24,7 +26,7 @@ class DatabaseService(Service):
|
|||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
connections: Dict[str, datetime] = {}
|
connections: Dict[str, datetime] = {}
|
||||||
|
|
||||||
backup_server: IPv4Address = None
|
backup_server_ip: IPv4Address = None
|
||||||
"""IP address of the backup server."""
|
"""IP address of the backup server."""
|
||||||
|
|
||||||
latest_backup_directory: str = None
|
latest_backup_directory: str = None
|
||||||
@@ -38,7 +40,6 @@ class DatabaseService(Service):
|
|||||||
kwargs["port"] = Port.POSTGRES_SERVER
|
kwargs["port"] = Port.POSTGRES_SERVER
|
||||||
kwargs["protocol"] = IPProtocol.TCP
|
kwargs["protocol"] = IPProtocol.TCP
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._db_file: File
|
|
||||||
self._create_db_file()
|
self._create_db_file()
|
||||||
|
|
||||||
def set_original_state(self):
|
def set_original_state(self):
|
||||||
@@ -48,7 +49,7 @@ class DatabaseService(Service):
|
|||||||
vals_to_include = {
|
vals_to_include = {
|
||||||
"password",
|
"password",
|
||||||
"connections",
|
"connections",
|
||||||
"backup_server",
|
"backup_server_ip",
|
||||||
"latest_backup_directory",
|
"latest_backup_directory",
|
||||||
"latest_backup_file_name",
|
"latest_backup_file_name",
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ class DatabaseService(Service):
|
|||||||
|
|
||||||
:param: backup_server_ip: The IP address of the backup server
|
:param: backup_server_ip: The IP address of the backup server
|
||||||
"""
|
"""
|
||||||
self.backup_server = backup_server
|
self.backup_server_ip = backup_server
|
||||||
|
|
||||||
def backup_database(self) -> bool:
|
def backup_database(self) -> bool:
|
||||||
"""Create a backup of the database to the configured backup server."""
|
"""Create a backup of the database to the configured backup server."""
|
||||||
@@ -75,7 +76,7 @@ class DatabaseService(Service):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# check if the backup server was configured
|
# check if the backup server was configured
|
||||||
if self.backup_server is None:
|
if self.backup_server_ip is None:
|
||||||
self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.")
|
self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -84,9 +85,9 @@ class DatabaseService(Service):
|
|||||||
|
|
||||||
# send backup copy of database file to FTP server
|
# send backup copy of database file to FTP server
|
||||||
response = ftp_client_service.send_file(
|
response = ftp_client_service.send_file(
|
||||||
dest_ip_address=self.backup_server,
|
dest_ip_address=self.backup_server_ip,
|
||||||
src_file_name=self._db_file.name,
|
src_file_name=self.db_file.name,
|
||||||
src_folder_name=self.folder.name,
|
src_folder_name="database",
|
||||||
dest_folder_name=str(self.uuid),
|
dest_folder_name=str(self.uuid),
|
||||||
dest_file_name="database.db",
|
dest_file_name="database.db",
|
||||||
)
|
)
|
||||||
@@ -112,7 +113,7 @@ class DatabaseService(Service):
|
|||||||
src_file_name="database.db",
|
src_file_name="database.db",
|
||||||
dest_folder_name="downloads",
|
dest_folder_name="downloads",
|
||||||
dest_file_name="database.db",
|
dest_file_name="database.db",
|
||||||
dest_ip_address=self.backup_server,
|
dest_ip_address=self.backup_server_ip,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
@@ -120,13 +121,10 @@ class DatabaseService(Service):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# replace db file
|
# replace db file
|
||||||
self.file_system.delete_file(folder_name=self.folder.name, file_name="downloads.db")
|
self.file_system.delete_file(folder_name="database", file_name="downloads.db")
|
||||||
self.file_system.copy_file(
|
self.file_system.copy_file(src_folder_name="downloads", src_file_name="database.db", dst_folder_name="database")
|
||||||
src_folder_name="downloads", src_file_name="database.db", dst_folder_name=self.folder.name
|
|
||||||
)
|
|
||||||
self._db_file = self.file_system.get_file(folder_name=self.folder.name, file_name="database.db")
|
|
||||||
|
|
||||||
if self._db_file is None:
|
if self.db_file is None:
|
||||||
self.sys_log.error("Copying database backup failed.")
|
self.sys_log.error("Copying database backup failed.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -136,8 +134,17 @@ class DatabaseService(Service):
|
|||||||
|
|
||||||
def _create_db_file(self):
|
def _create_db_file(self):
|
||||||
"""Creates the Simulation File and sqlite file in the file system."""
|
"""Creates the Simulation File and sqlite file in the file system."""
|
||||||
self._db_file: File = self.file_system.create_file(folder_name="database", file_name="database.db")
|
self.file_system.create_file(folder_name="database", file_name="database.db")
|
||||||
self.folder = self.file_system.get_folder_by_id(self._db_file.folder_id)
|
|
||||||
|
@property
|
||||||
|
def db_file(self) -> File:
|
||||||
|
"""Returns the database file."""
|
||||||
|
return self.file_system.get_file(folder_name="database", file_name="database.db")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def folder(self) -> Folder:
|
||||||
|
"""Returns the database folder."""
|
||||||
|
return self.file_system.get_folder_by_id(self.db_file.folder_id)
|
||||||
|
|
||||||
def _process_connect(
|
def _process_connect(
|
||||||
self, session_id: str, password: Optional[str] = None
|
self, session_id: str, password: Optional[str] = None
|
||||||
@@ -170,16 +177,13 @@ class DatabaseService(Service):
|
|||||||
"""
|
"""
|
||||||
self.sys_log.info(f"{self.name}: Running {query}")
|
self.sys_log.info(f"{self.name}: Running {query}")
|
||||||
if query == "SELECT":
|
if query == "SELECT":
|
||||||
if self.health_state_actual == SoftwareHealthState.GOOD:
|
if self.db_file.health_status == FileSystemItemHealthStatus.GOOD:
|
||||||
return {"status_code": 200, "type": "sql", "data": True, "uuid": query_id}
|
return {"status_code": 200, "type": "sql", "data": True, "uuid": query_id}
|
||||||
else:
|
else:
|
||||||
return {"status_code": 404, "data": False}
|
return {"status_code": 404, "data": False}
|
||||||
elif query == "DELETE":
|
elif query == "DELETE":
|
||||||
if self.health_state_actual == SoftwareHealthState.GOOD:
|
self.db_file.health_status = FileSystemItemHealthStatus.COMPROMISED
|
||||||
self.health_state_actual = SoftwareHealthState.COMPROMISED
|
return {"status_code": 200, "type": "sql", "data": False, "uuid": query_id}
|
||||||
return {"status_code": 200, "type": "sql", "data": False, "uuid": query_id}
|
|
||||||
else:
|
|
||||||
return {"status_code": 404, "data": False}
|
|
||||||
else:
|
else:
|
||||||
# Invalid query
|
# Invalid query
|
||||||
return {"status_code": 500, "data": False}
|
return {"status_code": 500, "data": False}
|
||||||
@@ -233,3 +237,13 @@ class DatabaseService(Service):
|
|||||||
software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
|
software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
|
||||||
|
|
||||||
return payload["status_code"] == 200
|
return payload["status_code"] == 200
|
||||||
|
|
||||||
|
def apply_timestep(self, timestep: int) -> None:
|
||||||
|
"""
|
||||||
|
Apply a single timestep of simulation dynamics to this service.
|
||||||
|
|
||||||
|
Here at the first step, the database backup is created, in addition to normal service update logic.
|
||||||
|
"""
|
||||||
|
if timestep == 1:
|
||||||
|
self.backup_database()
|
||||||
|
return super().apply_timestep(timestep)
|
||||||
|
|||||||
@@ -106,5 +106,5 @@ class FTPServer(FTPServiceABC):
|
|||||||
if payload.status_code is not None:
|
if payload.status_code is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id)
|
self._process_ftp_command(payload=payload, session_id=session_id)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import shutil
|
import shutil
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from typing import Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from primaite.simulator.file_system.file_system import File
|
from primaite.simulator.file_system.file_system import File
|
||||||
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
||||||
@@ -16,6 +16,10 @@ class FTPServiceABC(Service, ABC):
|
|||||||
Contains shared methods between both classes.
|
Contains shared methods between both classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def describe_state(self) -> Dict:
|
||||||
|
"""Returns a Dict of the FTPService state."""
|
||||||
|
return super().describe_state()
|
||||||
|
|
||||||
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket:
|
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket:
|
||||||
"""
|
"""
|
||||||
Process the command in the FTP Packet.
|
Process the command in the FTP Packet.
|
||||||
@@ -52,10 +56,12 @@ class FTPServiceABC(Service, ABC):
|
|||||||
folder_name = payload.ftp_command_args["dest_folder_name"]
|
folder_name = payload.ftp_command_args["dest_folder_name"]
|
||||||
file_size = payload.ftp_command_args["file_size"]
|
file_size = payload.ftp_command_args["file_size"]
|
||||||
real_file_path = payload.ftp_command_args.get("real_file_path")
|
real_file_path = payload.ftp_command_args.get("real_file_path")
|
||||||
|
health_status = payload.ftp_command_args["health_status"]
|
||||||
is_real = real_file_path is not None
|
is_real = real_file_path is not None
|
||||||
file = self.file_system.create_file(
|
file = self.file_system.create_file(
|
||||||
file_name=file_name, folder_name=folder_name, size=file_size, real=is_real
|
file_name=file_name, folder_name=folder_name, size=file_size, real=is_real
|
||||||
)
|
)
|
||||||
|
file.health_status = health_status
|
||||||
self.sys_log.info(
|
self.sys_log.info(
|
||||||
f"{self.name}: Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/"
|
f"{self.name}: Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/"
|
||||||
f"{payload.ftp_command_args['dest_file_name']}"
|
f"{payload.ftp_command_args['dest_file_name']}"
|
||||||
@@ -110,6 +116,7 @@ class FTPServiceABC(Service, ABC):
|
|||||||
"dest_file_name": dest_file_name,
|
"dest_file_name": dest_file_name,
|
||||||
"file_size": file.sim_size,
|
"file_size": file.sim_size,
|
||||||
"real_file_path": file.sim_path if file.real else None,
|
"real_file_path": file.sim_path if file.real else None,
|
||||||
|
"health_status": file.health_status,
|
||||||
},
|
},
|
||||||
packet_payload_size=file.sim_size,
|
packet_payload_size=file.sim_size,
|
||||||
status_code=FTPStatusCode.OK if is_response else None,
|
status_code=FTPStatusCode.OK if is_response else None,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class DataManipulationBot(DatabaseClient):
|
|||||||
payload: Optional[str] = None,
|
payload: Optional[str] = None,
|
||||||
port_scan_p_of_success: float = 0.1,
|
port_scan_p_of_success: float = 0.1,
|
||||||
data_manipulation_p_of_success: float = 0.1,
|
data_manipulation_p_of_success: float = 0.1,
|
||||||
repeat: bool = False,
|
repeat: bool = True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Configure the DataManipulatorBot to communicate with a DatabaseService.
|
Configure the DataManipulatorBot to communicate with a DatabaseService.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
@@ -77,9 +78,6 @@ class Service(IOSoftware):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.health_state_visible = SoftwareHealthState.UNUSED
|
|
||||||
self.health_state_actual = SoftwareHealthState.UNUSED
|
|
||||||
|
|
||||||
def set_original_state(self):
|
def set_original_state(self):
|
||||||
"""Sets the original state."""
|
"""Sets the original state."""
|
||||||
super().set_original_state()
|
super().set_original_state()
|
||||||
@@ -98,6 +96,7 @@ class Service(IOSoftware):
|
|||||||
rm.add_request("enable", RequestType(func=lambda request, context: self.enable()))
|
rm.add_request("enable", RequestType(func=lambda request, context: self.enable()))
|
||||||
return rm
|
return rm
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Produce a dictionary describing the current state of this object.
|
Produce a dictionary describing the current state of this object.
|
||||||
@@ -118,7 +117,6 @@ class Service(IOSoftware):
|
|||||||
if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]:
|
if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]:
|
||||||
self.sys_log.info(f"Stopping service {self.name}")
|
self.sys_log.info(f"Stopping service {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.STOPPED
|
self.operating_state = ServiceOperatingState.STOPPED
|
||||||
self.health_state_actual = SoftwareHealthState.UNUSED
|
|
||||||
|
|
||||||
def start(self, **kwargs) -> None:
|
def start(self, **kwargs) -> None:
|
||||||
"""Start the service."""
|
"""Start the service."""
|
||||||
@@ -129,42 +127,39 @@ class Service(IOSoftware):
|
|||||||
if self.operating_state == ServiceOperatingState.STOPPED:
|
if self.operating_state == ServiceOperatingState.STOPPED:
|
||||||
self.sys_log.info(f"Starting service {self.name}")
|
self.sys_log.info(f"Starting service {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.RUNNING
|
self.operating_state = ServiceOperatingState.RUNNING
|
||||||
self.health_state_actual = SoftwareHealthState.GOOD
|
# set software health state to GOOD if initially set to UNUSED
|
||||||
|
if self.health_state_actual == SoftwareHealthState.UNUSED:
|
||||||
|
self.set_health_state(SoftwareHealthState.GOOD)
|
||||||
|
|
||||||
def pause(self) -> None:
|
def pause(self) -> None:
|
||||||
"""Pause the service."""
|
"""Pause the service."""
|
||||||
if self.operating_state == ServiceOperatingState.RUNNING:
|
if self.operating_state == ServiceOperatingState.RUNNING:
|
||||||
self.sys_log.info(f"Pausing service {self.name}")
|
self.sys_log.info(f"Pausing service {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.PAUSED
|
self.operating_state = ServiceOperatingState.PAUSED
|
||||||
self.health_state_actual = SoftwareHealthState.OVERWHELMED
|
|
||||||
|
|
||||||
def resume(self) -> None:
|
def resume(self) -> None:
|
||||||
"""Resume paused service."""
|
"""Resume paused service."""
|
||||||
if self.operating_state == ServiceOperatingState.PAUSED:
|
if self.operating_state == ServiceOperatingState.PAUSED:
|
||||||
self.sys_log.info(f"Resuming service {self.name}")
|
self.sys_log.info(f"Resuming service {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.RUNNING
|
self.operating_state = ServiceOperatingState.RUNNING
|
||||||
self.health_state_actual = SoftwareHealthState.GOOD
|
|
||||||
|
|
||||||
def restart(self) -> None:
|
def restart(self) -> None:
|
||||||
"""Restart running service."""
|
"""Restart running service."""
|
||||||
if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]:
|
if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]:
|
||||||
self.sys_log.info(f"Pausing service {self.name}")
|
self.sys_log.info(f"Pausing service {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.RESTARTING
|
self.operating_state = ServiceOperatingState.RESTARTING
|
||||||
self.health_state_actual = SoftwareHealthState.OVERWHELMED
|
|
||||||
self.restart_countdown = self.restart_duration
|
self.restart_countdown = self.restart_duration
|
||||||
|
|
||||||
def disable(self) -> None:
|
def disable(self) -> None:
|
||||||
"""Disable the service."""
|
"""Disable the service."""
|
||||||
self.sys_log.info(f"Disabling Application {self.name}")
|
self.sys_log.info(f"Disabling Application {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.DISABLED
|
self.operating_state = ServiceOperatingState.DISABLED
|
||||||
self.health_state_actual = SoftwareHealthState.OVERWHELMED
|
|
||||||
|
|
||||||
def enable(self) -> None:
|
def enable(self) -> None:
|
||||||
"""Enable the disabled service."""
|
"""Enable the disabled service."""
|
||||||
if self.operating_state == ServiceOperatingState.DISABLED:
|
if self.operating_state == ServiceOperatingState.DISABLED:
|
||||||
self.sys_log.info(f"Enabling Application {self.name}")
|
self.sys_log.info(f"Enabling Application {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.STOPPED
|
self.operating_state = ServiceOperatingState.STOPPED
|
||||||
self.health_state_actual = SoftwareHealthState.OVERWHELMED
|
|
||||||
|
|
||||||
def apply_timestep(self, timestep: int) -> None:
|
def apply_timestep(self, timestep: int) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -181,5 +176,4 @@ class Service(IOSoftware):
|
|||||||
if self.restart_countdown <= 0:
|
if self.restart_countdown <= 0:
|
||||||
_LOGGER.debug(f"Restarting finished for service {self.name}")
|
_LOGGER.debug(f"Restarting finished for service {self.name}")
|
||||||
self.operating_state = ServiceOperatingState.RUNNING
|
self.operating_state = ServiceOperatingState.RUNNING
|
||||||
self.health_state_actual = SoftwareHealthState.GOOD
|
|
||||||
self.restart_countdown -= 1
|
self.restart_countdown -= 1
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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 DatabaseClient
|
||||||
from primaite.simulator.system.services.service import Service
|
from primaite.simulator.system.services.service import Service
|
||||||
|
from primaite.simulator.system.software import SoftwareHealthState
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
@@ -123,7 +124,10 @@ class WebServer(Service):
|
|||||||
# get all users
|
# get all users
|
||||||
if db_client.query("SELECT"):
|
if db_client.query("SELECT"):
|
||||||
# query succeeded
|
# query succeeded
|
||||||
|
self.set_health_state(SoftwareHealthState.GOOD)
|
||||||
response.status_code = HttpStatusCode.OK
|
response.status_code = HttpStatusCode.OK
|
||||||
|
else:
|
||||||
|
self.set_health_state(SoftwareHealthState.COMPROMISED)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ class Software(SimComponent):
|
|||||||
|
|
||||||
name: str
|
name: str
|
||||||
"The name of the software."
|
"The name of the software."
|
||||||
health_state_actual: SoftwareHealthState = SoftwareHealthState.GOOD
|
health_state_actual: SoftwareHealthState = SoftwareHealthState.UNUSED
|
||||||
"The actual health state of the software."
|
"The actual health state of the software."
|
||||||
health_state_visible: SoftwareHealthState = SoftwareHealthState.GOOD
|
health_state_visible: SoftwareHealthState = SoftwareHealthState.UNUSED
|
||||||
"The health state of the software visible to the red agent."
|
"The health state of the software visible to the red agent."
|
||||||
criticality: SoftwareCriticality = SoftwareCriticality.LOWEST
|
criticality: SoftwareCriticality = SoftwareCriticality.LOWEST
|
||||||
"The criticality level of the software."
|
"The criticality level of the software."
|
||||||
@@ -278,7 +278,7 @@ class IOSoftware(Software):
|
|||||||
|
|
||||||
Returns true if the software can perform actions.
|
Returns true if the software can perform actions.
|
||||||
"""
|
"""
|
||||||
if self.software_manager and self.software_manager.node.operating_state is not NodeOperatingState.ON:
|
if self.software_manager and self.software_manager.node.operating_state != NodeOperatingState.ON:
|
||||||
_LOGGER.debug(f"{self.name} Error: {self.software_manager.node.hostname} is not online.")
|
_LOGGER.debug(f"{self.name} Error: {self.software_manager.node.hostname} is not online.")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ from primaite.simulator.network.hardware.base import Link, Node
|
|||||||
class TestService(Service):
|
class TestService(Service):
|
||||||
"""Test Service class"""
|
"""Test Service class"""
|
||||||
|
|
||||||
|
def describe_state(self) -> Dict:
|
||||||
|
return super().describe_state()
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs["name"] = "TestService"
|
kwargs["name"] = "TestService"
|
||||||
kwargs["port"] = Port.HTTP
|
kwargs["port"] = Port.HTTP
|
||||||
@@ -60,7 +63,7 @@ class TestApplication(Application):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
pass
|
return super().describe_state()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def test_data_manipulation(uc2_network):
|
|||||||
assert db_client.query("SELECT")
|
assert db_client.query("SELECT")
|
||||||
|
|
||||||
# Now we run the DataManipulationBot
|
# Now we run the DataManipulationBot
|
||||||
db_manipulation_bot.run()
|
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_client.query("SELECT")
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ def populated_node(application_class) -> Tuple[Application, Computer]:
|
|||||||
return app, computer
|
return app, computer
|
||||||
|
|
||||||
|
|
||||||
def test_service_on_offline_node(application_class):
|
def test_application_on_offline_node(application_class):
|
||||||
"""Test to check that the service cannot be interacted with when node it is on is off."""
|
"""Test to check that the application cannot be interacted with when node it is on is off."""
|
||||||
computer: Computer = Computer(
|
computer: Computer = Computer(
|
||||||
hostname="test_computer",
|
hostname="test_computer",
|
||||||
ip_address="192.168.1.2",
|
ip_address="192.168.1.2",
|
||||||
@@ -49,8 +49,8 @@ def test_service_on_offline_node(application_class):
|
|||||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||||
|
|
||||||
|
|
||||||
def test_server_turns_off_service(populated_node):
|
def test_server_turns_off_application(populated_node):
|
||||||
"""Check that the service is turned off when the server is turned off"""
|
"""Check that the application is turned off when the server is turned off"""
|
||||||
app, computer = populated_node
|
app, computer = populated_node
|
||||||
|
|
||||||
assert computer.operating_state is NodeOperatingState.ON
|
assert computer.operating_state is NodeOperatingState.ON
|
||||||
@@ -65,8 +65,8 @@ def test_server_turns_off_service(populated_node):
|
|||||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||||
|
|
||||||
|
|
||||||
def test_service_cannot_be_turned_on_when_server_is_off(populated_node):
|
def test_application_cannot_be_turned_on_when_server_is_off(populated_node):
|
||||||
"""Check that the service cannot be started when the server is off."""
|
"""Check that the application cannot be started when the server is off."""
|
||||||
app, computer = populated_node
|
app, computer = populated_node
|
||||||
|
|
||||||
assert computer.operating_state is NodeOperatingState.ON
|
assert computer.operating_state is NodeOperatingState.ON
|
||||||
@@ -86,8 +86,8 @@ def test_service_cannot_be_turned_on_when_server_is_off(populated_node):
|
|||||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||||
|
|
||||||
|
|
||||||
def test_server_turns_on_service(populated_node):
|
def test_server_turns_on_application(populated_node):
|
||||||
"""Check that turning on the server turns on service."""
|
"""Check that turning on the server turns on application."""
|
||||||
app, computer = populated_node
|
app, computer = populated_node
|
||||||
|
|
||||||
assert computer.operating_state is NodeOperatingState.ON
|
assert computer.operating_state is NodeOperatingState.ON
|
||||||
@@ -109,13 +109,14 @@ def test_server_turns_on_service(populated_node):
|
|||||||
assert computer.operating_state is NodeOperatingState.ON
|
assert computer.operating_state is NodeOperatingState.ON
|
||||||
assert app.operating_state is ApplicationOperatingState.RUNNING
|
assert app.operating_state is ApplicationOperatingState.RUNNING
|
||||||
|
|
||||||
computer.start_up_duration = 0
|
|
||||||
computer.shut_down_duration = 0
|
|
||||||
|
|
||||||
computer.power_off()
|
computer.power_off()
|
||||||
|
for i in range(computer.start_up_duration + 1):
|
||||||
|
computer.apply_timestep(timestep=i)
|
||||||
assert computer.operating_state is NodeOperatingState.OFF
|
assert computer.operating_state is NodeOperatingState.OFF
|
||||||
assert app.operating_state is ApplicationOperatingState.CLOSED
|
assert app.operating_state is ApplicationOperatingState.CLOSED
|
||||||
|
|
||||||
computer.power_on()
|
computer.power_on()
|
||||||
|
for i in range(computer.start_up_duration + 1):
|
||||||
|
computer.apply_timestep(timestep=i)
|
||||||
assert computer.operating_state is NodeOperatingState.ON
|
assert computer.operating_state is NodeOperatingState.ON
|
||||||
assert app.operating_state is ApplicationOperatingState.RUNNING
|
assert app.operating_state is ApplicationOperatingState.RUNNING
|
||||||
|
|||||||
@@ -117,13 +117,14 @@ def test_server_turns_on_service(populated_node):
|
|||||||
assert server.operating_state is NodeOperatingState.ON
|
assert server.operating_state is NodeOperatingState.ON
|
||||||
assert service.operating_state is ServiceOperatingState.RUNNING
|
assert service.operating_state is ServiceOperatingState.RUNNING
|
||||||
|
|
||||||
server.start_up_duration = 0
|
|
||||||
server.shut_down_duration = 0
|
|
||||||
|
|
||||||
server.power_off()
|
server.power_off()
|
||||||
|
for i in range(server.start_up_duration + 1):
|
||||||
|
server.apply_timestep(timestep=i)
|
||||||
assert server.operating_state is NodeOperatingState.OFF
|
assert server.operating_state is NodeOperatingState.OFF
|
||||||
assert service.operating_state is ServiceOperatingState.STOPPED
|
assert service.operating_state is ServiceOperatingState.STOPPED
|
||||||
|
|
||||||
server.power_on()
|
server.power_on()
|
||||||
|
for i in range(server.start_up_duration + 1):
|
||||||
|
server.apply_timestep(timestep=i)
|
||||||
assert server.operating_state is NodeOperatingState.ON
|
assert server.operating_state is NodeOperatingState.ON
|
||||||
assert service.operating_state is ServiceOperatingState.RUNNING
|
assert service.operating_state is ServiceOperatingState.RUNNING
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from ipaddress import IPv4Address
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
|
||||||
from primaite.simulator.network.hardware.base import Node
|
from primaite.simulator.network.hardware.base import Node
|
||||||
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.computer import Computer
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
@@ -42,6 +43,7 @@ def test_ftp_client_store_file(ftp_client):
|
|||||||
"dest_folder_name": "downloads",
|
"dest_folder_name": "downloads",
|
||||||
"dest_file_name": "file.txt",
|
"dest_file_name": "file.txt",
|
||||||
"file_size": 24,
|
"file_size": 24,
|
||||||
|
"health_status": FileSystemItemHealthStatus.GOOD,
|
||||||
},
|
},
|
||||||
packet_payload_size=24,
|
packet_payload_size=24,
|
||||||
status_code=FTPStatusCode.OK,
|
status_code=FTPStatusCode.OK,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from primaite.simulator.file_system.file_system_item_abc import FileSystemItemHealthStatus
|
||||||
from primaite.simulator.network.hardware.base import Node
|
from primaite.simulator.network.hardware.base import Node
|
||||||
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.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
@@ -41,6 +42,7 @@ def test_ftp_server_store_file(ftp_server):
|
|||||||
"dest_folder_name": "downloads",
|
"dest_folder_name": "downloads",
|
||||||
"dest_file_name": "file.txt",
|
"dest_file_name": "file.txt",
|
||||||
"file_size": 24,
|
"file_size": 24,
|
||||||
|
"health_status": FileSystemItemHealthStatus.GOOD,
|
||||||
},
|
},
|
||||||
packet_payload_size=24,
|
packet_payload_size=24,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from primaite.simulator.network.hardware.node_operating_state import NodeOperatingState
|
||||||
from primaite.simulator.network.hardware.nodes.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.network.protocols.http import (
|
from primaite.simulator.network.protocols.http import (
|
||||||
HttpRequestMethod,
|
HttpRequestMethod,
|
||||||
@@ -15,7 +16,11 @@ from primaite.simulator.system.services.web_server.web_server import WebServer
|
|||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def web_server() -> Server:
|
def web_server() -> Server:
|
||||||
node = Server(
|
node = Server(
|
||||||
hostname="web_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
hostname="web_server",
|
||||||
|
ip_address="192.168.1.10",
|
||||||
|
subnet_mask="255.255.255.0",
|
||||||
|
default_gateway="192.168.1.1",
|
||||||
|
operating_state=NodeOperatingState.ON,
|
||||||
)
|
)
|
||||||
node.software_manager.install(software_class=WebServer)
|
node.software_manager.install(software_class=WebServer)
|
||||||
node.software_manager.software.get("WebServer").start()
|
node.software_manager.software.get("WebServer").start()
|
||||||
|
|||||||
Reference in New Issue
Block a user