From 68b22b6444bae2f46a172e4affe9083992960b5b Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Fri, 27 Oct 2023 17:50:41 +0100 Subject: [PATCH] #1961: node scanning + applying timestep to all components within node + node revealing to red --- .../simulator/file_system/file_system.py | 58 ++++++++++++-- .../simulator/network/hardware/base.py | 79 ++++++++++++++++++- .../system/applications/application.py | 8 +- src/primaite/simulator/system/software.py | 4 + tests/conftest.py | 18 ++++- .../_network/_hardware/test_node_actions.py | 67 ++++++++++++++-- 6 files changed, 216 insertions(+), 18 deletions(-) diff --git a/src/primaite/simulator/file_system/file_system.py b/src/primaite/simulator/file_system/file_system.py index cddd276a..af76254d 100644 --- a/src/primaite/simulator/file_system/file_system.py +++ b/src/primaite/simulator/file_system/file_system.py @@ -84,6 +84,9 @@ class FileSystemItemABC(SimComponent): previous_hash: Optional[str] = None "Hash of the file contents or the description state" + revealed_to_red: bool = False + "If true, the folder/file has been revealed to the red agent." + def describe_state(self) -> Dict: """ Produce a dictionary describing the current state of this object. @@ -121,6 +124,10 @@ class FileSystemItemABC(SimComponent): """ return convert_size(self.size) + def reveal_to_red(self): + """Reveals the folder/file to the red agent.""" + self.revealed_to_red = True + @abstractmethod def check_hash(self) -> bool: """ @@ -231,11 +238,24 @@ class FileSystem(SimComponent): state["folders"] = {folder.name: folder.describe_state() for folder in self.folders.values()} return state + def apply_timestep(self, timestep: int) -> None: + """Apply time step to FileSystem and its child folders and files.""" + super().apply_timestep(timestep=timestep) + + # apply timestep to folders + for folder_id in self.folders: + self.folders[folder_id].apply_timestep(timestep=timestep) + def scan(self): - """Scan all the folders and files in the file system.""" + """Scan all the folders (and child files) in the file system.""" for folder_id in self.folders: self.folders[folder_id].scan() + def reveal_to_red(self): + """Reveals all the folders (and child files) in the file system to the red agent.""" + for folder_id in self.folders: + self.folders[folder_id].reveal_to_red() + def create_folder(self, folder_name: str) -> Folder: """ Creates a Folder and adds it to the list of folders. @@ -449,6 +469,9 @@ class Folder(FileSystemItemABC): scan_duration: int = -1 "How many timesteps to complete a scan." + red_scan_duration: int = -1 + "How many timesteps to complete reveal to red scan." + def _init_request_manager(self) -> RequestManager: rm = super()._init_request_manager() rm.add_request( @@ -494,7 +517,7 @@ class Folder(FileSystemItemABC): def apply_timestep(self, timestep: int): """ - Apply a single timestep of simulation dynamics to this service. + Apply a single timestep of simulation dynamics to this folder and its files. In this instance, if any multi-timestep processes are currently occurring (such as scanning), then they are brought one step closer to being finished. @@ -505,14 +528,25 @@ class Folder(FileSystemItemABC): super().apply_timestep(timestep=timestep) # scan files each timestep - if self.scan_duration > -1: + if self.scan_duration >= 0: # scan one file per timestep - file = self.get_file_by_id(file_uuid=list(self.files)[self.scan_duration - 1]) + file = self.get_file_by_id(file_uuid=list(self.files)[self.scan_duration]) file.scan() if file.visible_health_status == FileSystemItemHealthStatus.CORRUPT: self.visible_health_status = FileSystemItemHealthStatus.CORRUPT self.scan_duration -= 1 + # red scan file at each step + if self.red_scan_duration >= 0: + # scan one file per timestep + file = self.get_file_by_id(file_uuid=list(self.files)[self.red_scan_duration]) + file.reveal_to_red() + self.red_scan_duration -= 1 + + # apply timestep to files in folder + for file_id in self.files: + self.files[file_id].apply_timestep(timestep=timestep) + def get_file(self, file_name: str) -> Optional[File]: """ Get a file by its name. @@ -609,14 +643,26 @@ class Folder(FileSystemItemABC): def scan(self) -> None: """Update Folder visible status.""" - if self.scan_duration <= -1: + if self.scan_duration <= 0: # scan one file per timestep - self.scan_duration = len(self.files) + self.scan_duration = len(self.files) - 1 self.fs.sys_log.info(f"Scanning folder {self.name} (id: {self.uuid})") else: # scan already in progress self.fs.sys_log.info(f"Scan is already in progress {self.name} (id: {self.uuid})") + def reveal_to_red(self): + """Reveals the folders and files to the red agent.""" + super().reveal_to_red() + + if self.red_scan_duration <= 0: + # scan one file per timestep + self.red_scan_duration = len(self.files) - 1 + self.fs.sys_log.info(f"Folder revealed to red agent: {self.name} (id: {self.uuid})") + else: + # scan already in progress + self.fs.sys_log.info(f"Red Agent Scan is already in progress {self.name} (id: {self.uuid})") + def check_hash(self) -> bool: """ Runs a :func:`check_hash` on all files in the folder. diff --git a/src/primaite/simulator/network/hardware/base.py b/src/primaite/simulator/network/hardware/base.py index 153a0a15..1858345b 100644 --- a/src/primaite/simulator/network/hardware/base.py +++ b/src/primaite/simulator/network/hardware/base.py @@ -978,7 +978,7 @@ class Node(SimComponent): self._application_request_manager = RequestManager() rm.add_request("application", RequestType(func=self._application_request_manager)) - rm.add_request("scan", RequestType(func=lambda request, context: self.scan(reveal_to_red=True))) + rm.add_request("scan", RequestType(func=lambda request, context: self.reveal_to_red())) rm.add_request("shutdown", RequestType(func=lambda request, context: self.power_off())) rm.add_request("startup", RequestType(func=lambda request, context: self.power_on())) @@ -1060,7 +1060,7 @@ class Node(SimComponent): def apply_timestep(self, timestep: int): """ - Apply a single timestep of simulation dynamics to this service. + Apply a single timestep of simulation dynamics to this node. In this instance, if any multi-timestep processes are currently occurring (such as starting up or shutting down), then they are brought one step closer to @@ -1090,7 +1090,20 @@ class Node(SimComponent): self.operating_state = NodeOperatingState.OFF self.sys_log.info("Turned off") - def scan(self): + # apply time step to node components + if self.operating_state == NodeOperatingState.ON: + for process_id in self.processes: + self.processes[process_id].apply_timestep(timestep=timestep) + + for service_id in self.services: + self.services[service_id].apply_timestep(timestep=timestep) + + for application_id in self.applications: + self.applications[application_id].apply_timestep(timestep=timestep) + + self.file_system.apply_timestep(timestep=timestep) + + def scan(self) -> None: """ Scan the node and all the items within it. @@ -1118,6 +1131,34 @@ class Node(SimComponent): # scan file system self.file_system.scan() + def reveal_to_red(self) -> None: + """ + Reveals the node and all the items within it to the red agent. + + Set all the: + - Processes + - Services + - Applications + - Folders + - Files + + `revealed_to_red` to `True`. + """ + # scan processes + for process_id in self.processes: + self.processes[process_id].reveal_to_red() + + # scan services + for service_id in self.services: + self.services[service_id].reveal_to_red() + + # scan applications + for application_id in self.applications: + self.applications[application_id].reveal_to_red() + + # scan file system + self.file_system.reveal_to_red() + def power_on(self): """Power on the Node, enabling its NICs if it is in the OFF state.""" if self.operating_state == NodeOperatingState.OFF: @@ -1299,6 +1340,38 @@ class Node(SimComponent): _LOGGER.info(f"Removed service {service.uuid} from node {self.uuid}") self._service_request_manager.remove_request(service.uuid) + def install_application(self, application: Application) -> None: + """ + Install an application on this node. + + :param application: Application instance that has not been installed on any node yet. + :type application: Application + """ + if application in self: + _LOGGER.warning(f"Can't add application {application.uuid} to node {self.uuid}. It's already installed.") + return + self.applications[application.uuid] = application + application.parent = self + self.sys_log.info(f"Installed application {application.name}") + _LOGGER.info(f"Added application {application.uuid} to node {self.uuid}") + self._application_request_manager.add_request(application.uuid, RequestType(func=application._request_manager)) + + def uninstall_application(self, application: Application) -> None: + """ + Uninstall and completely remove application from this node. + + :param application: Application object that is currently associated with this node. + :type application: Application + """ + if application not in self: + _LOGGER.warning(f"Can't remove application {application.uuid} from node {self.uuid}. It's not installed.") + return + self.applications.pop(application.uuid) + application.parent = None + self.sys_log.info(f"Uninstalled application {application.name}") + _LOGGER.info(f"Removed application {application.uuid} from node {self.uuid}") + self._application_request_manager.remove_request(application.uuid) + def __contains__(self, item: Any) -> bool: if isinstance(item, Service): return item.uuid in self.services diff --git a/src/primaite/simulator/system/applications/application.py b/src/primaite/simulator/system/applications/application.py index 69b64aac..893e8c3a 100644 --- a/src/primaite/simulator/system/applications/application.py +++ b/src/primaite/simulator/system/applications/application.py @@ -2,7 +2,7 @@ from abc import abstractmethod from enum import Enum from typing import Any, Dict, Set -from primaite.simulator.system.software import IOSoftware +from primaite.simulator.system.software import IOSoftware, SoftwareHealthState class ApplicationOperatingState(Enum): @@ -32,6 +32,12 @@ class Application(IOSoftware): groups: Set[str] = set() "The set of groups to which the application belongs." + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.health_state_visible = SoftwareHealthState.UNUSED + self.health_state_actual = SoftwareHealthState.UNUSED + @abstractmethod def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/software.py b/src/primaite/simulator/system/software.py index cfc0e56f..7527ea40 100644 --- a/src/primaite/simulator/system/software.py +++ b/src/primaite/simulator/system/software.py @@ -177,6 +177,10 @@ class Software(SimComponent): """Update the observed health status to match the actual health status.""" self.health_state_visible = self.health_state_actual + def reveal_to_red(self) -> None: + """Reveals the software to the red agent.""" + self.revealed_to_red = True + class IOSoftware(Software): """ diff --git a/tests/conftest.py b/tests/conftest.py index d8c9cc50..851a3514 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,9 +4,10 @@ import shutil import tempfile from datetime import datetime from pathlib import Path -from typing import Any, Union +from typing import Any, Dict, Union from unittest.mock import patch +import nodeenv import pytest from primaite import getLogger @@ -15,6 +16,7 @@ from primaite.primaite_session import PrimaiteSession from primaite.simulator.network.container import Network from primaite.simulator.network.networks import arcd_uc2_network from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.core.sys_log import SysLog from primaite.simulator.system.services.service import Service from tests.mock_and_patch.get_session_path_mock import get_temp_session_path @@ -36,6 +38,13 @@ class TestService(Service): pass +class TestApplication(Application): + """Test Application class""" + + def describe_state(self) -> Dict: + pass + + @pytest.fixture(scope="function") def uc2_network() -> Network: return arcd_uc2_network() @@ -48,6 +57,13 @@ def service(file_system) -> TestService: ) +@pytest.fixture(scope="function") +def application(file_system) -> TestApplication: + return TestApplication( + name="TestApplication", port=Port.ARP, file_system=file_system, sys_log=SysLog(hostname="test_application") + ) + + @pytest.fixture(scope="function") def file_system() -> FileSystem: return Node(hostname="fs_node").file_system diff --git a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py index de4e0745..f886b8fe 100644 --- a/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py +++ b/tests/unit_tests/_primaite/_simulator/_network/_hardware/test_node_actions.py @@ -1,6 +1,8 @@ import pytest +from primaite.simulator.file_system.file_system import File, FileSystemItemHealthStatus, Folder from primaite.simulator.network.hardware.base import Node, NodeOperatingState +from primaite.simulator.system.applications.application import Application from primaite.simulator.system.processes.process import Process from primaite.simulator.system.services.service import Service from primaite.simulator.system.software import SoftwareHealthState @@ -44,21 +46,31 @@ def test_node_shutdown(node): assert node.operating_state == NodeOperatingState.OFF -def test_node_os_scan(node): +def test_node_os_scan(node, service, application): """Test OS Scanning.""" + node.operating_state = NodeOperatingState.ON + # add process to node - node.processes["process"] = Process(name="process") - node.processes["process"].health_state_actual = SoftwareHealthState.COMPROMISED - assert node.processes["process"].health_state_visible == SoftwareHealthState.GOOD + # TODO implement processes # add services to node - service = Service(name="service") service.health_state_actual = SoftwareHealthState.COMPROMISED node.install_service(service=service) + assert service.health_state_visible == SoftwareHealthState.UNUSED # add application to node + application.health_state_actual = SoftwareHealthState.COMPROMISED + node.install_application(application=application) + assert application.health_state_visible == SoftwareHealthState.UNUSED - # add file to node + # add folder and file to node + folder: Folder = node.file_system.create_folder(folder_name="test_folder") + folder.corrupt() + assert folder.visible_health_status == FileSystemItemHealthStatus.GOOD + + file: File = node.file_system.create_file(folder_name="test_folder", file_name="file.txt") + file.corrupt() + assert file.visible_health_status == FileSystemItemHealthStatus.GOOD # run os scan node.apply_request(["os", "scan"]) @@ -68,5 +80,46 @@ def test_node_os_scan(node): node.apply_timestep(timestep=i) # should update the state of all items - assert node.processes["process"].health_state_visible == SoftwareHealthState.COMPROMISED + # TODO assert process.health_state_visible == SoftwareHealthState.COMPROMISED assert service.health_state_visible == SoftwareHealthState.COMPROMISED + assert application.health_state_visible == SoftwareHealthState.COMPROMISED + assert folder.visible_health_status == FileSystemItemHealthStatus.CORRUPT + assert file.visible_health_status == FileSystemItemHealthStatus.CORRUPT + + +def test_node_red_scan(node, service, application): + """Test revealing to red""" + node.operating_state = NodeOperatingState.ON + + # add process to node + # TODO implement processes + + # add services to node + node.install_service(service=service) + assert service.revealed_to_red is False + + # add application to node + application.health_state_actual = SoftwareHealthState.COMPROMISED + node.install_application(application=application) + assert application.revealed_to_red is False + + # add folder and file to node + folder: Folder = node.file_system.create_folder(folder_name="test_folder") + assert folder.revealed_to_red is False + + file: File = node.file_system.create_file(folder_name="test_folder", file_name="file.txt") + assert file.revealed_to_red is False + + # run os scan + node.apply_request(["scan"]) + + # apply time steps + for i in range(20): + node.apply_timestep(timestep=i) + + # should update the state of all items + # TODO assert process.revealed_to_red is True + assert service.revealed_to_red is True + assert application.revealed_to_red is True + assert folder.revealed_to_red is True + assert file.revealed_to_red is True